diff --git a/BUILD_LINUX_CHEATSHEET.md b/BUILD_LINUX_CHEATSHEET.md new file mode 100644 index 0000000000..7d77f5d685 --- /dev/null +++ b/BUILD_LINUX_CHEATSHEET.md @@ -0,0 +1,27 @@ + # this guide is specific to Ubuntu 16.04. + # deb packages of High Fidelity domain server and assignment client are stored on debian.highfidelity.com +sudo su - +apt-get -y update +apt-get install -y software-properties-common +apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 15FF1AAE +add-apt-repository "deb http://debian.highfidelity.com stable main" +apt-get -y update +apt-get install -y hifi-domain-server +apt-get install -y hifi-assignment-client + + # When installing master/dev builds, the packages are slightly different and you just need to change the last 2 steps to: +apt-get install -y hifi-dev-domain-server +apt-get install -y hifi-dev-assignment-client + + # domain server and assignment clients should already be running. The processes are controlled via: +systemctl start hifi-domain-server +systemctl stop hifi-domain-server + + # Once the machine is setup and processes are running you should ensure that your firewall exposes port 40100 on TCP and all UDP ports. This will get your domain up and running and you could connect to it (for now) by using High Fidelity Interface and typing in the IP for the place name. (further customizations can be done via http://IPAddress:40100). + + # The server always depends on both hifi-domain-server and hifi-assignment-client running at the same time. + # As an additional step, you should ensure that your packages are automatically updated when a new version goes out. You could, for example, set the automatic update checks to happen every hour (though this could potentially result in the domain being unreachable for a whole hour by new clients when they are released - adjust the update checks accordingly). +To do this you can modify /etc/crontab by adding the following lines +0 */1 * * * root apt-get update +1 */1 * * * root apt-get install --only-upgrade -y hifi-domain-server +2 */1 * * * root apt-get install --only-upgrade -y hifi-assignment-client diff --git a/assignment-client/src/AssignmentClientApp.cpp b/assignment-client/src/AssignmentClientApp.cpp index dd3050ba4e..b37784cddc 100644 --- a/assignment-client/src/AssignmentClientApp.cpp +++ b/assignment-client/src/AssignmentClientApp.cpp @@ -46,8 +46,14 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : const QCommandLineOption helpOption = parser.addHelpOption(); - const QCommandLineOption clientTypeOption(ASSIGNMENT_TYPE_OVERRIDE_OPTION, - "run single assignment client of given type", "type"); + QString typeDescription = "run single assignment client of given type\n# | Type\n============================"; + for (Assignment::Type type = Assignment::FirstType; + type != Assignment::AllTypes; + type = static_cast(static_cast(type) + 1)) { + typeDescription.append(QStringLiteral("\n%1 | %2").arg(QString::number(type), Assignment::typeToString(type))); + } + const QCommandLineOption clientTypeOption(ASSIGNMENT_TYPE_OVERRIDE_OPTION, typeDescription, "type"); + parser.addOption(clientTypeOption); const QCommandLineOption poolOption(ASSIGNMENT_POOL_OPTION, "set assignment pool", "pool-name"); diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 995a5bad27..ecdf14ebec 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -477,7 +477,7 @@ void EntityServer::startDynamicDomainVerification() { QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location"); QJsonObject request; request["certificate_id"] = i.key(); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c2fe3af7c1..290f4a7f53 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -94,7 +94,7 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, root.insert(requestSubobjectKey, subobject); QJsonDocument doc { root }; - QUrl url { NetworkingConstants::METAVERSE_SERVER_URL.toString() + metaversePath }; + QUrl url { NetworkingConstants::METAVERSE_SERVER_URL().toString() + metaversePath }; QNetworkRequest req(url); req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); @@ -420,7 +420,7 @@ bool DomainServer::optionallySetupOAuth() { // if we don't have an oauth provider URL then we default to the default node auth url if (_oauthProviderURL.isEmpty()) { - _oauthProviderURL = NetworkingConstants::METAVERSE_SERVER_URL; + _oauthProviderURL = NetworkingConstants::METAVERSE_SERVER_URL(); } auto accountManager = DependencyManager::get(); @@ -945,7 +945,7 @@ void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const Q void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet& excludedTypes) { // enumerate over all assignment types and see if we've already excluded it - for (Assignment::Type defaultedType = Assignment::AudioMixerType; + for (Assignment::Type defaultedType = Assignment::FirstType; defaultedType != Assignment::AllTypes; defaultedType = static_cast(static_cast(defaultedType) + 1)) { if (!excludedTypes.contains(defaultedType) && defaultedType != Assignment::AgentType) { @@ -2159,7 +2159,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QJsonDocument doc(root); - QUrl url { NetworkingConstants::METAVERSE_SERVER_URL.toString() + "/api/v1/places/" + place_id }; + QUrl url { NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/api/v1/places/" + place_id }; url.setQuery("access_token=" + accessTokenVariant->toString()); diff --git a/ice-server/src/IceServer.cpp b/ice-server/src/IceServer.cpp index f8bf1f62ae..3cf1e1450e 100644 --- a/ice-server/src/IceServer.cpp +++ b/ice-server/src/IceServer.cpp @@ -208,7 +208,7 @@ void IceServer::requestDomainPublicKey(const QUuid& domainID) { // send a request to the metaverse API for the public key for this domain auto& networkAccessManager = NetworkAccessManager::getInstance(); - QUrl publicKeyURL { NetworkingConstants::METAVERSE_SERVER_URL }; + QUrl publicKeyURL { NetworkingConstants::METAVERSE_SERVER_URL() }; QString publicKeyPath = QString("/api/v1/domains/%1/public_key").arg(uuidStringWithoutCurlyBraces(domainID)); publicKeyURL.setPath(publicKeyPath); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 4c1b8d8d92..b3400c8dd0 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -12,8 +12,10 @@ function(JOIN VALUES GLUE OUTPUT) endfunction() -set(INTERFACE_QML_QRC ${CMAKE_CURRENT_BINARY_DIR}/qml.qrc) -generate_qrc(OUTPUT ${INTERFACE_QML_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources GLOBS *.qml *.qss *.js *.html *.ttf *.gif *.svg *.png *.jpg) +if (NOT DEV_BUILD) + set(INTERFACE_QML_QRC ${CMAKE_CURRENT_BINARY_DIR}/qml.qrc) + generate_qrc(OUTPUT ${INTERFACE_QML_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources GLOBS *.qml *.qss *.js *.html *.ttf *.gif *.svg *.png *.jpg) +endif() # set a default root dir for each of our optional externals if it was not passed set(OPTIONAL_EXTERNALS "LeapMotion") @@ -80,7 +82,9 @@ qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") # add them to the interface source files set(INTERFACE_SRCS ${INTERFACE_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") +if (NOT DEV_BUILD) list(APPEND INTERFACE_SRCS ${INTERFACE_QML_QRC}) +endif() if (UNIX) install( diff --git a/interface/resources/html/createGlobalEventBridge.js b/interface/resources/html/createGlobalEventBridge.js index b85aa33e33..a180fbc6cc 100644 --- a/interface/resources/html/createGlobalEventBridge.js +++ b/interface/resources/html/createGlobalEventBridge.js @@ -34,7 +34,7 @@ var EventBridge; var tempEventBridge = EventBridge; EventBridge = channel.objects.eventBridge; EventBridge.audioOutputDeviceChanged.connect(function(deviceName) { - navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function(mediaStream) { + navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(function(mediaStream) { navigator.mediaDevices.enumerateDevices().then(function(devices) { devices.forEach(function(device) { if (device.kind == "audiooutput") { diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 300bcd46f0..0e2f2a5282 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -52,7 +52,11 @@ Item { targetHeight += hifi.dimensions.contentSpacing.y + additionalInformation.height } - parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); + var newWidth = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); + if(!isNaN(newWidth)) { + parent.width = root.width = newWidth; + } + parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y); } diff --git a/interface/resources/qml/OverlayWindowTest.qml b/interface/resources/qml/OverlayWindowTest.qml new file mode 100644 index 0000000000..7b82b2f705 --- /dev/null +++ b/interface/resources/qml/OverlayWindowTest.qml @@ -0,0 +1,18 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Rectangle { + width: 100 + height: 100 + color: "white" + Rectangle { + width: 10 + height: 10 + color: "red" + } + + Label { + text: OverlayWindowTestString + anchors.centerIn: parent + } +} diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 9a84418b3a..7c1ce704c3 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -22,7 +22,6 @@ Windows.Window { // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false property var source; - property var component; property var dynamicContent; // Keyboard control properties in case needed by QML content. @@ -35,28 +34,9 @@ Windows.Window { dynamicContent.destroy(); dynamicContent = null; } - component = Qt.createComponent(source); - console.log("Created component " + component + " from source " + source); - } - - onComponentChanged: { - console.log("Component changed to " + component) - populate(); - } - - function populate() { - console.log("Populate called: dynamicContent " + dynamicContent + " component " + component); - if (!dynamicContent && component) { - if (component.status == Component.Error) { - console.log("Error loading component:", component.errorString()); - } else if (component.status == Component.Ready) { - console.log("Building dynamic content"); - dynamicContent = component.createObject(contentHolder); - } else { - console.log("Component not yet ready, connecting to status change"); - component.statusChanged.connect(populate); - } - } + QmlSurface.load(source, contentHolder, function(newObject) { + dynamicContent = newObject; + }); } // Handle message traffic from the script that launched us to the loaded QML diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controls-uit/Button.qml index c068fdcfaf..02c6181952 100644 --- a/interface/resources/qml/controls-uit/Button.qml +++ b/interface/resources/qml/controls-uit/Button.qml @@ -29,12 +29,12 @@ Original.Button { onHoveredChanged: { if (hovered) { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); } } onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } style: ButtonStyle { diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 22b25671c3..e60f646327 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -31,12 +31,12 @@ Original.CheckBox { activeFocusOnPress: true onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } // TODO: doesnt works for QQC1. check with QQC2 // onHovered: { -// tabletInterface.playSound(TabletEnums.ButtonHover); +// Tablet.playSound(TabletEnums.ButtonHover); // } style: CheckBoxStyle { diff --git a/interface/resources/qml/controls-uit/CheckBoxQQC2.qml b/interface/resources/qml/controls-uit/CheckBoxQQC2.qml index 32d69cf339..12b8c80003 100644 --- a/interface/resources/qml/controls-uit/CheckBoxQQC2.qml +++ b/interface/resources/qml/controls-uit/CheckBoxQQC2.qml @@ -36,12 +36,12 @@ CheckBox { hoverEnabled: true onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } onHoveredChanged: { if (hovered) { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); } } diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controls-uit/GlyphButton.qml index bc7bc636fe..9c23171ee1 100644 --- a/interface/resources/qml/controls-uit/GlyphButton.qml +++ b/interface/resources/qml/controls-uit/GlyphButton.qml @@ -27,12 +27,12 @@ Original.Button { onHoveredChanged: { if (hovered) { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); } } onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } style: ButtonStyle { diff --git a/interface/resources/qml/controls-uit/Key.qml b/interface/resources/qml/controls-uit/Key.qml index 4ca6cd1b53..b2c720368d 100644 --- a/interface/resources/qml/controls-uit/Key.qml +++ b/interface/resources/qml/controls-uit/Key.qml @@ -41,13 +41,13 @@ Item { onContainsMouseChanged: { if (containsMouse) { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); } } onClicked: { mouse.accepted = true; - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); webEntity.synthesizeKeyPress(glyph); webEntity.synthesizeKeyPress(glyph, mirrorText); diff --git a/interface/resources/qml/controls-uit/RadioButton.qml b/interface/resources/qml/controls-uit/RadioButton.qml index 65d36d2dcb..a818be072b 100644 --- a/interface/resources/qml/controls-uit/RadioButton.qml +++ b/interface/resources/qml/controls-uit/RadioButton.qml @@ -30,12 +30,12 @@ Original.RadioButton { readonly property int checkRadius: 2 onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } // TODO: doesnt works for QQC1. check with QQC2 // onHovered: { -// tabletInterface.playSound(TabletEnums.ButtonHover); +// Tablet.playSound(TabletEnums.ButtonHover); // } style: RadioButtonStyle { diff --git a/interface/resources/qml/controls/FlickableWebViewCore.qml b/interface/resources/qml/controls/FlickableWebViewCore.qml index 6ab4fd9758..efc8519c1e 100644 --- a/interface/resources/qml/controls/FlickableWebViewCore.qml +++ b/interface/resources/qml/controls/FlickableWebViewCore.qml @@ -49,7 +49,7 @@ Item { } if (WebEngineView.LoadFailedStatus === loadRequest.status) { - console.log(" Tablet WebEngineView failed to load url: " + loadRequest.url.toString()); + console.log("Tablet WebEngineView failed to load url: " + loadRequest.url.toString()); } if (WebEngineView.LoadSucceededStatus === loadRequest.status) { diff --git a/interface/resources/qml/dialogs/preferences/ButtonPreference.qml b/interface/resources/qml/dialogs/preferences/ButtonPreference.qml index 3a5c850031..454a9124ae 100644 --- a/interface/resources/qml/dialogs/preferences/ButtonPreference.qml +++ b/interface/resources/qml/dialogs/preferences/ButtonPreference.qml @@ -25,13 +25,13 @@ Preference { id: button onHoveredChanged: { if (hovered) { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); } } onClicked: { preference.trigger(); - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } width: 180 anchors.bottom: parent.bottom diff --git a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml index 8904896ab7..73819839a1 100644 --- a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml +++ b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml @@ -41,12 +41,12 @@ Preference { id: checkBox onHoveredChanged: { if (hovered) { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); } } onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } anchors { diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index fc7b8c6200..83bf1e2c54 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -246,12 +246,12 @@ Item { anchors.fill: parent; acceptedButtons: Qt.LeftButton; onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); goFunction("hifi://" + hifiUrl); } hoverEnabled: true; onEntered: { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); hoverThunk(); } onExited: unhoverThunk(); @@ -269,7 +269,7 @@ Item { } } function go() { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); } MouseArea { diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 96bb359aea..896b3cf10e 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -45,11 +45,13 @@ OriginalDesktop.Desktop { Toolbar { id: sysToolbar; objectName: "com.highfidelity.interface.toolbar.system"; + property var tablet: Tablet.getTablet("com.highfidelity.interface.tablet.system"); anchors.horizontalCenter: settings.constrainToolbarToCenterX ? desktop.horizontalCenter : undefined; // Literal 50 is overwritten by settings from previous session, and sysToolbar.x comes from settings when not constrained. x: sysToolbar.x y: 50 - shown: true + buttonModel: tablet.buttons; + shown: tablet.toolbarMode; } Settings { diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index b6699d6ceb..c66904034b 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -61,12 +61,12 @@ Rectangle { scrollGestureEnabled: false; onClicked: { Audio.muted = !Audio.muted; - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; onContainsMouseChanged: { if (containsMouse) { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); } } } diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index a058f32994..6b7432b8b2 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -41,10 +41,11 @@ Rectangle { property bool debugCheckoutSuccess: false; property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified(); property bool isWearable; + property string referrer; // Style color: hifi.colors.white; - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onWalletStatusResult: { if (walletStatus === 0) { @@ -59,6 +60,7 @@ Rectangle { } else if (walletStatus === 2) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; + UserActivityLogger.commercePassphraseEntry("marketplace checkout"); } } else if (walletStatus === 3) { authSuccessStep(); @@ -71,7 +73,7 @@ Rectangle { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; } else { - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } } @@ -114,7 +116,7 @@ Rectangle { } onItemIdChanged: { - commerce.inventory(); + Commerce.inventory(); itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; } @@ -123,14 +125,14 @@ Rectangle { } onItemPriceChanged: { - commerce.balance(); + Commerce.balance(); } Timer { id: notSetUpTimer; interval: 200; onTriggered: { - sendToScript({method: 'checkout_walletNotSetUp', itemId: itemId}); + sendToScript({method: 'checkout_walletNotSetUp', itemId: itemId, referrer: referrer}); } } @@ -202,7 +204,7 @@ Rectangle { Component.onCompleted: { purchasesReceived = false; balanceReceived = false; - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } } @@ -223,7 +225,7 @@ Rectangle { Connections { target: GlobalServices onMyUsernameChanged: { - commerce.getLoginStatus(); + Commerce.getLoginStatus(); } } @@ -473,9 +475,9 @@ Rectangle { if (itemIsJson) { buyButton.enabled = false; if (!root.shouldBuyWithControlledFailure) { - commerce.buy(itemId, itemPrice); + Commerce.buy(itemId, itemPrice); } else { - commerce.buy(itemId, itemPrice, true); + Commerce.buy(itemId, itemPrice, true); } } else { if (urlHandler.canHandleUrl(itemHref)) { @@ -876,6 +878,7 @@ Rectangle { itemName = message.params.itemName; root.itemPrice = message.params.itemPrice; itemHref = message.params.itemHref; + referrer = message.params.referrer; setBuyText(); break; default: @@ -939,8 +942,8 @@ Rectangle { } root.balanceReceived = false; root.purchasesReceived = false; - commerce.inventory(); - commerce.balance(); + Commerce.inventory(); + Commerce.balance(); } // diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 8b3f017fd2..a4b0f57e8f 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -31,14 +31,14 @@ Item { height: mainContainer.height + additionalDropdownHeight; - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onWalletStatusResult: { if (walletStatus === 0) { sendToParent({method: "needsLogIn"}); } else if (walletStatus === 3) { - commerce.getSecurityImage(); + Commerce.getSecurityImage(); } else if (walletStatus > 3) { console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus); } @@ -48,7 +48,7 @@ Item { if (!isLoggedIn) { sendToParent({method: "needsLogIn"}); } else { - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } } @@ -61,13 +61,13 @@ Item { } Component.onCompleted: { - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } Connections { target: GlobalServices onMyUsernameChanged: { - commerce.getLoginStatus(); + Commerce.getLoginStatus(); } } diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index d98c453a0c..28c32c59de 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -36,8 +36,8 @@ Rectangle { property bool isCertificateInvalid: false; // Style color: hifi.colors.faintGray; - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onCertificateInfoResult: { if (result.status !== 'success') { @@ -109,7 +109,7 @@ Rectangle { onCertificateIdChanged: { if (certificateId !== "") { - commerce.certificateInfo(certificateId); + Commerce.certificateInfo(certificateId); } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index ff9ce16e93..de66be4a88 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -38,8 +38,8 @@ Rectangle { property bool isDebuggingFirstUseTutorial: false; // Style color: hifi.colors.white; - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onWalletStatusResult: { if (walletStatus === 0) { @@ -54,13 +54,14 @@ Rectangle { } else if (walletStatus === 2) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; + UserActivityLogger.commercePassphraseEntry("marketplace purchases"); } } else if (walletStatus === 3) { if ((Settings.getValue("isFirstUseOfPurchases", true) || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { root.activeView = "firstUseTutorial"; } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { root.activeView = "purchasesMain"; - commerce.inventory(); + Commerce.inventory(); } } else { console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus); @@ -71,7 +72,7 @@ Rectangle { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; } else { - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } } @@ -197,7 +198,7 @@ Rectangle { Component.onCompleted: { securityImageResultReceived = false; purchasesReceived = false; - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } } @@ -218,7 +219,7 @@ Rectangle { Connections { target: GlobalServices onMyUsernameChanged: { - commerce.getLoginStatus(); + Commerce.getLoginStatus(); } } @@ -233,7 +234,7 @@ Rectangle { onSendSignalToParent: { if (msg.method === "authSuccess") { root.activeView = "initialize"; - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } else { sendToScript(msg); } @@ -254,7 +255,7 @@ Rectangle { case 'tutorial_finished': Settings.setValue("isFirstUseOfPurchases", false); root.activeView = "purchasesMain"; - commerce.inventory(); + Commerce.inventory(); break; } } @@ -594,7 +595,7 @@ Rectangle { if (root.activeView === "purchasesMain" && !root.pendingInventoryReply) { console.log("Refreshing Purchases..."); root.pendingInventoryReply = true; - commerce.inventory(); + Commerce.inventory(); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml index ebba2b87c6..409833df98 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Help.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -27,8 +27,8 @@ Item { property string keyFilePath; property bool showDebugButtons: true; - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onKeyFilePathIfExistsResult: { root.keyFilePath = path; @@ -37,7 +37,7 @@ Item { onVisibleChanged: { if (visible) { - commerce.getKeyFilePathIfExists(); + Commerce.getKeyFilePathIfExists(); } } @@ -55,6 +55,37 @@ Item { // Style color: hifi.colors.blueHighlight; } + HifiControlsUit.Button { + id: clearCachedPassphraseButton; + visible: root.showDebugButtons; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.left: helpTitleText.right; + anchors.leftMargin: 20; + height: 40; + width: 150; + text: "DBG: Clear Pass"; + onClicked: { + Commerce.setPassphrase(""); + sendSignalToWallet({method: 'passphraseReset'}); + } + } + HifiControlsUit.Button { + id: resetButton; + visible: root.showDebugButtons; + color: hifi.buttons.red; + colorScheme: hifi.colorSchemes.dark; + anchors.top: clearCachedPassphraseButton.top; + anchors.left: clearCachedPassphraseButton.right; + height: 40; + width: 150; + text: "DBG: RST Wallet"; + onClicked: { + Commerce.reset(); + sendSignalToWallet({method: 'walletReset'}); + } + } ListModel { id: helpModel; diff --git a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml index 7ce0cf3853..404d7e84cf 100644 --- a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml +++ b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml @@ -30,8 +30,8 @@ Item { source: "images/wallet-bg.jpg"; } - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; } // diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml index a75d511793..91d2ab9f7f 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml @@ -25,10 +25,6 @@ Item { id: root; - SecurityImageModel { - id: securityImageModel; - } - // Username Text RalewayRegular { id: usernameText; diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index 582052c4c3..87430246f3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -36,8 +36,8 @@ Item { source: "images/wallet-bg.jpg"; } - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onSecurityImageResult: { titleBarSecurityImage.source = ""; @@ -50,8 +50,12 @@ Item { submitPassphraseInputButton.enabled = true; if (!isAuthenticated) { errorText.text = "Authentication failed - please try again."; + passphraseField.error = true; + UserActivityLogger.commercePassphraseAuthenticationStatus("auth failure"); } else { - sendSignalToParent({method: 'authSuccess'});; + sendSignalToParent({method: 'authSuccess'}); + passphraseField.error = false; + UserActivityLogger.commercePassphraseAuthenticationStatus("auth success"); } } } @@ -70,6 +74,7 @@ Item { // TODO: Fix this unlikely bug onVisibleChanged: { if (visible) { + passphraseField.error = false; passphraseField.focus = true; sendSignalToParent({method: 'disableHmdPreview'}); } else { @@ -208,7 +213,7 @@ Item { onAccepted: { submitPassphraseInputButton.enabled = false; - commerce.setPassphrase(passphraseField.text); + Commerce.setPassphrase(passphraseField.text); } } @@ -248,7 +253,7 @@ Item { source: "image://security/securityImage"; cache: false; onVisibleChanged: { - commerce.getSecurityImage(); + Commerce.getSecurityImage(); } } Item { @@ -316,7 +321,7 @@ Item { text: "Submit" onClicked: { submitPassphraseInputButton.enabled = false; - commerce.setPassphrase(passphraseField.text); + Commerce.setPassphrase(passphraseField.text); } } @@ -336,6 +341,7 @@ Item { text: "Cancel" onClicked: { sendSignalToParent({method: 'passphrasePopup_cancelClicked'}); + UserActivityLogger.commercePassphraseAuthenticationStatus("passphrase modal cancelled"); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index 9ba066a9fd..50e58f8cc4 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -36,8 +36,8 @@ Item { propagateComposedEvents: false; } - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onSecurityImageResult: { passphrasePageSecurityImage.source = ""; passphrasePageSecurityImage.source = "image://security/securityImage"; @@ -54,6 +54,9 @@ Item { // TODO: Fix this unlikely bug onVisibleChanged: { if (visible) { + passphraseField.error = false; + passphraseFieldAgain.error = false; + currentPassphraseField.error = false; if (root.shouldImmediatelyFocus) { focusFirstTextField(); } @@ -160,7 +163,7 @@ Item { source: "image://security/securityImage"; cache: false; onVisibleChanged: { - commerce.getSecurityImage(); + Commerce.getSecurityImage(); } } Item { @@ -283,7 +286,7 @@ Item { passphraseFieldAgain.error = false; currentPassphraseField.error = false; setErrorText(""); - commerce.changePassphrase(currentPassphraseField.text, passphraseField.text); + Commerce.changePassphrase(currentPassphraseField.text, passphraseField.text); return true; } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml index 485b0ec086..d825196655 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Security.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Security.qml @@ -27,8 +27,8 @@ Item { id: root; property string keyFilePath; - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onKeyFilePathIfExistsResult: { root.keyFilePath = path; @@ -234,7 +234,7 @@ Item { onVisibleChanged: { if (visible) { - commerce.getKeyFilePathIfExists(); + Commerce.getKeyFilePathIfExists(); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml index 7f767060f6..b261743434 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml @@ -26,12 +26,8 @@ Item { id: root; property bool justSubmitted: false; - SecurityImageModel { - id: securityImageModel; - } - - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onSecurityImageResult: { securityImageChangePageSecurityImage.source = ""; @@ -213,4 +209,8 @@ Item { securityImageSubmitButton.enabled = true; securityImageSubmitButton.text = "Submit"; } + + function initModel() { + securityImageSelection.initModel(); + } } diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml index 7b1434aa3c..b8e9db09ab 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml @@ -15,29 +15,28 @@ import QtQuick 2.5 ListModel { id: root; - ListElement{ - sourcePath: "images/01.jpg" - securityImageEnumValue: 1; + + function initModel() { + var array = []; + while (array.length < 6) { + // We currently have 34 security images to choose from + var randomNumber = Math.floor(Math.random() * 34) + 1; + if (array.indexOf(randomNumber) > -1) { + continue; + } + array[array.length] = randomNumber; + } + + var modelElement; + + for (var i = 0; i < 6; i++) { + modelElement = { "sourcePath":"images/" + addLeadingZero(array[i]) + ".jpg", "securityImageEnumValue": (i + 1) } + root.insert(i, modelElement); + } } - ListElement{ - sourcePath: "images/02.jpg" - securityImageEnumValue: 2; - } - ListElement{ - sourcePath: "images/03.jpg" - securityImageEnumValue: 3; - } - ListElement{ - sourcePath: "images/04.jpg" - securityImageEnumValue: 4; - } - ListElement{ - sourcePath: "images/05.jpg" - securityImageEnumValue: 5; - } - ListElement{ - sourcePath: "images/06.jpg" - securityImageEnumValue: 6; + + function addLeadingZero(n) { + return n < 10 ? '0' + n : '' + n; } function getImagePathFromImageID(imageID) { diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml index e12332cd0c..1f5e67eaa5 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml @@ -95,6 +95,10 @@ Item { function getSelectedImageIndex() { return gridModel.get(securityImageGrid.currentIndex).securityImageEnumValue; } + + function initModel() { + gridModel.initModel(); + } // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml b/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml index 75334b1686..11a6c99b0c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml @@ -25,8 +25,8 @@ Item { id: root; - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; } // "Unavailable" diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ac05bf7c84..ef2b007dc8 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -31,13 +31,15 @@ Rectangle { property bool keyboardRaised: false; property bool isPassword: false; + anchors.fill: (typeof parent === undefined) ? undefined : parent; + Image { anchors.fill: parent; source: "images/wallet-bg.jpg"; } - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onWalletStatusResult: { if (walletStatus === 0) { @@ -47,7 +49,7 @@ Rectangle { } else if (walletStatus === 1) { if (root.activeView !== "walletSetup") { root.activeView = "walletSetup"; - commerce.resetLocalWalletOnly(); + Commerce.resetLocalWalletOnly(); var timestamp = new Date(); walletSetup.startingTimestamp = timestamp; walletSetup.setupAttemptID = generateUUID(); @@ -57,10 +59,13 @@ Rectangle { } else if (walletStatus === 2) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; + UserActivityLogger.commercePassphraseEntry("wallet app"); } } else if (walletStatus === 3) { - root.activeView = "walletHome"; - commerce.getSecurityImage(); + if (root.activeView !== "walletSetup") { + root.activeView = "walletHome"; + Commerce.getSecurityImage(); + } } else { console.log("ERROR in Wallet.qml: Unknown wallet status: " + walletStatus); } @@ -70,7 +75,7 @@ Rectangle { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; } else if (isLoggedIn) { - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } } @@ -82,10 +87,6 @@ Rectangle { } } - SecurityImageModel { - id: securityImageModel; - } - HifiCommerceCommon.CommerceLightbox { id: lightboxPopup; visible: false; @@ -180,7 +181,7 @@ Rectangle { if (msg.method === 'walletSetup_finished') { if (msg.referrer === '' || msg.referrer === 'marketplace cta') { root.activeView = "initialize"; - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } else if (msg.referrer === 'purchases') { sendToScript({method: 'goToPurchases'}); } else { @@ -209,17 +210,19 @@ Rectangle { Connections { onSendSignalToWallet: { - if (msg.method === 'walletSetup_raiseKeyboard') { - root.keyboardRaised = true; - root.isPassword = msg.isPasswordField; - } else if (msg.method === 'walletSetup_lowerKeyboard') { - root.keyboardRaised = false; - } else if (msg.method === 'walletSecurity_changePassphraseCancelled') { - root.activeView = "security"; - } else if (msg.method === 'walletSecurity_changePassphraseSuccess') { - root.activeView = "security"; - } else { - sendToScript(msg); + if (passphraseChange.visible) { + if (msg.method === 'walletSetup_raiseKeyboard') { + root.keyboardRaised = true; + root.isPassword = msg.isPasswordField; + } else if (msg.method === 'walletSetup_lowerKeyboard') { + root.keyboardRaised = false; + } else if (msg.method === 'walletSecurity_changePassphraseCancelled') { + root.activeView = "security"; + } else if (msg.method === 'walletSecurity_changePassphraseSuccess') { + root.activeView = "security"; + } else { + sendToScript(msg); + } } } } @@ -260,7 +263,7 @@ Rectangle { color: hifi.colors.baseGray; Component.onCompleted: { - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } } @@ -281,7 +284,7 @@ Rectangle { Connections { target: GlobalServices onMyUsernameChanged: { - commerce.getLoginStatus(); + Commerce.getLoginStatus(); } } @@ -295,7 +298,7 @@ Rectangle { Connections { onSendSignalToParent: { if (msg.method === "authSuccess") { - commerce.getWalletStatus(); + Commerce.getWalletStatus(); } else { sendToScript(msg); } @@ -342,6 +345,7 @@ Rectangle { passphraseChange.clearPassphraseFields(); passphraseChange.resetSubmitButton(); } else if (msg.method === 'walletSecurity_changeSecurityImage') { + securityImageChange.initModel(); root.activeView = "securityImageChange"; } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index d23079d3f3..5a5eeeb91f 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -28,8 +28,8 @@ Item { property bool historyReceived: false; property int pendingCount: 0; - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onBalanceResult : { balanceText.text = result.data.balance; @@ -135,8 +135,8 @@ Item { onVisibleChanged: { if (visible) { historyReceived = false; - commerce.balance(); - commerce.history(); + Commerce.balance(); + Commerce.history(); } else { refreshTimer.stop(); } @@ -165,8 +165,8 @@ Item { interval: 4000; onTriggered: { console.log("Refreshing Wallet Home..."); - commerce.balance(); - commerce.history(); + Commerce.balance(); + Commerce.history(); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index d7859d2800..21da38def0 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -41,8 +41,8 @@ Item { source: "images/wallet-bg.jpg"; } - Hifi.QmlCommerce { - id: commerce; + Connections { + target: Commerce; onSecurityImageResult: { if (!exists && root.lastPage === "step_2") { @@ -236,6 +236,7 @@ Item { height: 50; text: "Set Up Wallet"; onClicked: { + securityImageSelection.initModel(); root.activeView = "step_2"; } } @@ -251,7 +252,7 @@ Item { height: 50; text: "Cancel"; onClicked: { - sendSignalToWallet({method: 'walletSetup_cancelClicked'}); + sendSignalToWallet({method: 'walletSetup_cancelClicked', referrer: root.referrer }); } } } @@ -365,7 +366,7 @@ Item { onClicked: { root.lastPage = "step_2"; var securityImagePath = securityImageSelection.getImagePathFromImageID(securityImageSelection.getSelectedImageIndex()) - commerce.chooseSecurityImage(securityImagePath); + Commerce.chooseSecurityImage(securityImagePath); root.activeView = "step_3"; passphraseSelection.clearPassphraseFields(); } @@ -448,7 +449,7 @@ Item { onVisibleChanged: { if (visible) { - commerce.getWalletAuthenticatedStatus(); + Commerce.getWalletAuthenticatedStatus(); } } @@ -534,7 +535,7 @@ Item { onClicked: { if (passphraseSelection.validateAndSubmitPassphrase()) { root.lastPage = "step_3"; - commerce.generateKeyPair(); + Commerce.generateKeyPair(); root.activeView = "step_4"; } } @@ -667,7 +668,7 @@ Item { onVisibleChanged: { if (visible) { - commerce.getKeyFilePathIfExists(); + Commerce.getKeyFilePathIfExists(); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/images/01.jpg b/interface/resources/qml/hifi/commerce/wallet/images/01.jpg index 821be6c584..199920a32d 100644 Binary files a/interface/resources/qml/hifi/commerce/wallet/images/01.jpg and b/interface/resources/qml/hifi/commerce/wallet/images/01.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/02.jpg b/interface/resources/qml/hifi/commerce/wallet/images/02.jpg index cfcd3e38bf..821be6c584 100644 Binary files a/interface/resources/qml/hifi/commerce/wallet/images/02.jpg and b/interface/resources/qml/hifi/commerce/wallet/images/02.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/04.jpg b/interface/resources/qml/hifi/commerce/wallet/images/04.jpg index 82e3e64be3..1acedf8b10 100644 Binary files a/interface/resources/qml/hifi/commerce/wallet/images/04.jpg and b/interface/resources/qml/hifi/commerce/wallet/images/04.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/05.jpg b/interface/resources/qml/hifi/commerce/wallet/images/05.jpg index c6ce0ac0a1..da73727dea 100644 Binary files a/interface/resources/qml/hifi/commerce/wallet/images/05.jpg and b/interface/resources/qml/hifi/commerce/wallet/images/05.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/06.jpg b/interface/resources/qml/hifi/commerce/wallet/images/06.jpg index 1acedf8b10..bbd0399d3f 100644 Binary files a/interface/resources/qml/hifi/commerce/wallet/images/06.jpg and b/interface/resources/qml/hifi/commerce/wallet/images/06.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/07.jpg b/interface/resources/qml/hifi/commerce/wallet/images/07.jpg new file mode 100644 index 0000000000..e5f86a4a80 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/07.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/08.jpg b/interface/resources/qml/hifi/commerce/wallet/images/08.jpg new file mode 100644 index 0000000000..9d49866c91 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/08.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/09.jpg b/interface/resources/qml/hifi/commerce/wallet/images/09.jpg new file mode 100644 index 0000000000..a1c31bce7d Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/09.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/10.jpg b/interface/resources/qml/hifi/commerce/wallet/images/10.jpg new file mode 100644 index 0000000000..1f6fc1de27 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/10.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/11.jpg b/interface/resources/qml/hifi/commerce/wallet/images/11.jpg new file mode 100644 index 0000000000..37c3b937a5 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/11.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/12.jpg b/interface/resources/qml/hifi/commerce/wallet/images/12.jpg new file mode 100644 index 0000000000..39f653630c Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/12.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/13.jpg b/interface/resources/qml/hifi/commerce/wallet/images/13.jpg new file mode 100644 index 0000000000..3ae86be465 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/13.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/14.jpg b/interface/resources/qml/hifi/commerce/wallet/images/14.jpg new file mode 100644 index 0000000000..8de661e58e Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/14.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/15.jpg b/interface/resources/qml/hifi/commerce/wallet/images/15.jpg new file mode 100644 index 0000000000..f0ac801b89 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/15.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/16.jpg b/interface/resources/qml/hifi/commerce/wallet/images/16.jpg new file mode 100644 index 0000000000..f7c32a696b Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/16.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/17.jpg b/interface/resources/qml/hifi/commerce/wallet/images/17.jpg new file mode 100644 index 0000000000..75e811ee36 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/17.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/18.jpg b/interface/resources/qml/hifi/commerce/wallet/images/18.jpg new file mode 100644 index 0000000000..e4ca0b6f28 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/18.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/19.jpg b/interface/resources/qml/hifi/commerce/wallet/images/19.jpg new file mode 100644 index 0000000000..4c25f3dda2 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/19.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/20.jpg b/interface/resources/qml/hifi/commerce/wallet/images/20.jpg new file mode 100644 index 0000000000..a5a4565a87 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/20.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/21.jpg b/interface/resources/qml/hifi/commerce/wallet/images/21.jpg new file mode 100644 index 0000000000..3747c9558e Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/21.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/22.jpg b/interface/resources/qml/hifi/commerce/wallet/images/22.jpg new file mode 100644 index 0000000000..66003db9c7 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/22.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/23.jpg b/interface/resources/qml/hifi/commerce/wallet/images/23.jpg new file mode 100644 index 0000000000..72e02c6db0 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/23.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/24.jpg b/interface/resources/qml/hifi/commerce/wallet/images/24.jpg new file mode 100644 index 0000000000..e481a587a5 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/24.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/25.jpg b/interface/resources/qml/hifi/commerce/wallet/images/25.jpg new file mode 100644 index 0000000000..1300edf984 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/25.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/26.jpg b/interface/resources/qml/hifi/commerce/wallet/images/26.jpg new file mode 100644 index 0000000000..9e3aaf9383 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/26.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/27.jpg b/interface/resources/qml/hifi/commerce/wallet/images/27.jpg new file mode 100644 index 0000000000..f64c0c6a3d Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/27.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/28.jpg b/interface/resources/qml/hifi/commerce/wallet/images/28.jpg new file mode 100644 index 0000000000..8f1580ab21 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/28.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/29.jpg b/interface/resources/qml/hifi/commerce/wallet/images/29.jpg new file mode 100644 index 0000000000..fdfb4438ec Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/29.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/30.jpg b/interface/resources/qml/hifi/commerce/wallet/images/30.jpg new file mode 100644 index 0000000000..785ea5a999 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/30.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/31.jpg b/interface/resources/qml/hifi/commerce/wallet/images/31.jpg new file mode 100644 index 0000000000..06dc95afab Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/31.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/32.jpg b/interface/resources/qml/hifi/commerce/wallet/images/32.jpg new file mode 100644 index 0000000000..b987c44e13 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/32.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/33.jpg b/interface/resources/qml/hifi/commerce/wallet/images/33.jpg new file mode 100644 index 0000000000..19b05a5f73 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/33.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/34.jpg b/interface/resources/qml/hifi/commerce/wallet/images/34.jpg new file mode 100644 index 0000000000..39f2a5f4ce Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/34.jpg differ diff --git a/interface/resources/qml/hifi/tablet/NewEntityButton.qml b/interface/resources/qml/hifi/tablet/NewEntityButton.qml index 7f838717df..1952ef7ee8 100644 --- a/interface/resources/qml/hifi/tablet/NewEntityButton.qml +++ b/interface/resources/qml/hifi/tablet/NewEntityButton.qml @@ -123,11 +123,11 @@ Item { hoverEnabled: true enabled: true onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); newEntityButton.clicked(); } onEntered: { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); newEntityButton.state = "hover state"; } onExited: { diff --git a/interface/resources/qml/hifi/tablet/Tablet.qml b/interface/resources/qml/hifi/tablet/Tablet.qml deleted file mode 100644 index 2a086bae94..0000000000 --- a/interface/resources/qml/hifi/tablet/Tablet.qml +++ /dev/null @@ -1,261 +0,0 @@ -import QtQuick 2.5 -import QtGraphicalEffects 1.0 -import QtQuick.Layouts 1.3 - -import "../../styles-uit" -import "../audio" as HifiAudio - -Item { - id: tablet - objectName: "tablet" - property int rowIndex: 6 // by default - property int columnIndex: 1 // point to 'go to location' - property int count: (flowMain.children.length - 1) - - // used to look up a button by its uuid - function findButtonIndex(uuid) { - if (!uuid) { - return -1; - } - - for (var i in flowMain.children) { - var child = flowMain.children[i]; - if (child.uuid === uuid) { - return i; - } - } - return -1; - } - - function sortButtons() { - var children = []; - for (var i = 0; i < flowMain.children.length; i++) { - children[i] = flowMain.children[i]; - } - - children.sort(function (a, b) { - if (a.sortOrder === b.sortOrder) { - // subsort by stableOrder, because JS sort is not stable in qml. - return a.stableOrder - b.stableOrder; - } else { - return a.sortOrder - b.sortOrder; - } - }); - - flowMain.children = children; - } - - // called by C++ code when a button should be added to the tablet - function addButtonProxy(properties) { - var component = Qt.createComponent("TabletButton.qml"); - var button = component.createObject(flowMain); - - // copy all properites to button - var keys = Object.keys(properties).forEach(function (key) { - button[key] = properties[key]; - }); - - // pass a reference to the tabletRoot object to the button. - if (tabletRoot) { - button.tabletRoot = tabletRoot; - } else { - button.tabletRoot = parent.parent; - } - - sortButtons(); - - return button; - } - - // called by C++ code when a button should be removed from the tablet - function removeButtonProxy(properties) { - var index = findButtonIndex(properties.uuid); - if (index < 0) { - console.log("Warning: Tablet.qml could not find button with uuid = " + properties.uuid); - } else { - flowMain.children[index].destroy(); - } - } - - Rectangle { - id: bgTopBar - height: 90 - - anchors { - top: parent.top - topMargin: 0 - left: parent.left - leftMargin: 0 - right: parent.right - rightMargin: 0 - } - - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - } - - GradientStop { - position: 1 - color: "#1e1e1e" - } - } - - HifiAudio.MicBar { - anchors { - left: parent.left - leftMargin: 30 - verticalCenter: parent.verticalCenter - } - } - - Item { - width: 150 - height: 50 - anchors.right: parent.right - anchors.rightMargin: 30 - anchors.verticalCenter: parent.verticalCenter - - ColumnLayout { - anchors.fill: parent - - RalewaySemiBold { - text: Account.loggedIn ? qsTr("Log out") : qsTr("Log in") - horizontalAlignment: Text.AlignRight - anchors.right: parent.right - font.pixelSize: 20 - color: "#afafaf" - } - - RalewaySemiBold { - visible: Account.loggedIn - height: Account.loggedIn ? parent.height/2 - parent.spacing/2 : 0 - text: Account.loggedIn ? "[" + tabletRoot.usernameShort + "]" : "" - horizontalAlignment: Text.AlignRight - anchors.right: parent.right - font.pixelSize: 20 - color: "#afafaf" - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - if (!Account.loggedIn) { - DialogsManager.showLoginDialog() - } else { - Account.logOut() - } - } - } - } - } - - Rectangle { - id: bgMain - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - } - - GradientStop { - position: 1 - color: "#0f212e" - } - } - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.top: bgTopBar.bottom - anchors.topMargin: 0 - - Flickable { - id: flickable - width: parent.width - height: parent.height - contentWidth: parent.width - contentHeight: flowMain.childrenRect.height + flowMain.anchors.topMargin + flowMain.anchors.bottomMargin + flowMain.spacing - clip: true - Flow { - id: flowMain - spacing: 16 - anchors.right: parent.right - anchors.rightMargin: 30 - anchors.left: parent.left - anchors.leftMargin: 30 - anchors.bottom: parent.bottom - anchors.bottomMargin: 30 - anchors.top: parent.top - anchors.topMargin: 30 - } - } - } - - function setCurrentItemState(state) { - var index = rowIndex + columnIndex; - - if (index >= 0 && index <= count ) { - flowMain.children[index].state = state; - } - } - - function nextItem() { - setCurrentItemState("base state"); - var nextColumnIndex = (columnIndex + 3 + 1) % 3; - var nextIndex = rowIndex + nextColumnIndex; - if(nextIndex <= count) { - columnIndex = nextColumnIndex; - }; - setCurrentItemState("hover state"); - } - - function previousItem() { - setCurrentItemState("base state"); - var prevIndex = (columnIndex + 3 - 1) % 3; - if((rowIndex + prevIndex) <= count){ - columnIndex = prevIndex; - } - setCurrentItemState("hover state"); - } - - function upItem() { - setCurrentItemState("base state"); - rowIndex = rowIndex - 3; - if (rowIndex < 0 ) { - rowIndex = (count - (count % 3)); - var index = rowIndex + columnIndex; - if(index > count) { - rowIndex = rowIndex - 3; - } - } - setCurrentItemState("hover state"); - } - - function downItem() { - setCurrentItemState("base state"); - rowIndex = rowIndex + 3; - var index = rowIndex + columnIndex; - if (index > count ) { - rowIndex = 0; - } - setCurrentItemState("hover state"); - } - - function selectItem() { - flowMain.children[rowIndex + columnIndex].clicked(); - if (tabletRoot) { - tabletRoot.playButtonClickSound(); - } - } - - Keys.onRightPressed: nextItem(); - Keys.onLeftPressed: previousItem(); - Keys.onDownPressed: downItem(); - Keys.onUpPressed: upItem(); - Keys.onReturnPressed: selectItem(); -} diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 6aa3b8e7fe..160151144b 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -20,6 +20,8 @@ import "../" import "../toolbars" import "../../styles-uit" as HifiStyles import "../../controls-uit" as HifiControls +import QtQuick.Controls 2.2 as QQC2 +import QtQuick.Templates 2.2 as T // references HMD, AddressManager, AddressBarDialog from root context @@ -223,7 +225,7 @@ StackView { visible: addressLine.text.length === 0 } - TextField { + QQC2.TextField { id: addressLine width: addressLineContainer.width - addressLineContainer.anchors.leftMargin - addressLineContainer.anchors.rightMargin; anchors { @@ -238,16 +240,36 @@ StackView { addressBarDialog.keyboardEnabled = false; toggleOrGo(); } - placeholderText: "Type domain address here" + + // unfortunately TextField from Quick Controls 2 disallow customization of placeHolderText color without creation of new style + property string placeholderText2: "Type domain address here" verticalAlignment: TextInput.AlignBottom - style: TextFieldStyle { - textColor: hifi.colors.text - placeholderTextColor: "gray" - font { - family: hifi.fonts.fontFamily - pixelSize: hifi.fonts.pixelSize * 0.75 + + font { + family: hifi.fonts.fontFamily + pixelSize: hifi.fonts.pixelSize * 0.75 + } + + color: hifi.colors.text + background: Item {} + + QQC2.Label { + T.TextField { + id: control + + padding: 6 // numbers taken from Qt\5.9.2\Src\qtquickcontrols2\src\imports\controls\TextField.qml + leftPadding: padding + 4 } - background: Item {} + + font: parent.font + + x: control.leftPadding + y: control.topPadding + + text: parent.placeholderText2 + verticalAlignment: "AlignVCenter" + color: 'gray' + visible: parent.text === '' } } diff --git a/interface/resources/qml/hifi/tablet/TabletButton.qml b/interface/resources/qml/hifi/tablet/TabletButton.qml index 8fc31d1cd6..1508367318 100644 --- a/interface/resources/qml/hifi/tablet/TabletButton.qml +++ b/interface/resources/qml/hifi/tablet/TabletButton.qml @@ -123,7 +123,6 @@ Item { enabled: true preventStealing: true onClicked: { - console.log("Tablet Button Clicked!"); if (tabletButton.inDebugMode) { if (tabletButton.isActive) { tabletButton.isActive = false; @@ -133,12 +132,12 @@ Item { } tabletButton.clicked(); if (tabletRoot) { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); } } onEntered: { tabletButton.isEntered = true; - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); if (tabletButton.isActive) { tabletButton.state = "hover active state"; diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml new file mode 100644 index 0000000000..3f9451436c --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -0,0 +1,159 @@ +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import QtQuick.Layouts 1.3 + +import "." +import "../../styles-uit" +import "../audio" as HifiAudio + +Item { + id: tablet + objectName: "tablet" + property var tabletProxy: Tablet.getTablet("com.highfidelity.interface.tablet.system"); + + Rectangle { + id: bgTopBar + height: 90 + + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } + + GradientStop { + position: 1 + color: "#1e1e1e" + } + } + + HifiAudio.MicBar { + anchors { + left: parent.left + leftMargin: 30 + verticalCenter: parent.verticalCenter + } + } + + Item { + width: 150 + height: 50 + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.verticalCenter: parent.verticalCenter + + ColumnLayout { + anchors.fill: parent + + RalewaySemiBold { + text: Account.loggedIn ? qsTr("Log out") : qsTr("Log in") + horizontalAlignment: Text.AlignRight + anchors.right: parent.right + font.pixelSize: 20 + color: "#afafaf" + } + + RalewaySemiBold { + visible: Account.loggedIn + height: Account.loggedIn ? parent.height/2 - parent.spacing/2 : 0 + text: Account.loggedIn ? "[" + tabletRoot.usernameShort + "]" : "" + horizontalAlignment: Text.AlignRight + anchors.right: parent.right + font.pixelSize: 20 + color: "#afafaf" + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (!Account.loggedIn) { + DialogsManager.showLoginDialog() + } else { + Account.logOut() + } + } + } + } + } + + Rectangle { + id: bgMain + clip: true + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } + + GradientStop { + position: 1 + color: "#0f212e" + } + } + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.top: bgTopBar.bottom + + GridView { + id: flickable + anchors.top: parent.top + anchors.topMargin: 15 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: cellWidth * 3 + cellHeight: 145 + cellWidth: 145 + model: tabletProxy.buttons + delegate: Item { + width: flickable.cellWidth + height: flickable.cellHeight + property var proxy: modelData + + TabletButton { + id: tabletButton + anchors.centerIn: parent + onClicked: modelData.clicked() + state: wrapper.GridView.isCurrentItem ? "hover state" : "base state" + } + + Connections { + target: modelData; + onPropertiesChanged: { + updateProperties(); + } + } + + Component.onCompleted: updateProperties() + + function updateProperties() { + var keys = Object.keys(modelData.properties).forEach(function (key) { + if (tabletButton[key] !== modelData.properties[key]) { + tabletButton[key] = modelData.properties[key]; + } + }); + } + } + } + } + + Keys.onRightPressed: flickable.moveCurrentIndexRight(); + Keys.onLeftPressed: flickable.moveCurrentIndexLeft(); + Keys.onDownPressed: flickable.moveCurrentIndexDown(); + Keys.onUpPressed: flickable.moveCurrentIndexUp(); + Keys.onReturnPressed: { + if (flickable.currentItem) { + flickable.currentItem.proxy.clicked(); + if (tabletRoot) { + tabletRoot.playButtonClickSound(); + } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletMenuView.qml b/interface/resources/qml/hifi/tablet/TabletMenuView.qml index 4a4a6b7f87..636ebfe71f 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuView.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuView.qml @@ -77,12 +77,12 @@ FocusScope { anchors.fill: parent hoverEnabled: true onEntered: { - tabletInterface.playSound(TabletEnums.ButtonHover); + Tablet.playSound(TabletEnums.ButtonHover); listView.currentIndex = index } onClicked: { - tabletInterface.playSound(TabletEnums.ButtonClick); + Tablet.playSound(TabletEnums.ButtonClick); root.selected(item); } } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index a161741049..da544c2114 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -68,37 +68,36 @@ Item { function loadSource(url) { tabletApps.clear(); - loader.source = ""; // make sure we load the qml fresh each time. - loader.source = url; tabletApps.append({"appUrl": url, "isWebUrl": false, "scriptUrl": "", "appWebUrl": ""}); + loader.load(url) } function loadQMLOnTop(url) { tabletApps.append({"appUrl": url, "isWebUrl": false, "scriptUrl": "", "appWebUrl": ""}); - loader.source = ""; - loader.source = tabletApps.get(currentApp).appUrl; - if (loader.item.hasOwnProperty("gotoPreviousApp")) { - loader.item.gotoPreviousApp = true; - } + loader.load(tabletApps.get(currentApp).appUrl, function(){ + if (loader.item.hasOwnProperty("gotoPreviousApp")) { + loader.item.gotoPreviousApp = true; + } + }) } - function loadWebOnTop(url, injectJavaScriptUrl) { - tabletApps.append({"appUrl": loader.source, "isWebUrl": true, "scriptUrl": injectJavaScriptUrl, "appWebUrl": url}); - loader.item.url = tabletApps.get(currentApp).appWebUrl; - loader.item.scriptUrl = tabletApps.get(currentApp).scriptUrl; - if (loader.item.hasOwnProperty("gotoPreviousApp")) { - loader.item.gotoPreviousApp = true; - } + function loadWebContent(source, url, injectJavaScriptUrl) { + tabletApps.append({"appUrl": source, "isWebUrl": true, "scriptUrl": injectJavaScriptUrl, "appWebUrl": url}); + loader.load(source, function() { + loader.item.scriptURL = injectJavaScriptUrl; + loader.item.url = url; + if (loader.item.hasOwnProperty("gotoPreviousApp")) { + loader.item.gotoPreviousApp = true; + } + }); } - function loadWebBase() { - loader.source = ""; - loader.source = "TabletWebView.qml"; + function loadWebBase(url, injectJavaScriptUrl) { + loadWebContent("hifi/tablet/TabletWebView.qml", url, injectJavaScriptUrl); } - function loadTabletWebBase() { - loader.source = ""; - loader.source = "./BlocksWebView.qml"; + function loadTabletWebBase(url, injectJavaScriptUrl) { + loadWebContent("hifi/tablet/BlocksWebView.qml", url, injectJavaScriptUrl); } function returnToPreviousApp() { @@ -110,7 +109,7 @@ Item { loadSource("TabletWebView.qml"); loadWebUrl(webUrl, scriptUrl); } else { - loader.source = tabletApps.get(currentApp).appUrl; + loader.load(tabletApps.get(currentApp).appUrl); } } @@ -173,47 +172,79 @@ Item { } } - Loader { - id: loader - objectName: "loader" - asynchronous: false - - width: parent.width - height: parent.height - - // Hook up callback for clara.io download from the marketplace. - Connections { - id: eventBridgeConnection - target: eventBridge - onWebEventReceived: { - if (message.slice(0, 17) === "CLARA.IO DOWNLOAD") { - ApplicationInterface.addAssetToWorldFromURL(message.slice(18)); - } - } - } - - onLoaded: { - if (loader.item.hasOwnProperty("sendToScript")) { - loader.item.sendToScript.connect(tabletRoot.sendToScript); - } - if (loader.item.hasOwnProperty("setRootMenu")) { - loader.item.setRootMenu(tabletRoot.rootMenu, tabletRoot.subMenu); - } - loader.item.forceActiveFocus(); - - if (openModal) { - openModal.canceled(); - openModal.destroy(); - openModal = null; - } - - if (openBrowser) { - openBrowser.destroy(); - openBrowser = null; + // Hook up callback for clara.io download from the marketplace. + Connections { + id: eventBridgeConnection + target: eventBridge + onWebEventReceived: { + if (message.slice(0, 17) === "CLARA.IO DOWNLOAD") { + ApplicationInterface.addAssetToWorldFromURL(message.slice(18)); } } } + Item { + id: loader + objectName: "loader"; + anchors.fill: parent; + property string source: ""; + property var item: null; + signal loaded; + + onWidthChanged: { + if (loader.item) { + loader.item.width = loader.width; + } + } + + onHeightChanged: { + if (loader.item) { + loader.item.height = loader.height; + } + } + + function load(newSource, callback) { + if (loader.source == newSource) { + loader.loaded(); + return; + } + + if (loader.item) { + loader.item.destroy(); + loader.item = null; + } + + QmlSurface.load(newSource, loader, function(newItem) { + loader.item = newItem; + loader.item.width = loader.width; + loader.item.height = loader.height; + loader.loaded(); + if (loader.item.hasOwnProperty("sendToScript")) { + loader.item.sendToScript.connect(tabletRoot.sendToScript); + } + if (loader.item.hasOwnProperty("setRootMenu")) { + loader.item.setRootMenu(tabletRoot.rootMenu, tabletRoot.subMenu); + } + loader.item.forceActiveFocus(); + + if (openModal) { + openModal.canceled(); + openModal.destroy(); + openModal = null; + } + + if (openBrowser) { + openBrowser.destroy(); + openBrowser = null; + } + + if (callback) { + callback(); + } + }); + } + } + width: 480 height: 706 diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml index e3170f85ef..9c027308b8 100644 --- a/interface/resources/qml/hifi/tablet/WindowRoot.qml +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -59,26 +59,25 @@ Windows.ScrollingWindow { } function loadSource(url) { - loader.source = ""; // make sure we load the qml fresh each time. - loader.source = url; + loader.load(url) } - function loadWebBase() { - loader.source = ""; - loader.source = "WindowWebView.qml"; + function loadWebContent(source, url, injectJavaScriptUrl) { + loader.load(source, function() { + loader.item.scriptURL = injectJavaScriptUrl; + loader.item.url = url; + if (loader.item.hasOwnProperty("closeButtonVisible")) { + loader.item.closeButtonVisible = false; + } + }); } - function loadTabletWebBase() { - loader.source = ""; - loader.source = "./BlocksWebView.qml"; + function loadWebBase(url, injectJavaScriptUrl) { + loadWebContent("hifi/tablet/TabletWebView.qml", url, injectJavaScriptUrl); } - function loadWebUrl(url, injectedJavaScriptUrl) { - loader.item.url = url; - loader.item.scriptURL = injectedJavaScriptUrl; - if (loader.item.hasOwnProperty("closeButtonVisible")) { - loader.item.closeButtonVisible = false; - } + function loadTabletWebBase(url, injectJavaScriptUrl) { + loadWebContent("hifi/tablet/BlocksWebView.qml", url, injectJavaScriptUrl); } // used to send a message from qml to interface script. @@ -111,38 +110,68 @@ Windows.ScrollingWindow { username = newUsername; } - Loader { + // Hook up callback for clara.io download from the marketplace. + Connections { + id: eventBridgeConnection + target: eventBridge + onWebEventReceived: { + if (message.slice(0, 17) === "CLARA.IO DOWNLOAD") { + ApplicationInterface.addAssetToWorldFromURL(message.slice(18)); + } + } + } + + Item { id: loader - objectName: "loader" - asynchronous: false + objectName: "loader"; + property string source: ""; + property var item: null; height: pane.scrollHeight width: pane.contentWidth anchors.left: parent.left anchors.top: parent.top - - // Hook up callback for clara.io download from the marketplace. - Connections { - id: eventBridgeConnection - target: eventBridge - onWebEventReceived: { - if (message.slice(0, 17) === "CLARA.IO DOWNLOAD") { - ApplicationInterface.addAssetToWorldFromURL(message.slice(18)); - } + signal loaded; + + onWidthChanged: { + if (loader.item) { + loader.item.width = loader.width; } } - - onLoaded: { - if (loader.item.hasOwnProperty("sendToScript")) { - loader.item.sendToScript.connect(tabletRoot.sendToScript); + + onHeightChanged: { + if (loader.item) { + loader.item.height = loader.height; } - if (loader.item.hasOwnProperty("setRootMenu")) { - loader.item.setRootMenu(tabletRoot.rootMenu, tabletRoot.subMenu); + } + + function load(newSource, callback) { + if (loader.item) { + loader.item.destroy(); + loader.item = null; } - loader.item.forceActiveFocus(); + + QmlSurface.load(newSource, loader, function(newItem) { + loader.item = newItem; + loader.item.width = loader.width; + loader.item.height = loader.height; + loader.loaded(); + if (loader.item.hasOwnProperty("sendToScript")) { + loader.item.sendToScript.connect(tabletRoot.sendToScript); + } + if (loader.item.hasOwnProperty("setRootMenu")) { + loader.item.setRootMenu(tabletRoot.rootMenu, tabletRoot.subMenu); + } + loader.item.forceActiveFocus(); + + if (callback) { + callback(); + } + }); } } + implicitWidth: 480 implicitHeight: 706 } diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 01aa29f665..ff7e835690 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -11,6 +11,8 @@ Window { horizontalSpacers: horizontal verticalSpacers: !horizontal } + property var tabletProxy; + property var buttonModel: ListModel {} hideBackground: true resizable: false destroyOnCloseButton: false @@ -23,24 +25,32 @@ Window { activator: Item {} property bool horizontal: true property real buttonSize: 50; - property var buttons: [] - property var container: horizontal ? row : column Settings { category: "toolbar/" + window.objectName property alias x: window.x property alias y: window.y } + + Component { + id: buttonComponent + ToolbarButton { + id: toolbarButton + property var proxy: modelData; + onClicked: proxy.clicked() + Component.onCompleted: updateProperties() - onHorizontalChanged: { - var newParent = horizontal ? row : column; - for (var i in buttons) { - var child = buttons[i]; - child.parent = newParent; - if (horizontal) { - child.y = 0 - } else { - child.x = 0 + Connections { + target: proxy; + onPropertiesChanged: updateProperties(); + } + + function updateProperties() { + Object.keys(proxy.properties).forEach(function (key) { + if (toolbarButton[key] !== proxy.properties[key]) { + toolbarButton[key] = proxy.properties[key]; + } + }); } } } @@ -52,97 +62,22 @@ Window { Row { id: row + visible: window.horizontal spacing: 6 + Repeater { + model: buttonModel + delegate: buttonComponent + } } - + Column { id: column + visible: !window.horizontal spacing: 6 - } - - Component { id: toolbarButtonBuilder; ToolbarButton { } } - } - - - function findButtonIndex(name) { - if (!name) { - return -1; - } - - for (var i in buttons) { - var child = buttons[i]; - if (child.objectName === name) { - return i; + Repeater { + model: buttonModel + delegate: buttonComponent } } - return -1; - } - - function findButton(name) { - var index = findButtonIndex(name); - if (index < 0) { - return; - } - return buttons[index]; - } - - function sortButtons() { - var children = []; - for (var i = 0; i < container.children.length; i++) { - children[i] = container.children[i]; - } - - children.sort(function (a, b) { - if (a.sortOrder === b.sortOrder) { - // subsort by stableOrder, because JS sort is not stable in qml. - return a.stableOrder - b.stableOrder; - } else { - return a.sortOrder - b.sortOrder; - } - }); - - container.children = children; - } - - function addButton(properties) { - properties = properties || {} - - // If a name is specified, then check if there's an existing button with that name - // and return it if so. This will allow multiple clients to listen to a single button, - // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded - var result = findButton(properties.objectName); - if (result) { - for (var property in properties) { - result[property] = properties[property]; - } - return result; - } - properties.toolbar = this; - properties.opacity = 0; - result = toolbarButtonBuilder.createObject(container, properties); - buttons.push(result); - - result.opacity = 1; - - sortButtons(); - - fadeIn(null); - - return result; - } - - function removeButton(name) { - var index = findButtonIndex(name); - if (index < -1) { - console.warn("Tried to remove non-existent button " + name); - return; - } - - buttons[index].destroy(); - buttons.splice(index, 1); - - if (buttons.length === 0) { - fadeOut(null); - } } } diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index f27ba6bb24..7781d1140b 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -27,7 +27,8 @@ StateImage { property string activeHoverIcon: button.activeIcon property int sortOrder: 100 - property int stableSortOrder: 0 + property int stableOrder: 0 + property var uuid; signal clicked() diff --git a/interface/resources/qml/js/Utils.jsc b/interface/resources/qml/js/Utils.jsc new file mode 100644 index 0000000000..ab20e996b9 Binary files /dev/null and b/interface/resources/qml/js/Utils.jsc differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 585f0b464d..459197f1e3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -971,7 +971,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // set the account manager's root URL and trigger a login request if we don't have the access token accountManager->setIsAgent(true); - accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); auto addressManager = DependencyManager::get(); @@ -1496,6 +1496,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } }); + connect(getEntities()->getTree().get(), &EntityTree::deletingEntity, [=](const EntityItemID& entityItemID) { + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + if (myAvatar) { + myAvatar->clearAvatarEntity(entityItemID); + } + }); + // Keyboard focus handling for Web overlays. auto overlays = &(qApp->getOverlays()); connect(overlays, &Overlays::overlayDeleted, [=](const OverlayID& overlayID) { @@ -2240,27 +2248,65 @@ extern void setupPreferences(); void Application::initializeUi() { // Make sure all QML surfaces share the main thread GL context OffscreenQmlSurface::setSharedContext(_offscreenContext->getContext()); + OffscreenQmlSurface::addWhitelistContextHandler(QUrl{ "OverlayWindowTest.qml" }, + [](QQmlContext* context) { + qDebug() << "Whitelist OverlayWindow worked"; + context->setContextProperty("OverlayWindowTestString", "TestWorked"); + }); + OffscreenQmlSurface::addWhitelistContextHandler(QUrl{ "hifi/audio/Audio.qml" }, + [](QQmlContext* context) { + qDebug() << "QQQ" << __FUNCTION__ << "Whitelist Audio worked"; + }); + AddressBarDialog::registerType(); ErrorDialog::registerType(); LoginDialog::registerType(); Tooltip::registerType(); UpdateDialog::registerType(); - QmlCommerce::registerType(); + QmlContextCallback callback = [](QQmlContext* context) { + context->setContextProperty("Commerce", new QmlCommerce()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/checkout/Checkout.qml" }, + QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, + QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, + QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, + QUrl{ "hifi/commerce/common/SortableListModel.qml" }, + QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" }, + QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" }, + QUrl{ "hifi/commerce/purchases/Purchases.qml" }, + QUrl{ "hifi/commerce/wallet/Help.qml" }, + QUrl{ "hifi/commerce/wallet/NeedsLogIn.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseChange.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseModal.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseSelection.qml" }, + QUrl{ "hifi/commerce/wallet/Security.qml" }, + QUrl{ "hifi/commerce/wallet/SecurityImageChange.qml" }, + QUrl{ "hifi/commerce/wallet/SecurityImageModel.qml" }, + QUrl{ "hifi/commerce/wallet/SecurityImageSelection.qml" }, + QUrl{ "hifi/commerce/wallet/SendMoney.qml" }, + QUrl{ "hifi/commerce/wallet/Wallet.qml" }, + QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, + QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, + }, callback); qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); qmlRegisterType("Hifi", 1, 0, "Preference"); qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); + { + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->getTablet(SYSTEM_TABLET); + } auto offscreenUi = DependencyManager::get(); offscreenUi->create(); auto surfaceContext = offscreenUi->getSurfaceContext(); offscreenUi->setProxyWindow(_window->windowHandle()); - offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to // support the window management and scripting proxies for VR use - offscreenUi->createDesktop(QString("qrc:///qml/hifi/Desktop.qml")); + offscreenUi->createDesktop(QString("hifi/Desktop.qml")); // FIXME either expose so that dialogs can set this themselves or // do better detection in the offscreen UI of what has focus @@ -2328,9 +2374,6 @@ void Application::initializeUi() { surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); surfaceContext->setContextProperty("Account", AccountScriptingInterface::getInstance()); - surfaceContext->setContextProperty("Tablet", DependencyManager::get().data()); - // Tablet inteference with Tablet.qml. Need to avoid this in QML space - surfaceContext->setContextProperty("tabletInterface", DependencyManager::get().data()); surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); surfaceContext->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); @@ -3222,8 +3265,6 @@ void Application::keyPressEvent(QKeyEvent* event) { } } - - void Application::keyReleaseEvent(QKeyEvent* event) { _keysPressed.remove(event->key()); @@ -4282,9 +4323,11 @@ void Application::updateLOD(float deltaTime) const { PerformanceTimer perfTimer("LOD"); // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode if (!isThrottleRendering()) { - float batchTime = (float)_gpuContext->getFrameTimerBatchAverage(); + float presentTime = getActiveDisplayPlugin()->getAveragePresentTime(); float engineRunTime = (float)(_renderEngine->getConfiguration().get()->getCPURunTime()); - DependencyManager::get()->autoAdjustLOD(batchTime, engineRunTime, deltaTime); + float gpuTime = getGPUContext()->getFrameTimerGPUAverage(); + float maxRenderTime = glm::max(gpuTime, glm::max(presentTime, engineRunTime)); + DependencyManager::get()->autoAdjustLOD(maxRenderTime, deltaTime); } else { DependencyManager::get()->resetLODAdjust(); } @@ -4848,8 +4891,7 @@ void Application::update(float deltaTime) { if (_physicsEnabled) { { PROFILE_RANGE(simulation_physics, "PreStep"); - - PerformanceTimer perfTimer("updateStates)"); + PerformanceTimer perfTimer("preStep)"); static VectorOfMotionStates motionStates; _entitySimulation->getObjectsToRemoveFromPhysics(motionStates); _physicsEngine->removeObjects(motionStates); @@ -4882,22 +4924,22 @@ void Application::update(float deltaTime) { } { PROFILE_RANGE(simulation_physics, "Step"); - PerformanceTimer perfTimer("stepSimulation"); + PerformanceTimer perfTimer("step"); getEntities()->getTree()->withWriteLock([&] { _physicsEngine->stepSimulation(); }); } { PROFILE_RANGE(simulation_physics, "PostStep"); - PerformanceTimer perfTimer("harvestChanges"); + PerformanceTimer perfTimer("postStep"); if (_physicsEngine->hasOutgoingChanges()) { // grab the collision events BEFORE handleOutgoingChanges() because at this point // we have a better idea of which objects we own or should own. auto& collisionEvents = _physicsEngine->getCollisionEvents(); getEntities()->getTree()->withWriteLock([&] { - PROFILE_RANGE(simulation_physics, "Harvest"); - PerformanceTimer perfTimer("handleOutgoingChanges"); + PROFILE_RANGE(simulation_physics, "HandleChanges"); + PerformanceTimer perfTimer("handleChanges"); const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); _entitySimulation->handleChangedMotionStates(outgoingChanges); @@ -4908,17 +4950,15 @@ void Application::update(float deltaTime) { }); if (!_aboutToQuit) { - // handleCollisionEvents() AFTER handleOutgoinChanges() + // handleCollisionEvents() AFTER handleOutgoingChanges() { PROFILE_RANGE(simulation_physics, "CollisionEvents"); - PerformanceTimer perfTimer("entities"); avatarManager->handleCollisionEvents(collisionEvents); // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk // deadlock.) _entitySimulation->handleCollisionEvents(collisionEvents); } - PROFILE_RANGE(simulation_physics, "UpdateEntities"); // NOTE: the getEntities()->update() call below will wait for lock // and will simulate entity motion (the EntityTree has been given an EntitySimulation). getEntities()->update(true); // update the models... @@ -4929,7 +4969,8 @@ void Application::update(float deltaTime) { myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); } - if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && + if (PerformanceTimer::isActive() && + Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsSimulationTiming)) { _physicsEngine->harvestPerformanceStats(); } @@ -5832,9 +5873,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); - // Tablet inteference with Tablet.qml. Need to avoid this in QML space - scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Tablet", DependencyManager::get().data()); + // FIXME remove these deprecated names for the tablet scripting interface + scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get().data()); auto toolbarScriptingInterface = DependencyManager::get().data(); DependencyManager::get().data()->setToolbarScriptingInterface(toolbarScriptingInterface); @@ -7241,13 +7282,17 @@ void Application::updateDisplayMode() { } auto offscreenUi = DependencyManager::get(); + auto desktop = offscreenUi->getDesktop(); // Make the switch atomic from the perspective of other threads { std::unique_lock lock(_displayPluginLock); - // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. - bool wasRepositionLocked = offscreenUi->getDesktop()->property("repositionLocked").toBool(); - offscreenUi->getDesktop()->setProperty("repositionLocked", true); + bool wasRepositionLocked = false; + if (desktop) { + // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. + wasRepositionLocked = offscreenUi->getDesktop()->property("repositionLocked").toBool(); + offscreenUi->getDesktop()->setProperty("repositionLocked", true); + } if (_displayPlugin) { disconnect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); @@ -7293,7 +7338,6 @@ void Application::updateDisplayMode() { getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); _displayPlugin = newDisplayPlugin; connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent, Qt::DirectConnection); - auto desktop = offscreenUi->getDesktop(); if (desktop) { desktop->setProperty("repositionLocked", wasRepositionLocked); } @@ -7506,4 +7550,9 @@ void Application::setAvatarOverrideUrl(const QUrl& url, bool save) { _avatarOverrideUrl = url; _saveAvatarOverrideUrl = save; } + +void Application::saveNextPhysicsStats(QString filename) { + _physicsEngine->saveNextPhysicsStats(filename); +} + #include "Application.moc" diff --git a/interface/src/Application.h b/interface/src/Application.h index 9542c5ccb6..ee16740f20 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -280,6 +280,7 @@ public: void clearAvatarOverrideUrl() { _avatarOverrideUrl = QUrl(); _saveAvatarOverrideUrl = false; } QUrl getAvatarOverrideUrl() { return _avatarOverrideUrl; } bool getSaveAvatarOverrideUrl() { return _saveAvatarOverrideUrl; } + void saveNextPhysicsStats(QString filename); signals: void svoImportRequested(const QString& url); @@ -432,6 +433,7 @@ private slots: void handleSandboxStatus(QNetworkReply* reply); void switchDisplayMode(); + private: static void initDisplay(); void init(); diff --git a/interface/src/FancyCamera.h b/interface/src/FancyCamera.h index 3552dc6ca8..bee21bad22 100644 --- a/interface/src/FancyCamera.h +++ b/interface/src/FancyCamera.h @@ -19,10 +19,14 @@ class FancyCamera : public Camera { Q_OBJECT /**jsdoc - * @namespace Camera - * @property cameraEntity {EntityID} The position and rotation properties of - * the entity specified by this ID are then used as the camera's position and - * orientation. Only works when mode is "entity". + * @namespace + * @augments Camera + */ + + // FIXME: JSDoc 3.5.5 doesn't augment @property definitions. The following definition is repeated in Camera.h. + /**jsdoc + * @property cameraEntity {Uuid} The ID of the entity that the camera position and orientation follow when the camera is in + * entity mode. */ Q_PROPERTY(QUuid cameraEntity READ getCameraEntity WRITE setCameraEntity) @@ -34,7 +38,25 @@ public: public slots: + /**jsdoc + * Get the ID of the entity that the camera is set to use the position and orientation from when it's in entity mode. You can + * also get the entity ID using the Camera.cameraEntity property. + * @function Camera.getCameraEntity + * @returns {Uuid} The ID of the entity that the camera is set to follow when in entity mode; null if no camera + * entity has been set. + */ QUuid getCameraEntity() const; + + /**jsdoc + * Set the entity that the camera should use the position and orientation from when it's in entity mode. You can also set the + * entity using the Camera.cameraEntity property. + * @function Camera.setCameraEntity + * @param {Uuid} entityID The entity that the camera should follow when it's in entity mode. + * @example Move your camera to the position and orientation of the closest entity. + * Camera.setModeString("entity"); + * var entity = Entities.findClosestEntity(MyAvatar.position, 100.0); + * Camera.setCameraEntity(entity); + */ void setCameraEntity(QUuid entityID); private: diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index 01ccbd0d9a..9e6fabd439 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -19,6 +19,7 @@ #include "LODManager.h" + Setting::Handle desktopLODDecreaseFPS("desktopLODDecreaseFPS", DEFAULT_DESKTOP_LOD_DOWN_FPS); Setting::Handle hmdLODDecreaseFPS("hmdLODDecreaseFPS", DEFAULT_HMD_LOD_DOWN_FPS); @@ -39,156 +40,95 @@ float LODManager::getLODIncreaseFPS() { return getDesktopLODIncreaseFPS(); } -void LODManager::autoAdjustLOD(float batchTime, float engineRunTime, float deltaTimeSec) { +// We use a "time-weighted running average" of the renderTime and compare it against min/max thresholds +// to determine if we should adjust the level of detail (LOD). +// +// A time-weighted running average has a timescale which determines how fast the average tracks the measured +// value in real-time. Given a step-function in the mesured value, and assuming measurements happen +// faster than the runningAverage is computed, the error between the value and its runningAverage will be +// reduced by 1/e every timescale of real-time that passes. +const float LOD_ADJUST_RUNNING_AVG_TIMESCALE = 0.1f; // sec +// +// Assuming the measured value is affected by logic invoked by the runningAverage bumping up against its +// thresholds, we expect the adjustment to introduce a step-function. We want the runningAverage settle +// to the new value BEFORE we test it aginst its thresholds again. Hence we test on a period that is a few +// multiples of the running average timescale: +const uint64_t LOD_AUTO_ADJUST_PERIOD = 5 * (uint64_t)(LOD_ADJUST_RUNNING_AVG_TIMESCALE * (float)USECS_PER_MSEC); // usec - // NOTE: our first ~100 samples at app startup are completely all over the place, and we don't - // really want to count them in our average, so we will ignore the real frame rates and stuff - // our moving average with simulated good data - const int IGNORE_THESE_SAMPLES = 100; - if (_fpsAverageUpWindow.getSampleCount() < IGNORE_THESE_SAMPLES) { - _lastStable = _lastUpShift = _lastDownShift = usecTimestampNow(); - } +const float LOD_AUTO_ADJUST_DECREMENT_FACTOR = 0.8f; +const float LOD_AUTO_ADJUST_INCREMENT_FACTOR = 1.2f; +void LODManager::autoAdjustLOD(float renderTime, float realTimeDelta) { // compute time-weighted running average renderTime - const float OVERLAY_AND_SWAP_TIME_BUDGET = 2.0f; // msec - float renderTime = batchTime + OVERLAY_AND_SWAP_TIME_BUDGET; - float maxTime = glm::max(renderTime, engineRunTime); - const float BLEND_TIMESCALE = 0.3f; // sec - const float MIN_DELTA_TIME = 0.001f; - const float safeDeltaTime = glm::max(deltaTimeSec, MIN_DELTA_TIME); - float blend = BLEND_TIMESCALE / safeDeltaTime; - if (blend > 1.0f) { - blend = 1.0f; + // Note: we MUST clamp the blend to 1.0 for stability + float blend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE) ? realTimeDelta / LOD_ADJUST_RUNNING_AVG_TIMESCALE : 1.0f; + _avgRenderTime = (1.0f - blend) * _avgRenderTime + blend * renderTime; // msec + if (!_automaticLODAdjust) { + // early exit + return; } - _avgRenderTime = (1.0f - blend) * _avgRenderTime + blend * maxTime; // msec - // translate into fps for legacy implementation + float oldOctreeSizeScale = _octreeSizeScale; float currentFPS = (float)MSECS_PER_SECOND / _avgRenderTime; - - _fpsAverageStartWindow.updateAverage(currentFPS); - _fpsAverageDownWindow.updateAverage(currentFPS); - _fpsAverageUpWindow.updateAverage(currentFPS); - - quint64 now = usecTimestampNow(); - - quint64 elapsedSinceDownShift = now - _lastDownShift; - quint64 elapsedSinceUpShift = now - _lastUpShift; - - quint64 lastStableOrUpshift = glm::max(_lastUpShift, _lastStable); - quint64 elapsedSinceStableOrUpShift = now - lastStableOrUpshift; - - if (_automaticLODAdjust) { - bool changed = false; - - // LOD Downward adjustment - // If we've been downshifting, we watch a shorter downshift window so that we will quickly move toward our - // target frame rate. But if we haven't just done a downshift (either because our last shift was an upshift, - // or because we've just started out) then we look at a much longer window to consider whether or not to start - // downshifting. - bool doDownShift = false; - - if (_isDownshifting) { - // only consider things if our DOWN_SHIFT time has elapsed... - if (elapsedSinceDownShift > DOWN_SHIFT_ELPASED) { - doDownShift = _fpsAverageDownWindow.getAverage() < getLODDecreaseFPS(); - - if (!doDownShift) { - qCDebug(interfaceapp) << "---- WE APPEAR TO BE DONE DOWN SHIFTING -----"; - _isDownshifting = false; - _lastStable = now; - } - } - } else { - doDownShift = (elapsedSinceStableOrUpShift > START_SHIFT_ELPASED - && _fpsAverageStartWindow.getAverage() < getLODDecreaseFPS()); - } - - if (doDownShift) { - - // Octree items... stepwise adjustment + uint64_t now = usecTimestampNow(); + if (currentFPS < getLODDecreaseFPS()) { + if (now > _decreaseFPSExpiry) { + _decreaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; if (_octreeSizeScale > ADJUST_LOD_MIN_SIZE_SCALE) { - _octreeSizeScale *= ADJUST_LOD_DOWN_BY; + _octreeSizeScale *= LOD_AUTO_ADJUST_DECREMENT_FACTOR; if (_octreeSizeScale < ADJUST_LOD_MIN_SIZE_SCALE) { _octreeSizeScale = ADJUST_LOD_MIN_SIZE_SCALE; } - changed = true; - } - - if (changed) { - if (_isDownshifting) { - // subsequent downshift - qCDebug(interfaceapp) << "adjusting LOD DOWN..." - << "average fps for last "<< DOWN_SHIFT_WINDOW_IN_SECS <<"seconds was " - << _fpsAverageDownWindow.getAverage() - << "minimum is:" << getLODDecreaseFPS() - << "elapsedSinceDownShift:" << elapsedSinceDownShift - << " NEW _octreeSizeScale=" << _octreeSizeScale; - } else { - // first downshift - qCDebug(interfaceapp) << "adjusting LOD DOWN after initial delay..." - << "average fps for last "<< START_DELAY_WINDOW_IN_SECS <<"seconds was " - << _fpsAverageStartWindow.getAverage() - << "minimum is:" << getLODDecreaseFPS() - << "elapsedSinceUpShift:" << elapsedSinceUpShift - << " NEW _octreeSizeScale=" << _octreeSizeScale; - } - - _lastDownShift = now; - _isDownshifting = true; - + qCDebug(interfaceapp) << "adjusting LOD DOWN" + << "fps =" << currentFPS + << "targetFPS =" << getLODDecreaseFPS() + << "octreeSizeScale =" << _octreeSizeScale; emit LODDecreased(); } - } else { - - // LOD Upward adjustment - if (elapsedSinceUpShift > UP_SHIFT_ELPASED) { - - if (_fpsAverageUpWindow.getAverage() > getLODIncreaseFPS()) { - - // Octee items... stepwise adjustment - if (_octreeSizeScale < ADJUST_LOD_MAX_SIZE_SCALE) { - if (_octreeSizeScale < ADJUST_LOD_MIN_SIZE_SCALE) { - _octreeSizeScale = ADJUST_LOD_MIN_SIZE_SCALE; - } else { - _octreeSizeScale *= ADJUST_LOD_UP_BY; - } - if (_octreeSizeScale > ADJUST_LOD_MAX_SIZE_SCALE) { - _octreeSizeScale = ADJUST_LOD_MAX_SIZE_SCALE; - } - changed = true; - } - } - - if (changed) { - qCDebug(interfaceapp) << "adjusting LOD UP... average fps for last "<< UP_SHIFT_WINDOW_IN_SECS <<"seconds was " - << _fpsAverageUpWindow.getAverage() - << "upshift point is:" << getLODIncreaseFPS() - << "elapsedSinceUpShift:" << elapsedSinceUpShift - << " NEW _octreeSizeScale=" << _octreeSizeScale; - - _lastUpShift = now; - _isDownshifting = false; - - emit LODIncreased(); - } - } + _decreaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; } - - if (changed) { - auto lodToolsDialog = DependencyManager::get()->getLodToolsDialog(); - if (lodToolsDialog) { - lodToolsDialog->reloadSliders(); + _increaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; + } else if (currentFPS > getLODIncreaseFPS()) { + if (now > _increaseFPSExpiry) { + _increaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; + if (_octreeSizeScale < ADJUST_LOD_MAX_SIZE_SCALE) { + if (_octreeSizeScale < ADJUST_LOD_MIN_SIZE_SCALE) { + _octreeSizeScale = ADJUST_LOD_MIN_SIZE_SCALE; + } else { + _octreeSizeScale *= LOD_AUTO_ADJUST_INCREMENT_FACTOR; + } + if (_octreeSizeScale > ADJUST_LOD_MAX_SIZE_SCALE) { + _octreeSizeScale = ADJUST_LOD_MAX_SIZE_SCALE; + } + qCDebug(interfaceapp) << "adjusting LOD UP" + << "fps =" << currentFPS + << "targetFPS =" << getLODDecreaseFPS() + << "octreeSizeScale =" << _octreeSizeScale; + emit LODIncreased(); } + _increaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; } + _decreaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; + } else { + _increaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; + _decreaseFPSExpiry = _increaseFPSExpiry; + } + + if (oldOctreeSizeScale != _octreeSizeScale) { + auto lodToolsDialog = DependencyManager::get()->getLodToolsDialog(); + if (lodToolsDialog) { + lodToolsDialog->reloadSliders(); + } + // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime + // to be at middle of target zone. It will drift close to its true value within + // about three few LOD_ADJUST_TIMESCALEs and we'll adjust again as necessary. + float expectedFPS = 0.5f * (getLODIncreaseFPS() + getLODDecreaseFPS()); + _avgRenderTime = MSECS_PER_SECOND / expectedFPS; } } void LODManager::resetLODAdjust() { - _fpsAverageStartWindow.reset(); - _fpsAverageDownWindow.reset(); - _fpsAverageUpWindow.reset(); - _lastUpShift = _lastDownShift = usecTimestampNow(); - _isDownshifting = false; + _decreaseFPSExpiry = _increaseFPSExpiry = usecTimestampNow() + LOD_AUTO_ADJUST_PERIOD; } const float MIN_DECREASE_FPS = 0.5f; @@ -206,7 +146,7 @@ float LODManager::getDesktopLODDecreaseFPS() const { } float LODManager::getDesktopLODIncreaseFPS() const { - return glm::max(((float)MSECS_PER_SECOND / _desktopMaxRenderTime) + INCREASE_LOD_GAP, MAX_LIKELY_DESKTOP_FPS); + return glm::max(((float)MSECS_PER_SECOND / _desktopMaxRenderTime) + INCREASE_LOD_GAP_FPS, MAX_LIKELY_DESKTOP_FPS); } void LODManager::setHMDLODDecreaseFPS(float fps) { @@ -222,7 +162,7 @@ float LODManager::getHMDLODDecreaseFPS() const { } float LODManager::getHMDLODIncreaseFPS() const { - return glm::max(((float)MSECS_PER_SECOND / _hmdMaxRenderTime) + INCREASE_LOD_GAP, MAX_LIKELY_HMD_FPS); + return glm::max(((float)MSECS_PER_SECOND / _hmdMaxRenderTime) + INCREASE_LOD_GAP_FPS, MAX_LIKELY_HMD_FPS); } QString LODManager::getLODFeedbackText() { diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index 1b3797a0ca..cf38342db0 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -19,29 +19,13 @@ #include #include -const float DEFAULT_DESKTOP_LOD_DOWN_FPS = 20.0; -const float DEFAULT_HMD_LOD_DOWN_FPS = 20.0; +const float DEFAULT_DESKTOP_LOD_DOWN_FPS = 30.0f; +const float DEFAULT_HMD_LOD_DOWN_FPS = 45.0f; const float DEFAULT_DESKTOP_MAX_RENDER_TIME = (float)MSECS_PER_SECOND / DEFAULT_DESKTOP_LOD_DOWN_FPS; // msec const float DEFAULT_HMD_MAX_RENDER_TIME = (float)MSECS_PER_SECOND / DEFAULT_HMD_LOD_DOWN_FPS; // msec -const float MAX_LIKELY_DESKTOP_FPS = 59.0; // this is essentially, V-synch - 1 fps -const float MAX_LIKELY_HMD_FPS = 74.0; // this is essentially, V-synch - 1 fps -const float INCREASE_LOD_GAP = 15.0f; - -const float START_DELAY_WINDOW_IN_SECS = 3.0f; // wait at least this long after steady state/last upshift to consider downshifts -const float DOWN_SHIFT_WINDOW_IN_SECS = 0.5f; -const float UP_SHIFT_WINDOW_IN_SECS = 2.5f; - -const int ASSUMED_FPS = 60; -const quint64 START_SHIFT_ELPASED = USECS_PER_SECOND * START_DELAY_WINDOW_IN_SECS; -const quint64 DOWN_SHIFT_ELPASED = USECS_PER_SECOND * DOWN_SHIFT_WINDOW_IN_SECS; // Consider adjusting LOD down after half a second -const quint64 UP_SHIFT_ELPASED = USECS_PER_SECOND * UP_SHIFT_WINDOW_IN_SECS; - -const int START_DELAY_SAMPLES_OF_FRAMES = ASSUMED_FPS * START_DELAY_WINDOW_IN_SECS; -const int DOWN_SHIFT_SAMPLES_OF_FRAMES = ASSUMED_FPS * DOWN_SHIFT_WINDOW_IN_SECS; -const int UP_SHIFT_SAMPLES_OF_FRAMES = ASSUMED_FPS * UP_SHIFT_WINDOW_IN_SECS; - -const float ADJUST_LOD_DOWN_BY = 0.9f; -const float ADJUST_LOD_UP_BY = 1.1f; +const float MAX_LIKELY_DESKTOP_FPS = 59.0f; // this is essentially, V-synch - 1 fps +const float MAX_LIKELY_HMD_FPS = 74.0f; // this is essentially, V-synch - 1 fps +const float INCREASE_LOD_GAP_FPS = 15.0f; // fps // The default value DEFAULT_OCTREE_SIZE_SCALE means you can be 400 meters away from a 1 meter object in order to see it (which is ~20:20 vision). const float ADJUST_LOD_MAX_SIZE_SCALE = DEFAULT_OCTREE_SIZE_SCALE; @@ -78,7 +62,7 @@ public: Q_INVOKABLE float getLODIncreaseFPS(); static bool shouldRender(const RenderArgs* args, const AABox& bounds); - void autoAdjustLOD(float batchTime, float engineRunTime, float deltaTimeSec); + void autoAdjustLOD(float renderTime, float realTimeDelta); void loadSettings(); void saveSettings(); @@ -92,21 +76,15 @@ private: LODManager(); bool _automaticLODAdjust = true; - float _avgRenderTime { 0.0 }; + float _avgRenderTime { 0.0f }; float _desktopMaxRenderTime { DEFAULT_DESKTOP_MAX_RENDER_TIME }; float _hmdMaxRenderTime { DEFAULT_HMD_MAX_RENDER_TIME }; float _octreeSizeScale = DEFAULT_OCTREE_SIZE_SCALE; int _boundaryLevelAdjust = 0; - - quint64 _lastDownShift = 0; - quint64 _lastUpShift = 0; - quint64 _lastStable = 0; - bool _isDownshifting = false; // start out as if we're not downshifting - - SimpleMovingAverage _fpsAverageStartWindow = START_DELAY_SAMPLES_OF_FRAMES; - SimpleMovingAverage _fpsAverageDownWindow = DOWN_SHIFT_SAMPLES_OF_FRAMES; - SimpleMovingAverage _fpsAverageUpWindow = UP_SHIFT_SAMPLES_OF_FRAMES; + + uint64_t _decreaseFPSExpiry { 0 }; + uint64_t _increaseFPSExpiry { 0 }; }; #endif // hifi_LODManager_h diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 9bbb72357b..eeda9e9036 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -101,7 +101,7 @@ Menu::Menu() { auto action = addActionToQMenuAndActionHash(editMenu, MenuOption::RunningScripts, Qt::CTRL | Qt::Key_J); connect(action, &QAction::triggered, [] { static const QUrl widgetUrl("hifi/dialogs/RunningScripts.qml"); - static const QUrl tabletUrl("../../hifi/dialogs/TabletRunningScripts.qml"); + static const QUrl tabletUrl("hifi/dialogs/TabletRunningScripts.qml"); static const QString name("RunningScripts"); qApp->showDialog(widgetUrl, tabletUrl, name); }); @@ -338,7 +338,7 @@ Menu::Menu() { connect(action, &QAction::triggered, [] { auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); auto hmd = DependencyManager::get(); - tablet->loadQMLSource("ControllerSettings.qml"); + tablet->loadQMLSource("hifi/tablet/ControllerSettings.qml"); if (!hmd->getShouldShowTablet()) { hmd->toggleShouldShowTablet(); @@ -645,7 +645,8 @@ Menu::Menu() { // Developer > Timing >>> MenuWrapper* timingMenu = developerMenu->addMenu("Timing"); MenuWrapper* perfTimerMenu = timingMenu->addMenu("Performance Timer"); - addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayDebugTimingDetails, 0, false); + addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayDebugTimingDetails, 0, false, + qApp, SLOT(enablePerfStats(bool))); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::OnlyDisplayTopTen, 0, true); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandUpdateTiming, 0, false); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandMyAvatarTiming, 0, false); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c874205611..02a1959a95 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -81,6 +81,7 @@ const QString& DEFAULT_AVATAR_COLLISION_SOUND_URL = "https://hifi-public.s3.amaz const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; const float MyAvatar::ZOOM_DEFAULT = 1.5f; +const float MIN_SCALE_CHANGED_DELTA = 0.001f; MyAvatar::MyAvatar(QThread* thread) : Avatar(thread), @@ -670,6 +671,11 @@ void MyAvatar::updateSensorToWorldMatrix() { glm::mat4 desiredMat = createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), getWorldOrientation(), getWorldPosition()); _sensorToWorldMatrix = desiredMat * glm::inverse(_bodySensorMatrix); + bool hasSensorToWorldScaleChanged = false; + if (fabsf(getSensorToWorldScale() - sensorToWorldScale) > MIN_SCALE_CHANGED_DELTA) { + hasSensorToWorldScaleChanged = true; + } + lateUpdatePalms(); if (_enableDebugDrawSensorToWorldMatrix) { @@ -678,9 +684,13 @@ void MyAvatar::updateSensorToWorldMatrix() { } _sensorToWorldMatrixCache.set(_sensorToWorldMatrix); - updateJointFromController(controller::Action::LEFT_HAND, _controllerLeftHandMatrixCache); updateJointFromController(controller::Action::RIGHT_HAND, _controllerRightHandMatrixCache); + + if (hasSensorToWorldScaleChanged) { + emit sensorToWorldScaleChanged(sensorToWorldScale); + } + } // Update avatar head rotation with sensor data @@ -1405,6 +1415,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _skeletonModel->setVisibleInScene(true, qApp->getMain3DScene()); _headBoneSet.clear(); emit skeletonChanged(); + } @@ -1440,6 +1451,7 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN UserActivityLogger::getInstance().changedModel("skeleton", urlString); } markIdentityDataChanged(); + } void MyAvatar::setAttachmentData(const QVector& attachmentData) { diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index a707031167..f249be33ea 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -9,9 +9,12 @@ #include "MySkeletonModel.h" #include +#include #include "Application.h" #include "InterfaceLogging.h" +#include "AnimUtil.h" + MySkeletonModel::MySkeletonModel(Avatar* owningAvatar, QObject* parent) : SkeletonModel(owningAvatar, parent) { } @@ -30,6 +33,39 @@ Rig::CharacterControllerState convertCharacterControllerState(CharacterControlle }; } +static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { + glm::mat4 hipsMat = myAvatar->deriveBodyFromHMDSensor(); + glm::vec3 hipsPos = extractTranslation(hipsMat); + glm::quat hipsRot = glmExtractRotation(hipsMat); + + glm::mat4 avatarToWorldMat = myAvatar->getTransform().getMatrix(); + glm::mat4 worldToSensorMat = glm::inverse(myAvatar->getSensorToWorldMatrix()); + glm::mat4 avatarToSensorMat = worldToSensorMat * avatarToWorldMat; + + // dampen hips rotation, by mixing it with the avatar orientation in sensor space + const float MIX_RATIO = 0.5f; + hipsRot = safeLerp(glmExtractRotation(avatarToSensorMat), hipsRot, MIX_RATIO); + + if (isFlying) { + // rotate the hips back to match the flying animation. + + const float TILT_ANGLE = 0.523f; + const glm::quat tiltRot = glm::angleAxis(TILT_ANGLE, transformVectorFast(avatarToSensorMat, -Vectors::UNIT_X)); + + glm::vec3 headPos; + int headIndex = myAvatar->getJointIndex("Head"); + if (headIndex != -1) { + headPos = transformPoint(avatarToSensorMat, myAvatar->getAbsoluteJointTranslationInObjectFrame(headIndex)); + } else { + headPos = transformPoint(myAvatar->getSensorToWorldMatrix(), myAvatar->getHMDSensorPosition()); + } + hipsRot = tiltRot * hipsRot; + hipsPos = headPos + tiltRot * (hipsPos - headPos); + } + + return AnimPose(hipsRot * Quaternions::Y_180, hipsPos); +} + // Called within Model::simulate call, below. void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { const FBXGeometry& geometry = getFBXGeometry(); @@ -124,6 +160,39 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { } } + // if hips are not under direct control, estimate the hips position. + if (avatarHeadPose.isValid() && !params.primaryControllerActiveFlags[Rig::PrimaryControllerType_Hips]) { + bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS); + + if (!_prevHipsValid) { + AnimPose hips = computeHipsInSensorFrame(myAvatar, isFlying); + _prevHips = hips; + } + + AnimPose hips = computeHipsInSensorFrame(myAvatar, isFlying); + + // smootly lerp hips, in sensorframe, with different coeff for horiz and vertical translation. + const float ROT_ALPHA = 0.9f; + const float TRANS_HORIZ_ALPHA = 0.9f; + const float TRANS_VERT_ALPHA = 0.1f; + float hipsY = hips.trans().y; + hips.trans() = lerp(hips.trans(), _prevHips.trans(), TRANS_HORIZ_ALPHA); + hips.trans().y = lerp(hipsY, _prevHips.trans().y, TRANS_VERT_ALPHA); + hips.rot() = safeLerp(hips.rot(), _prevHips.rot(), ROT_ALPHA); + + _prevHips = hips; + _prevHipsValid = true; + + glm::mat4 invRigMat = glm::inverse(myAvatar->getTransform().getMatrix() * Matrices::Y_180); + AnimPose sensorToRigPose(invRigMat * myAvatar->getSensorToWorldMatrix()); + + params.primaryControllerPoses[Rig::PrimaryControllerType_Hips] = sensorToRigPose * hips; + params.primaryControllerActiveFlags[Rig::PrimaryControllerType_Hips] = true; + + } else { + _prevHipsValid = false; + } + params.isTalking = head->getTimeWithoutTalking() <= 1.5f; // pass detailed torso k-dops to rig. diff --git a/interface/src/avatar/MySkeletonModel.h b/interface/src/avatar/MySkeletonModel.h index ad0ae1b8e9..d9f57a439a 100644 --- a/interface/src/avatar/MySkeletonModel.h +++ b/interface/src/avatar/MySkeletonModel.h @@ -25,6 +25,9 @@ public: private: void updateFingers(); + + AnimPose _prevHips; // sensor frame + bool _prevHipsValid { false }; }; #endif // hifi_MySkeletonModel_h diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index b0d293584c..3257a634c7 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -130,7 +130,7 @@ QString amountString(const QString& label, const QString&color, const QJsonValue return result + QString(""); } -static const QString MARKETPLACE_ITEMS_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL.toString() + "/marketplace/items/"; +static const QString MARKETPLACE_ITEMS_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace/items/"; void Ledger::historySuccess(QNetworkReply& reply) { // here we send a historyResult with some extra stuff in it // Namely, the styled text we'd like to show. The issue is the diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 69089df9c2..62e87f9c66 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -16,9 +16,7 @@ #include "Wallet.h" #include -HIFI_QML_DEF(QmlCommerce) - -QmlCommerce::QmlCommerce(QQuickItem* parent) : OffscreenQmlDialog(parent) { +QmlCommerce::QmlCommerce() { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); connect(ledger.data(), &Ledger::buyResult, this, &QmlCommerce::buyResult); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index fd8dd0d395..c53e73d565 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -16,16 +16,14 @@ #define hifi_QmlCommerce_h #include -#include #include -class QmlCommerce : public OffscreenQmlDialog { +class QmlCommerce : public QObject { Q_OBJECT - HIFI_QML_DECL public: - QmlCommerce(QQuickItem* parent = nullptr); + QmlCommerce(); signals: void walletStatusResult(uint walletStatus); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 69914e97a4..00941d6c50 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -315,7 +315,6 @@ Wallet::Wallet() { } walletScriptingInterface->setWalletStatus(status); - emit walletStatusResult(status); }); auto accountManager = DependencyManager::get(); @@ -491,6 +490,7 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() { } if (_publicKeys.count() > 0) { // we _must_ be authenticated if the publicKeys are there + DependencyManager::get()->setWalletStatus((uint)WalletStatus::WALLET_STATUS_READY); return true; } @@ -503,6 +503,7 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() { // be sure to add the public key so we don't do this over and over _publicKeys.push_back(publicKey.toBase64()); + DependencyManager::get()->setWalletStatus((uint)WalletStatus::WALLET_STATUS_READY); return true; } } @@ -801,15 +802,12 @@ void Wallet::account() { void Wallet::getWalletStatus() { auto walletScriptingInterface = DependencyManager::get(); - uint status; if (DependencyManager::get()->isLoggedIn()) { // This will set account info for the wallet, allowing us to decrypt and display the security image. account(); } else { - status = (uint)WalletStatus::WALLET_STATUS_NOT_LOGGED_IN; - emit walletStatusResult(status); - walletScriptingInterface->setWalletStatus(status); + walletScriptingInterface->setWalletStatus((uint)WalletStatus::WALLET_STATUS_NOT_LOGGED_IN); return; } } diff --git a/interface/src/networking/CloseEventSender.cpp b/interface/src/networking/CloseEventSender.cpp index de8bd897b2..fe939afe05 100644 --- a/interface/src/networking/CloseEventSender.cpp +++ b/interface/src/networking/CloseEventSender.cpp @@ -28,7 +28,7 @@ QNetworkRequest createNetworkRequest() { QNetworkRequest request; - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); requestURL.setPath(USER_ACTIVITY_URL); request.setUrl(requestURL); diff --git a/interface/src/raypick/StylusPointer.cpp b/interface/src/raypick/StylusPointer.cpp index 21d257048c..5afbc2058a 100644 --- a/interface/src/raypick/StylusPointer.cpp +++ b/interface/src/raypick/StylusPointer.cpp @@ -23,10 +23,10 @@ static const float WEB_STYLUS_LENGTH = 0.2f; static const float TABLET_MIN_HOVER_DISTANCE = -0.1f; static const float TABLET_MAX_HOVER_DISTANCE = 0.1f; static const float TABLET_MIN_TOUCH_DISTANCE = -0.1f; -static const float TABLET_MAX_TOUCH_DISTANCE = 0.01f; +static const float TABLET_MAX_TOUCH_DISTANCE = 0.005f; static const float HOVER_HYSTERESIS = 0.01f; -static const float TOUCH_HYSTERESIS = 0.02f; +static const float TOUCH_HYSTERESIS = 0.001f; static const float STYLUS_MOVE_DELAY = 0.33f * USECS_PER_SECOND; static const float TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0481f; diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index f8db061299..c2d2b69883 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -34,7 +34,7 @@ bool ClipboardScriptingInterface::exportEntities(const QString& filename, const return retVal; } -bool ClipboardScriptingInterface::exportEntities(const QString& filename, float x, float y, float z, float s) { +bool ClipboardScriptingInterface::exportEntities(const QString& filename, float x, float y, float z, float scale) { bool retVal; BLOCKING_INVOKE_METHOD(qApp, "exportEntities", Q_RETURN_ARG(bool, retVal), @@ -42,7 +42,7 @@ bool ClipboardScriptingInterface::exportEntities(const QString& filename, float Q_ARG(float, x), Q_ARG(float, y), Q_ARG(float, z), - Q_ARG(float, s)); + Q_ARG(float, scale)); return retVal; } diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index 826732c777..cce300e831 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -18,6 +18,8 @@ #include /**jsdoc + * The Clipboard API enables you to export and import entities to and from JSON files. + * * @namespace Clipboard */ class ClipboardScriptingInterface : public QObject { @@ -25,48 +27,56 @@ class ClipboardScriptingInterface : public QObject { public: ClipboardScriptingInterface(); -signals: - void readyToImport(); - public: /**jsdoc + * Compute the extents of the contents held in the clipboard. * @function Clipboard.getContentsDimensions - * @return {Vec3} The extents of the contents held in the clipboard. + * @returns {Vec3} The extents of the contents held in the clipboard. */ Q_INVOKABLE glm::vec3 getContentsDimensions(); /**jsdoc - * Compute largest dimension of the extents of the contents held in the clipboard + * Compute the largest dimension of the extents of the contents held in the clipboard. * @function Clipboard.getClipboardContentsLargestDimension - * @return {float} The largest dimension computed. + * @returns {number} The largest dimension computed. */ Q_INVOKABLE float getClipboardContentsLargestDimension(); /**jsdoc - * Import entities from a .json file containing entity data into the clipboard. - * You can generate * a .json file using {Clipboard.exportEntities}. + * Import entities from a JSON file containing entity data into the clipboard. + * You can generate a JSON file using {@link Clipboard.exportEntities}. * @function Clipboard.importEntities - * @param {string} filename Filename of file to import. - * @return {bool} True if the import was succesful, otherwise false. + * @param {string} filename Path and name of file to import. + * @returns {boolean} true if the import was successful, otherwise false. */ Q_INVOKABLE bool importEntities(const QString& filename); /**jsdoc - * Export the entities listed in `entityIDs` to the file `filename` + * Export the entities specified to a JSON file. * @function Clipboard.exportEntities - * @param {string} filename Path to the file to export entities to. - * @param {EntityID[]} entityIDs IDs of entities to export. - * @return {bool} True if the export was succesful, otherwise false. + * @param {string} filename Path and name of the file to export the entities to. Should have the extension ".json". + * @param {Uuid[]} entityIDs Array of IDs of the entities to export. + * @returns {boolean} true if the export was successful, otherwise false. */ Q_INVOKABLE bool exportEntities(const QString& filename, const QVector& entityIDs); - Q_INVOKABLE bool exportEntities(const QString& filename, float x, float y, float z, float s); + + /**jsdoc + * Export the entities with centers within a cube to a JSON file. + * @function Clipboard.exportEntities + * @param {string} filename Path and name of the file to export the entities to. Should have the extension ".json". + * @param {number} x X-coordinate of the cube center. + * @param {number} y Y-coordinate of the cube center. + * @param {number} z Z-coordinate of the cube center. + * @param {number} scale Half dimension of the cube. + * @returns {boolean} true if the export was successful, otherwise false. + */ + Q_INVOKABLE bool exportEntities(const QString& filename, float x, float y, float z, float scale); /**jsdoc * Paste the contents of the clipboard into the world. * @function Clipboard.pasteEntities - * @param {Vec3} position Position to paste clipboard at. - * @return {EntityID[]} Array of entity IDs for the new entities that were - * created as a result of the paste operation. + * @param {Vec3} position Position to paste the clipboard contents at. + * @returns {Uuid[]} Array of entity IDs for the new entities that were created as a result of the paste operation. */ Q_INVOKABLE QVector pasteEntities(glm::vec3 position); }; diff --git a/interface/src/scripting/MenuScriptingInterface.h b/interface/src/scripting/MenuScriptingInterface.h index b9c29ccf08..59cfc76d21 100644 --- a/interface/src/scripting/MenuScriptingInterface.h +++ b/interface/src/scripting/MenuScriptingInterface.h @@ -18,15 +18,18 @@ class MenuItemProperties; /**jsdoc - * The `Menu` provides access to the menu that is shown at the top of the window - * shown on a user's desktop and the right click menu that is accessible - * in both Desktop and HMD mode. + * The Menu API provides access to the menu that is displayed at the top of the window + * on a user's desktop and in the tablet when the "MENU" button is pressed. + * + *

* *

Groupings

- * A `grouping` is a way to group a set of menus and/or menu items together - * so that they can all be set visible or invisible as a group. There are - * 2 available groups: "Advanced" and "Developer" + * + * A "grouping" provides a way to group a set of menus or menu items together so + * that they can all be set visible or invisible as a group. + * There are two available groups: "Advanced" and "Developer". * These groupings can be toggled in the "Settings" menu. + * If a menu item doesn't belong to a group it is always displayed. * * @namespace Menu */ @@ -55,86 +58,113 @@ public slots: /**jsdoc * Add a new top-level menu. * @function Menu.addMenu - * @param {string} menuName Name that will be shown in the menu. - * @param {string} grouping Name of the grouping to add this menu to. + * @param {string} menuName - Name that will be displayed for the menu. Nested menus can be described using the ">" symbol. + * @param {string} [grouping] - Name of the grouping, if any, to add this menu to. + * + * @example Add a menu and a nested submenu. + * Menu.addMenu("Test Menu"); + * Menu.addMenu("Test Menu > Test Sub Menu"); + * + * @example Add a menu to the Settings menu that is only visible if Settings > Advanced is enabled. + * Menu.addMenu("Settings > Test Grouping Menu", "Advanced"); */ void addMenu(const QString& menuName, const QString& grouping = QString()); /**jsdoc * Remove a top-level menu. * @function Menu.removeMenu - * @param {string} menuName Name of the menu to remove. + * @param {string} menuName - Name of the menu to remove. + * @example Remove a menu and nested submenu. + * Menu.removeMenu("Test Menu > Test Sub Menu"); + * Menu.removeMenu("Test Menu"); */ void removeMenu(const QString& menuName); /**jsdoc * Check whether a top-level menu exists. * @function Menu.menuExists - * @param {string} menuName Name of the menu to check for existence. - * @return {bool} `true` if the menu exists, otherwise `false`. + * @param {string} menuName - Name of the menu to check for existence. + * @returns {boolean} true if the menu exists, otherwise false. + * @example Check if the "Developer" menu exists. + * if (Menu.menuExists("Developer")) { + * print("Developer menu exists."); + * } */ bool menuExists(const QString& menuName); /**jsdoc - * Add a separator with an unclickable label below it. - * The line will be placed at the bottom of the menu. + * Add a separator with an unclickable label below it. The separator will be placed at the bottom of the menu. + * If you want to add a separator at a specific point in the menu, use {@link Menu.addMenuItem} with + * {@link Menu.MenuItemProperties} instead. * @function Menu.addSeparator - * @param {string} menuName Name of the menu to add a separator to. - * @param {string} separatorName Name of the separator that will be shown (but unclickable) below the separator line. + * @param {string} menuName - Name of the menu to add a separator to. + * @param {string} separatorName - Name of the separator that will be displayed as the label below the separator line. + * @example Add a separator. + * Menu.addSeparator("Developer","Test Separator"); */ void addSeparator(const QString& menuName, const QString& separatorName); /**jsdoc - * Remove a separator and its label from a menu. + * Remove a separator from a menu. * @function Menu.removeSeparator - * @param {string} menuName Name of the menu to remove a separator from. - * @param {string} separatorName Name of the separator to remove. + * @param {string} menuName - Name of the menu to remove the separator from. + * @param {string} separatorName - Name of the separator to remove. + * @example Remove a separator. + * Menu.removeSeparator("Developer","Test Separator"); */ void removeSeparator(const QString& menuName, const QString& separatorName); /**jsdoc * Add a new menu item to a menu. * @function Menu.addMenuItem - * @param {Menu.MenuItemProperties} properties + * @param {Menu.MenuItemProperties} properties - Properties of the menu item to create. + * @example Add a menu item using {@link Menu.MenuItemProperties}. + * Menu.addMenuItem({ + * menuName: "Developer", + * menuItemName: "Test", + * afterItem: "Log", + * shortcutKey: "Ctrl+Shift+T", + * grouping: "Advanced" + * }); */ void addMenuItem(const MenuItemProperties& properties); /**jsdoc - * Add a new menu item to a menu. + * Add a new menu item to a menu. The new item is added at the end of the menu. * @function Menu.addMenuItem - * @param {string} menuName Name of the menu to add a menu item to. - * @param {string} menuItem Name of the menu item. This is what will be displayed in the menu. - * @param {string} shortcutKey A shortcut key that can be used to trigger the menu item. + * @param {string} menuName - Name of the menu to add a menu item to. + * @param {string} menuItem - Name of the menu item. This is what will be displayed in the menu. + * @param {string} [shortcutKey] A shortcut key that can be used to trigger the menu item. + * @example Add a menu item to the end of the "Developer" menu. + * Menu.addMenuItem("Developer", "Test", "Ctrl+Shift+T"); */ void addMenuItem(const QString& menuName, const QString& menuitem, const QString& shortcutKey); - - /**jsdoc - * Add a new menu item to a menu. - * @function Menu.addMenuItem - * @param {string} menuName Name of the menu to add a menu item to. - * @param {string} menuItem Name of the menu item. This is what will be displayed in the menu. - */ void addMenuItem(const QString& menuName, const QString& menuitem); /**jsdoc * Remove a menu item from a menu. * @function Menu.removeMenuItem - * @param {string} menuName Name of the menu to remove a menu item from. - * @param {string} menuItem Name of the menu item to remove. + * @param {string} menuName - Name of the menu to remove a menu item from. + * @param {string} menuItem - Name of the menu item to remove. + * Menu.removeMenuItem("Developer", "Test"); */ void removeMenuItem(const QString& menuName, const QString& menuitem); /**jsdoc * Check if a menu item exists. * @function Menu.menuItemExists - * @param {string} menuName Name of the menu that the menu item is in. - * @param {string} menuItem Name of the menu item to check for existence of. - * @return {bool} `true` if the menu item exists, otherwise `false`. + * @param {string} menuName - Name of the menu that the menu item is in. + * @param {string} menuItem - Name of the menu item to check for existence of. + * @returns {boolean} true if the menu item exists, otherwise false. + * @example Determine if the Developer > Stats menu exists. + * if (Menu.menuItemExists("Developer", "Stats")) { + * print("Developer > Stats menu item exists."); + * } */ bool menuItemExists(const QString& menuName, const QString& menuitem); /** - * Not working, will not document until fixed + * TODO: Not working; don't document until fixed. */ void addActionGroup(const QString& groupName, const QStringList& actionList, const QString& selected = QString()); @@ -143,53 +173,73 @@ public slots: /**jsdoc * Check whether a checkable menu item is checked. * @function Menu.isOptionChecked - * @param {string} menuOption The name of the menu item. - * @return `true` if the option is checked, otherwise false. + * @param {string} menuOption - The name of the menu item. + * @returns {boolean} true if the option is checked, otherwise false. + * @example Report whether the Settings > Advanced menu item is turned on. + * print(Menu.isOptionChecked("Advanced Menus")); // true or false */ bool isOptionChecked(const QString& menuOption); /**jsdoc * Set a checkable menu item as checked or unchecked. * @function Menu.setIsOptionChecked - * @param {string} menuOption The name of the menu item to modify. - * @param {bool} isChecked If `true`, the menu item will be checked, otherwise it will not be checked. + * @param {string} menuOption - The name of the menu item to modify. + * @param {boolean} isChecked - If true, the menu item will be checked, otherwise it will not be checked. + * @example Turn on Settings > Advanced Menus. + * Menu.setIsOptionChecked("Advanced Menus", true); + * print(Menu.isOptionChecked("Advanced Menus")); // true */ void setIsOptionChecked(const QString& menuOption, bool isChecked); /**jsdoc - * Toggle the status of a checkable menu item. If it is checked, it will be unchecked. - * If it is unchecked, it will be checked. - * @function Menu.setIsOptionChecked - * @param {string} menuOption The name of the menu item to toggle. + * Trigger the menu item as if the user clicked on it. + * @function Menu.triggerOption + * @param {string} menuOption - The name of the menu item to trigger. + * @example Open the help window. + * Menu.triggerOption('Help...'); */ void triggerOption(const QString& menuOption); /**jsdoc - * Check whether a menu is enabled. If a menu is disabled it will be greyed out - * and unselectable. + * Check whether a menu or menu item is enabled. If disabled, the item is grayed out and unusable. * Menus are enabled by default. * @function Menu.isMenuEnabled - * @param {string} menuName The name of the menu to check. - * @return {bool} `true` if the menu is enabled, otherwise false. + * @param {string} menuName The name of the menu or menu item to check. + * @returns {boolean} true if the menu is enabled, otherwise false. + * @example Report with the Settings > Advanced Menus menu item is enabled. + * print(Menu.isMenuEnabled("Settings > Advanced Menus")); // true or false */ bool isMenuEnabled(const QString& menuName); /**jsdoc - * Set a menu to be enabled or disabled. + * Set a menu or menu item to be enabled or disabled. If disabled, the item is grayed out and unusable. * @function Menu.setMenuEnabled - * @param {string} menuName The name of the menu to modify. - * @param {bool} isEnabled Whether the menu will be enabled or not. + * @param {string} menuName - The name of the menu or menu item to modify. + * @param {boolean} isEnabled - If true, the menu will be enabled, otherwise it will be disabled. + * @example Disable the Settings > Advanced Menus menu item. + * Menu.setMenuEnabled("Settings > Advanced Menus", false); + * print(Menu.isMenuEnabled("Settings > Advanced Menus")); // false */ void setMenuEnabled(const QString& menuName, bool isEnabled); + /** + * TODO: Not used or useful; will not document until used. + */ void closeInfoView(const QString& path); bool isInfoViewVisible(const QString& path); signals: /**jsdoc - * This is a signal that is emitted when a menu item is clicked. + * Triggered when a menu item is clicked (or triggered by {@link Menu.triggerOption}). * @function Menu.menuItemEvent - * @param {string} menuItem Name of the menu item that was triggered. + * @param {string} menuItem - Name of the menu item that was clicked. + * @returns {Signal} + * @example Detect menu item events. + * function onMenuItemEvent(menuItem) { + * print("You clicked on " + menuItem); + * } + * + * Menu.menuItemEvent.connect(onMenuItemEvent); */ void menuItemEvent(const QString& menuItem); }; diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index d4b4ba1480..74a15db0ce 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -11,11 +11,12 @@ #include #include +#include #include #include -#include -#include #include +#include +#include #include "Application.h" @@ -141,6 +142,15 @@ void TestScriptingInterface::endTraceEvent(QString name) { tracing::traceEvent(trace_test(), name, tracing::DurationEnd); } +void TestScriptingInterface::savePhysicsSimulationStats(QString originalPath) { + QString path = FileUtils::replaceDateTimeTokens(originalPath); + path = FileUtils::computeDocumentPath(path); + if (!FileUtils::canCreateFile(path)) { + return; + } + qApp->saveNextPhysicsStats(path); +} + void TestScriptingInterface::profileRange(const QString& name, QScriptValue fn) { PROFILE_RANGE(script, name); fn.call(); diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index 73b8f0ac93..aca07d110b 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -71,6 +71,11 @@ public slots: void endTraceEvent(QString name); + /**jsdoc + * Write detailed timing stats of next physics stepSimulation() to filename + */ + void savePhysicsSimulationStats(QString filename); + Q_INVOKABLE void profileRange(const QString& name, QScriptValue function); private: diff --git a/interface/src/scripting/WalletScriptingInterface.cpp b/interface/src/scripting/WalletScriptingInterface.cpp index 8b4279af02..71a7076bdf 100644 --- a/interface/src/scripting/WalletScriptingInterface.cpp +++ b/interface/src/scripting/WalletScriptingInterface.cpp @@ -22,3 +22,8 @@ void WalletScriptingInterface::refreshWalletStatus() { auto wallet = DependencyManager::get(); wallet->getWalletStatus(); } + +void WalletScriptingInterface::setWalletStatus(const uint& status) { + _walletStatus = status; + emit DependencyManager::get()->walletStatusResult(status); +} \ No newline at end of file diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index d7f9d9242e..5469e732c7 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -39,7 +39,9 @@ public: Q_INVOKABLE void refreshWalletStatus(); Q_INVOKABLE uint getWalletStatus() { return _walletStatus; } - void setWalletStatus(const uint& status) { _walletStatus = status; } + // setWalletStatus() should never be made Q_INVOKABLE. If it were, + // scripts could cause the Wallet to incorrectly report its status. + void setWalletStatus(const uint& status); signals: void walletStatusChanged(); diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index cab533cce3..66f208ca90 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -30,7 +30,7 @@ public: bool forwardEnabled() { return _forwardEnabled; } bool useFeed() { return _useFeed; } void setUseFeed(bool useFeed) { if (_useFeed != useFeed) { _useFeed = useFeed; emit useFeedChanged(); } } - QString metaverseServerUrl() { return NetworkingConstants::METAVERSE_SERVER_URL.toString(); } + QString metaverseServerUrl() { return NetworkingConstants::METAVERSE_SERVER_URL().toString(); } signals: void backEnabledChanged(); diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index f216bb4edc..ff2d4868df 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -31,7 +31,7 @@ #include "scripting/HMDScriptingInterface.h" -static const QVariant TABLET_ADDRESS_DIALOG = "TabletAddressDialog.qml"; +static const QVariant TABLET_ADDRESS_DIALOG = "hifi/tablet/TabletAddressDialog.qml"; template void DialogsManager::maybeCreateDialog(QPointer& member) { if (!member) { @@ -91,7 +91,7 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { ConnectionFailureDialog::hide(); } } else { - static const QUrl url("../../dialogs/TabletConnectionFailureDialog.qml"); + static const QUrl url("dialogs/TabletConnectionFailureDialog.qml"); auto hmd = DependencyManager::get(); if (visible) { tablet->initialScreen(url); diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 7ce2a0146d..2e40d3c087 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -46,7 +46,7 @@ void LoginDialog::showWithSelection() if (tablet->getToolbarMode()) { LoginDialog::show(); } else { - static const QUrl url("../../dialogs/TabletLoginDialog.qml"); + static const QUrl url("dialogs/TabletLoginDialog.qml"); tablet->initialScreen(url); if (!hmd->getShouldShowTablet()) { hmd->openTablet(); diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index e194551add..f991420fe8 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -78,6 +78,8 @@ bool Stats::includeTimingRecord(const QString& name) { return Menu::getInstance()->isOptionChecked(MenuOption::ExpandPaintGLTiming); } else if (name.startsWith("/paintGL/")) { return Menu::getInstance()->isOptionChecked(MenuOption::ExpandPaintGLTiming); + } else if (name.startsWith("step/")) { + return Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsSimulationTiming); } return true; } diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 884a174041..6922ca87b7 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -200,6 +200,31 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { } } +// JSDoc for copying to @typedefs of overlay types that inherit Base3DOverlay. +/**jsdoc + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + */ QVariant Base3DOverlay::getProperty(const QString& property) { if (property == "name") { return _name; diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 6323ff9dc8..d65b9850d7 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -61,8 +61,8 @@ public: void notifyRenderVariableChange() const; - void setProperties(const QVariantMap& properties) override; - QVariant getProperty(const QString& property) override; + virtual void setProperties(const QVariantMap& properties) override; + virtual QVariant getProperty(const QString& property) override; virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal); diff --git a/interface/src/ui/overlays/Billboardable.cpp b/interface/src/ui/overlays/Billboardable.cpp index f3881b17df..4ca024f2e5 100644 --- a/interface/src/ui/overlays/Billboardable.cpp +++ b/interface/src/ui/overlays/Billboardable.cpp @@ -23,6 +23,11 @@ void Billboardable::setProperties(const QVariantMap& properties) { } } +// JSDoc for copying to @typedefs of overlay types that inherit Billboardable. +/**jsdoc + * @property {boolean} isFacingAvatar - If true, the overlay is rotated to face the user's camera about an axis + * parallel to the user's avatar's "up" direction. + */ QVariant Billboardable::getProperty(const QString &property) { if (property == "isFacingAvatar") { return isFacingAvatar(); diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index a8230d6bee..ff267adc1f 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -16,10 +16,7 @@ QString const Circle3DOverlay::TYPE = "circle3d"; -Circle3DOverlay::Circle3DOverlay() { - memset(&_minorTickMarksColor, 0, sizeof(_minorTickMarksColor)); - memset(&_majorTickMarksColor, 0, sizeof(_majorTickMarksColor)); -} +Circle3DOverlay::Circle3DOverlay() {} Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : Planar3DOverlay(circle3DOverlay), @@ -27,17 +24,21 @@ Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : _endAt(circle3DOverlay->_endAt), _outerRadius(circle3DOverlay->_outerRadius), _innerRadius(circle3DOverlay->_innerRadius), + _innerStartColor(circle3DOverlay->_innerStartColor), + _innerEndColor(circle3DOverlay->_innerEndColor), + _outerStartColor(circle3DOverlay->_outerStartColor), + _outerEndColor(circle3DOverlay->_outerEndColor), + _innerStartAlpha(circle3DOverlay->_innerStartAlpha), + _innerEndAlpha(circle3DOverlay->_innerEndAlpha), + _outerStartAlpha(circle3DOverlay->_outerStartAlpha), + _outerEndAlpha(circle3DOverlay->_outerEndAlpha), _hasTickMarks(circle3DOverlay->_hasTickMarks), _majorTickMarksAngle(circle3DOverlay->_majorTickMarksAngle), _minorTickMarksAngle(circle3DOverlay->_minorTickMarksAngle), _majorTickMarksLength(circle3DOverlay->_majorTickMarksLength), _minorTickMarksLength(circle3DOverlay->_minorTickMarksLength), _majorTickMarksColor(circle3DOverlay->_majorTickMarksColor), - _minorTickMarksColor(circle3DOverlay->_minorTickMarksColor), - _quadVerticesID(GeometryCache::UNKNOWN_ID), - _lineVerticesID(GeometryCache::UNKNOWN_ID), - _majorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _minorTicksVerticesID(GeometryCache::UNKNOWN_ID) + _minorTickMarksColor(circle3DOverlay->_minorTickMarksColor) { } @@ -80,9 +81,8 @@ void Circle3DOverlay::render(RenderArgs* args) { Q_ASSERT(args->_batch); auto& batch = *args->_batch; - if (args->_shapePipeline) { - batch.setPipeline(args->_shapePipeline->pipeline); - } + + DependencyManager::get()->bindSimpleProgram(batch, false, isTransparent(), false, !getIsSolid(), true); batch.setModelTransform(getRenderTransform()); @@ -185,11 +185,10 @@ void Circle3DOverlay::render(RenderArgs* args) { // for our overlay, is solid means we draw a ring between the inner and outer radius of the circle, otherwise // we just draw a line... if (getHasTickMarks()) { - - if (_majorTicksVerticesID == GeometryCache::UNKNOWN_ID) { + if (!_majorTicksVerticesID) { _majorTicksVerticesID = geometryCache->allocateID(); } - if (_minorTicksVerticesID == GeometryCache::UNKNOWN_ID) { + if (!_minorTicksVerticesID) { _minorTicksVerticesID = geometryCache->allocateID(); } @@ -368,6 +367,98 @@ void Circle3DOverlay::setProperties(const QVariantMap& properties) { } } +// Overlay's color and alpha properties are overridden. And the dimensions property is not used. +/**jsdoc + * These are the properties of a circle3d {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.Circle3DProperties + * + * @property {string} type=circle3d - Has the value "circle3d". Read-only. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Vec2} dimensions=1,1 - The dimensions of the overlay. Synonyms: scale, size. + * Not used. + * + * @property {number} startAt=0 - The counter-clockwise angle from the overlay's x-axis that drawing starts at, in degrees. + * @property {number} endAt=360 - The counter-clockwise angle from the overlay's x-axis that drawing ends at, in degrees. + * @property {number} outerRadius=1 - The outer radius of the overlay, in meters. Synonym: radius. + * @property {number} innerRadius=0 - The inner radius of the overlay, in meters. + * @property {Color} color=255,255,255 - The color of the overlay. Setting this value also sets the values of + * innerStartColor, innerEndColor, outerStartColor, and outerEndColor. + * @property {Color} startColor - Sets the values of innerStartColor and outerStartColor. + * Write-only. + * @property {Color} endColor - Sets the values of innerEndColor and outerEndColor. + * Write-only. + * @property {Color} innerColor - Sets the values of innerStartColor and innerEndColor. + * Write-only. + * @property {Color} outerColor - Sets the values of outerStartColor and outerEndColor. + * Write-only. + * @property {Color} innerStartcolor - The color at the inner start point of the overlay. Write-only. + * @property {Color} innerEndColor - The color at the inner end point of the overlay. Write-only. + * @property {Color} outerStartColor - The color at the outer start point of the overlay. Write-only. + * @property {Color} outerEndColor - The color at the outer end point of the overlay. Write-only. + * @property {number} alpha=0.5 - The opacity of the overlay, 0.0 - 1.0. Setting this value also sets + * the values of innerStartAlpha, innerEndAlpha, outerStartAlpha, and + * outerEndAlpha. Synonym: Alpha; write-only. + * @property {number} startAlpha - Sets the values of innerStartAlpha and outerStartAlpha. + * Write-only. + * @property {number} endAlpha - Sets the values of innerEndAlpha and outerEndAlpha. + * Write-only. + * @property {number} innerAlpha - Sets the values of innerStartAlpha and innerEndAlpha. + * Write-only. + * @property {number} outerAlpha - Sets the values of outerStartAlpha and outerEndAlpha. + * Write-only. + * @property {number} innerStartAlpha=0 - The alpha at the inner start point of the overlay. Write-only. + * @property {number} innerEndAlpha=0 - The alpha at the inner end point of the overlay. Write-only. + * @property {number} outerStartAlpha=0 - The alpha at the outer start point of the overlay. Write-only. + * @property {number} outerEndAlpha=0 - The alpha at the outer end point of the overlay. Write-only. + + * @property {boolean} hasTickMarks=false - If true, tick marks are drawn. + * @property {number} majorTickMarksAngle=0 - The angle between major tick marks, in degrees. + * @property {number} minorTickMarksAngle=0 - The angle between minor tick marks, in degrees. + * @property {number} majorTickMarksLength=0 - The length of the major tick marks, in meters. A positive value draws tick marks + * outwards from the inner radius; a negative value draws tick marks inwards from the outer radius. + * @property {number} minorTickMarksLength=0 - The length of the minor tick marks, in meters. A positive value draws tick marks + * outwards from the inner radius; a negative value draws tick marks inwards from the outer radius. + * @property {Color} majorTickMarksColor=0,0,0 - The color of the major tick marks. + * @property {Color} minorTickMarksColor=0,0,0 - The color of the minor tick marks. + */ QVariant Circle3DOverlay::getProperty(const QString& property) { if (property == "startAt") { return _startAt; @@ -384,6 +475,30 @@ QVariant Circle3DOverlay::getProperty(const QString& property) { if (property == "innerRadius") { return _innerRadius; } + if (property == "innerStartColor") { + return xColorToVariant(_innerStartColor); + } + if (property == "innerEndColor") { + return xColorToVariant(_innerEndColor); + } + if (property == "outerStartColor") { + return xColorToVariant(_outerStartColor); + } + if (property == "outerEndColor") { + return xColorToVariant(_outerEndColor); + } + if (property == "innerStartAlpha") { + return _innerStartAlpha; + } + if (property == "innerEndAlpha") { + return _innerEndAlpha; + } + if (property == "outerStartAlpha") { + return _outerStartAlpha; + } + if (property == "outerEndAlpha") { + return _outerEndAlpha; + } if (property == "hasTickMarks") { return _hasTickMarks; } @@ -409,7 +524,6 @@ QVariant Circle3DOverlay::getProperty(const QString& property) { return Planar3DOverlay::getProperty(property); } - bool Circle3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) { diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index 11c9c9710f..ef491b7f46 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -65,22 +65,22 @@ protected: float _outerRadius { 1 }; float _innerRadius { 0 }; - xColor _innerStartColor; - xColor _innerEndColor; - xColor _outerStartColor; - xColor _outerEndColor; - float _innerStartAlpha; - float _innerEndAlpha; - float _outerStartAlpha; - float _outerEndAlpha; + xColor _innerStartColor { DEFAULT_OVERLAY_COLOR }; + xColor _innerEndColor { DEFAULT_OVERLAY_COLOR }; + xColor _outerStartColor { DEFAULT_OVERLAY_COLOR }; + xColor _outerEndColor { DEFAULT_OVERLAY_COLOR }; + float _innerStartAlpha { DEFAULT_ALPHA }; + float _innerEndAlpha { DEFAULT_ALPHA }; + float _outerStartAlpha { DEFAULT_ALPHA }; + float _outerEndAlpha { DEFAULT_ALPHA }; bool _hasTickMarks { false }; float _majorTickMarksAngle { 0 }; float _minorTickMarksAngle { 0 }; float _majorTickMarksLength { 0 }; float _minorTickMarksLength { 0 }; - xColor _majorTickMarksColor; - xColor _minorTickMarksColor; + xColor _majorTickMarksColor { DEFAULT_OVERLAY_COLOR }; + xColor _minorTickMarksColor { DEFAULT_OVERLAY_COLOR }; gpu::Primitive _solidPrimitive { gpu::TRIANGLE_FAN }; int _quadVerticesID { 0 }; int _lineVerticesID { 0 }; diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index de644f165b..16b771567c 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -266,7 +266,7 @@ void ContextOverlayInterface::contextOverlays_hoverLeaveEntity(const EntityItemI } } -static const QString INSPECTION_CERTIFICATE_QML_PATH = qApp->applicationDirPath() + "../../../qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; +static const QString INSPECTION_CERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; void ContextOverlayInterface::openInspectionCertificate() { // lets open the tablet to the inspection certificate QML if (!_currentEntityWithContextOverlay.isNull() && _entityMarketplaceID.length() > 0) { @@ -289,7 +289,7 @@ void ContextOverlayInterface::openInspectionCertificate() { QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/transfer"); QJsonObject request; request["certificate_id"] = entityProperties.getCertificateID(); @@ -359,7 +359,7 @@ void ContextOverlayInterface::openInspectionCertificate() { } } -static const QString MARKETPLACE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL.toString() + "/marketplace/items/"; +static const QString MARKETPLACE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace/items/"; void ContextOverlayInterface::openMarketplace() { // lets open the tablet and go to the current item in diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index c0f57f1979..e34e68e053 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -134,6 +134,56 @@ void Cube3DOverlay::setProperties(const QVariantMap& properties) { } } +/**jsdoc + * These are the properties of a cube {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.CubeProperties + * + * @property {string} type=cube - Has the value "cube". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Vec3} dimensions - The dimensions of the overlay. Synonyms: scale, size. + * + * @property {number} borderSize - Not used. + */ QVariant Cube3DOverlay::getProperty(const QString& property) { if (property == "borderSize") { return _borderSize; diff --git a/interface/src/ui/overlays/Grid3DOverlay.cpp b/interface/src/ui/overlays/Grid3DOverlay.cpp index c0e4c1593b..e06fa22692 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.cpp +++ b/interface/src/ui/overlays/Grid3DOverlay.cpp @@ -112,6 +112,61 @@ void Grid3DOverlay::setProperties(const QVariantMap& properties) { updateGrid(); } +/**jsdoc + * These are the properties of a grid {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.GridProperties + * + * @property {string} type=grid - Has the value "grid". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Vec2} dimensions=1,1 - The dimensions of the overlay. Synonyms: scale, size. + * + * @property {boolean} followCamera=true - If true, the grid is always visible even as the camera moves to another + * position. + * @property {number} majorGridEvery=5 - Integer number of minorGridEvery intervals at which to draw a thick grid + * line. Minimum value = 1. + * @property {number} minorGridEvery=1 - Real number of meters at which to draw thin grid lines. Minimum value = + * 0.001. + */ QVariant Grid3DOverlay::getProperty(const QString& property) { if (property == "followCamera") { return _followCamera; diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index a8ae293c83..454945d31c 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -188,6 +188,62 @@ void Image3DOverlay::setProperties(const QVariantMap& properties) { } } +/**jsdoc + * These are the properties of an image3d {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.Image3DProperties + * + * @property {string} type=image3d - Has the value "image3d". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Vec2} dimensions=1,1 - The dimensions of the overlay. Synonyms: scale, size. + * + * @property {boolean} isFacingAvatar - If true, the overlay is rotated to face the user's camera about an axis + * parallel to the user's avatar's "up" direction. + * + * @property {string} url - The URL of the PNG or JPG image to display. + * @property {Rect} subImage - The portion of the image to display. Defaults to the full image. + * @property {boolean} emissive - If true, the overlay is displayed at full brightness, otherwise it is rendered + * with scene lighting. + */ QVariant Image3DOverlay::getProperty(const QString& property) { if (property == "url") { return _url; diff --git a/interface/src/ui/overlays/ImageOverlay.cpp b/interface/src/ui/overlays/ImageOverlay.cpp index 4077fb0ab5..19f32511f6 100644 --- a/interface/src/ui/overlays/ImageOverlay.cpp +++ b/interface/src/ui/overlays/ImageOverlay.cpp @@ -19,6 +19,29 @@ QString const ImageOverlay::TYPE = "image"; QUrl const ImageOverlay::URL(QString("hifi/overlays/ImageOverlay.qml")); +// ImageOverlay's properties are defined in the QML file specified above. +/**jsdoc + * These are the properties of an image {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.ImageProperties + * + * @property {Rect} bounds - The position and size of the image display area, in pixels. Write-only. + * @property {number} x - Integer left, x-coordinate value of the image display area = bounds.x. + * Write-only. + * @property {number} y - Integer top, y-coordinate value of the image display area = bounds.y. + * Write-only. + * @property {number} width - Integer width of the image display area = bounds.width. Write-only. + * @property {number} height - Integer height of the image display area = bounds.height. Write-only. + * @property {string} imageURL - The URL of the image file to display. The image is scaled to fit to the bounds. + * Write-only. + * @property {Vec2} subImage=0,0 - Integer coordinates of the top left pixel to start using image content from. + * Write-only. + * @property {Color} color=0,0,0 - The color to apply over the top of the image to colorize it. Write-only. + * @property {number} alpha=0.0 - The opacity of the color applied over the top of the image, 0.0 - + * 1.0. Write-only. + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * Write-only. + */ + ImageOverlay::ImageOverlay() : QmlOverlay(URL) { } diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 070e3930fe..62675bacea 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -255,6 +255,67 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { } } +/**jsdoc + * These are the properties of a line3d {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.Line3DProperties + * + * @property {string} type=line3d - Has the value "line3d". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Uuid} endParentID=null - The avatar, entity, or overlay that the end point of the line is parented to. + * @property {number} endParentJointIndex=65535 - Integer value specifying the skeleton joint that the end point of the line is + * attached to if parentID is an avatar skeleton. A value of 65535 means "no joint". + * @property {Vec3} start - The start point of the line. Synonyms: startPoint, p1, and + * position. + * @property {Vec3} end - The end point of the line. Synonyms: endPoint and p2. + * @property {Vec3} localStart - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as start. Synonym: localPosition. + * @property {Vec3} localEnd - The local position of the overlay relative to its parent if the overlay has a + * endParentID set, otherwise the same value as end. + * @property {number} length - The length of the line, in meters. This can be set after creating a line with start and end + * points. + * @property {number} glow=0 - If glow > 0, the line is rendered with a glow. + * @property {number} lineWidth=0.02 - If glow > 0, this is the width of the glow, in meters. + */ QVariant Line3DOverlay::getProperty(const QString& property) { if (property == "start" || property == "startPoint" || property == "p1") { return vec3toVariant(getStart()); diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 0846599728..ba8bf8cbef 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -152,7 +152,7 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { _scaleToFit = true; setDimensions(vec3FromVariant(dimensions)); } else if (scale.isValid()) { - // if "scale" property is set but "dimentions" is not. + // if "scale" property is set but "dimensions" is not. // do NOT scale to fit. _scaleToFit = false; } @@ -181,6 +181,10 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { Q_ARG(const QVariantMap&, textureMap)); } + // jointNames is read-only. + // jointPositions is read-only. + // jointOrientations is read-only. + // relative auto jointTranslationsValue = properties["jointTranslations"]; if (jointTranslationsValue.canConvert(QVariant::List)) { @@ -276,6 +280,75 @@ vectorType ModelOverlay::mapJoints(mapFunction function) const { return result; } +// Note: ModelOverlay overrides Volume3DOverlay's "dimensions" and "scale" properties. +/**jsdoc + * These are the properties of a model {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.ModelProperties + * + * @property {string} type=sphere - Has the value "model". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {string} url - The URL of the FBX or OBJ model used for the overlay. + * @property {Vec3} dimensions - The dimensions of the overlay. Synonym: size. + * @property {Vec3} scale - The scale factor applied to the model's dimensions. + * @property {object.} textures - Maps the named textures in the model to the JPG or PNG images in the urls. + * @property {Array.} jointNames - The names of the joints - if any - in the model. Read-only + * @property {Array.} jointRotations - The relative rotations of the model's joints. + * @property {Array.} jointTranslations - The relative translations of the model's joints. + * @property {Array.} jointOrientations - The absolute orientations of the model's joints, in world coordinates. + * Read-only + * @property {Array.} jointPositions - The absolute positions of the model's joints, in world coordinates. + * Read-only + * @property {string} animationSettings.url="" - The URL of an FBX file containing an animation to play. + * @property {number} animationSettings.fps=0 - The frame rate (frames/sec) to play the animation at. + * @property {number} animationSettings.firstFrame=0 - The frame to start playing at. + * @property {number} animationSettings.lastFrame=0 - The frame to finish playing at. + * @property {boolean} animationSettings.running=false - Whether or not the animation is playing. + * @property {boolean} animationSettings.loop=false - Whether or not the animation should repeat in a loop. + * @property {boolean} animationSettings.hold=false - Whether or not when the animation finishes, the rotations and + * translations of the last frame played should be maintained. + * @property {boolean} animationSettings.allowTranslation=false - Whether or not translations contained in the animation should + * be played. + */ QVariant ModelOverlay::getProperty(const QString& property) { if (property == "url") { return _url.toString(); diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 01ad56f20e..5cf9fc7c8b 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -15,8 +15,8 @@ #include "Application.h" -static const xColor DEFAULT_OVERLAY_COLOR = { 255, 255, 255 }; -static const float DEFAULT_ALPHA = 0.7f; +const xColor Overlay::DEFAULT_OVERLAY_COLOR = { 255, 255, 255 }; +const float Overlay::DEFAULT_ALPHA = 0.7f; Overlay::Overlay() : _renderItemID(render::Item::INVALID_ITEM_ID), @@ -101,6 +101,27 @@ void Overlay::setProperties(const QVariantMap& properties) { } } +// JSDoc for copying to @typedefs of overlay types that inherit Overlay. +/**jsdoc + * @property {string} type=TODO - Has the value "TODO". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + */ QVariant Overlay::getProperty(const QString& property) { if (property == "type") { return QVariant(getType()); diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index 806fc1aa14..9b07f24600 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -122,6 +122,9 @@ protected: unsigned int _stackOrder { 0 }; + static const xColor DEFAULT_OVERLAY_COLOR; + static const float DEFAULT_ALPHA; + private: OverlayID _overlayID; // only used for non-3d overlays }; diff --git a/interface/src/ui/overlays/Overlay2D.cpp b/interface/src/ui/overlays/Overlay2D.cpp index d5f88a84d7..71b74e9452 100644 --- a/interface/src/ui/overlays/Overlay2D.cpp +++ b/interface/src/ui/overlays/Overlay2D.cpp @@ -23,6 +23,15 @@ AABox Overlay2D::getBounds() const { glm::vec3(_bounds.width(), _bounds.height(), 0.01f)); } +// JSDoc for copying to @typedefs of overlay types that inherit Overlay2D. +// QmlOverlay-derived classes don't support getProperty(). +/**jsdoc + * @property {Rect} bounds - The position and size of the rectangle. Write-only. + * @property {number} x - Integer left, x-coordinate value = bounds.x. Write-only. + * @property {number} y - Integer top, y-coordinate value = bounds.y. Write-only. + * @property {number} width - Integer width of the rectangle = bounds.width. Write-only. + * @property {number} height - Integer height of the rectangle = bounds.height. Write-only. + */ void Overlay2D::setProperties(const QVariantMap& properties) { Overlay::setProperties(properties); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 34768c601e..052ef0b6d8 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -172,6 +172,63 @@ OverlayID Overlays::addOverlay(const QString& type, const QVariant& properties) Overlay::Pointer thisOverlay = nullptr; + /**jsdoc + *

An overlay may be one of the following types:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Value2D/3DDescription
circle3d3DA circle.
cube3DA cube. Can also use a shape overlay to create a + * cube.
grid3DA grid of lines in a plane.
image2DAn image. Synonym: billboard.
image3d3DAn image.
line3d3DA line.
model3DA model.
rectangle2DA rectangle.
rectangle3d3DA rectangle.
shape3DA geometric shape, such as a cube, sphere, or cylinder.
sphere3DA sphere. Can also use a shape overlay to create a + * sphere.
text2DText.
text3d3DText.
web3d3DWeb content.
+ *

2D overlays are rendered on the display surface in desktop mode and on the HUD surface in HMD mode. 3D overlays are + * rendered at a position and orientation in-world.

+ *

Each overlay type has different {@link Overlays.OverlayProperties|OverlayProperties}.

+ * @typedef {string} Overlays.OverlayType + */ + + /**jsdoc + *

Different overlay types have different properties:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
{@link Overlays.OverlayType|OverlayType}Overlay Properties
circle3d{@link Overlays.Circle3DProperties|Circle3DProperties}
cube{@link Overlays.CubeProperties|CubeProperties}
grid{@link Overlays.GridProperties|GridProperties}
image{@link Overlays.ImageProperties|ImageProperties}
image3d{@link Overlays.Image3DProperties|Image3DProperties}
line3d{@link Overlays.Line3DProperties|Line3DProperties}
model{@link Overlays.ModelProperties|ModelProperties}
rectangle{@link Overlays.RectangleProperties|RectangleProperties}
rectangle3d{@link Overlays.Rectangle3DProperties|Rectangle3DProperties}
shape{@link Overlays.ShapeProperties|ShapeProperties}
sphere{@link Overlays.SphereProperties|SphereProperties}
text{@link Overlays.TextProperties|TextProperties}
text3d{@link Overlays.Text3DProperties|Text3DProperties}
web3d{@link Overlays.Web3DProperties|Web3DProperties}
+ * @typedef {object} Overlays.OverlayProperties + */ + if (type == ImageOverlay::TYPE) { thisOverlay = Overlay::Pointer(new ImageOverlay(), [](Overlay* ptr) { ptr->deleteLater(); }); } else if (type == Image3DOverlay::TYPE || type == "billboard") { // "billboard" for backwards compatibility diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index e1996e6bfc..abd657074c 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -45,12 +45,13 @@ void OverlayPropertyResultFromScriptValue(const QScriptValue& object, OverlayPro const OverlayID UNKNOWN_OVERLAY_ID = OverlayID(); /**jsdoc - * @typedef Overlays.RayToOverlayIntersectionResult - * @property {bool} intersects True if the PickRay intersected with a 3D overlay. - * @property {Overlays.OverlayID} overlayID The ID of the overlay that was intersected with. - * @property {float} distance The distance from the PickRay origin to the intersection point. - * @property {Vec3} surfaceNormal The normal of the surface that was intersected with. - * @property {Vec3} intersection The point at which the PickRay intersected with the overlay. + * @typedef {object} Overlays.RayToOverlayIntersectionResult + * @property {boolean} intersects - true if the {@link PickRay} intersected with a 3D overlay, otherwise + * false. + * @property {Uuid} overlayID - The UUID of the overlay that was intersected. + * @property {number} distance - The distance from the {@link PickRay} origin to the intersection point. + * @property {Vec3} surfaceNormal - The normal of the overlay surface at the intersection point. + * @property {Vec3} intersection - The position of the intersection point. */ class RayToOverlayIntersectionResult { public: @@ -70,13 +71,11 @@ QScriptValue RayToOverlayIntersectionResultToScriptValue(QScriptEngine* engine, void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& object, RayToOverlayIntersectionResult& value); /**jsdoc - * @typedef {int} Overlays.OverlayID - */ - -/**jsdoc - * - * Overlays namespace... + * The Overlays API provides facilities to create and interact with overlays. Overlays are 2D and 3D objects visible only to + * yourself and that aren't persisted to the domain. They are used for UI. * @namespace Overlays + * @property {Uuid} keyboardFocusOverlay - Get or set the {@link Overlays.OverlayType|web3d} overlay that has keyboard focus. + * If no overlay is set, get returns null; set to null to clear keyboard focus. */ class Overlays : public QObject { @@ -111,100 +110,246 @@ public: public slots: /**jsdoc - * Add an overlays to the scene. The properties specified will depend - * on the type of overlay that is being created. - * + * Add an overlay to the scene. * @function Overlays.addOverlay - * @param {string} type The type of the overlay to add. - * @param {Overlays.OverlayProperties} The properties of the overlay that you want to add. - * @return {Overlays.OverlayID} The ID of the newly created overlay. + * @param {Overlays.OverlayType} type - The type of the overlay to add. + * @param {Overlays.OverlayProperties} properties - The properties of the overlay to add. + * @returns {Uuid} The ID of the newly created overlay. + * @example Add a cube overlay in front of your avatar. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); */ OverlayID addOverlay(const QString& type, const QVariant& properties); /**jsdoc * Create a clone of an existing overlay. - * * @function Overlays.cloneOverlay - * @param {Overlays.OverlayID} overlayID The ID of the overlay to clone. - * @return {Overlays.OverlayID} The ID of the new overlay. + * @param {Uuid} overlayID - The ID of the overlay to clone. + * @returns {Uuid} The ID of the new overlay. + * @example Add an overlay in front of your avatar, clone it, and move the clone to be above the + * original. + * var position = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })); + * var original = Overlays.addOverlay("cube", { + * position: position, + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * + * var clone = Overlays.cloneOverlay(original); + * Overlays.editOverlay(clone, { + * position: Vec3.sum({ x: 0, y: 0.5, z: 0}, position) + * }); */ OverlayID cloneOverlay(OverlayID id); /**jsdoc * Edit an overlay's properties. - * * @function Overlays.editOverlay - * @param {Overlays.OverlayID} overlayID The ID of the overlay to edit. - * @return {bool} `true` if the overlay was found and edited, otherwise false. + * @param {Uuid} overlayID - The ID of the overlay to edit. + * @param {Overlays.OverlayProperties} properties - The properties changes to make. + * @returns {boolean} true if the overlay was found and edited, otherwise false. + * @example Add an overlay in front of your avatar then change its color. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * + * var success = Overlays.editOverlay(overlay, { + * color: { red: 255, green: 0, blue: 0 } + * }); + * print("Success: " + success); */ bool editOverlay(OverlayID id, const QVariant& properties); - /// edits an overlay updating only the included properties, will return the identified OverlayID in case of - /// successful edit, if the input id is for an unknown overlay this function will have no effect + /**jsdoc + * Edit multiple overlays' properties. + * @function Overlays.editOverlays + * @param propertiesById {object.} - An object with overlay IDs as keys and + * {@link Overlays.OverlayProperties|OverlayProperties} edits to make as values. + * @returns {boolean} true if all overlays were found and edited, otherwise false (some may have + * been found and edited). + * @example Create two overlays in front of your avatar then change their colors. + * var overlayA = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: -0.3, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * var overlayB = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0.3, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * + * var overlayEdits = {}; + * overlayEdits[overlayA] = { color: { red: 255, green: 0, blue: 0 } }; + * overlayEdits[overlayB] = { color: { red: 0, green: 255, blue: 0 } }; + * var success = Overlays.editOverlays(overlayEdits); + * print("Success: " + success); + */ bool editOverlays(const QVariant& propertiesById); /**jsdoc * Delete an overlay. - * * @function Overlays.deleteOverlay - * @param {Overlays.OverlayID} overlayID The ID of the overlay to delete. + * @param {Uuid} overlayID - The ID of the overlay to delete. + * @example Create an overlay in front of your avatar then delete it. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * print("Overlay: " + overlay); + * Overlays.deleteOverlay(overlay); */ void deleteOverlay(OverlayID id); /**jsdoc * Get the type of an overlay. - * * @function Overlays.getOverlayType - * @param {Overlays.OverlayID} overlayID The ID of the overlay to get the type of. - * @return {string} The type of the overlay if found, otherwise the empty string. + * @param {Uuid} overlayID - The ID of the overlay to get the type of. + * @returns {Overlays.OverlayType} The type of the overlay if found, otherwise an empty string. + * @example Create an overlay in front of your avatar then get and report its type. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * var type = Overlays.getOverlayType(overlay); + * print("Type: " + type); */ QString getOverlayType(OverlayID overlayId); /**jsdoc - * Get the overlay Script object. - * - * @function Overlays.getOverlayObject - * @param {Overlays.OverlayID} overlayID The ID of the overlay to get the script object of. - * @return {Object} The script object for the overlay if found. - */ + * Get the overlay script object. + * @function Overlays.getOverlayObject + * @param {Uuid} overlayID - The ID of the overlay to get the script object of. + * @returns {object} The script object for the overlay if found. + */ QObject* getOverlayObject(OverlayID id); /**jsdoc - * Get the ID of the overlay at a particular point on the HUD/screen. - * + * Get the ID of the 2D overlay at a particular point on the screen or HUD. * @function Overlays.getOverlayAtPoint - * @param {Vec2} point The point to check for an overlay. - * @return {Overlays.OverlayID} The ID of the overlay at the point specified. - * If no overlay is found, `0` will be returned. + * @param {Vec2} point - The point to check for an overlay. + * @returns {Uuid} The ID of the 2D overlay at the specified point if found, otherwise null. + * @example Create a 2D overlay and add an event function that reports the overlay clicked on, if any. + * var overlay = Overlays.addOverlay("rectangle", { + * bounds: { x: 100, y: 100, width: 200, height: 100 }, + * color: { red: 255, green: 255, blue: 255 } + * }); + * print("Created: " + overlay); + * + * Controller.mousePressEvent.connect(function (event) { + * var overlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + * print("Clicked: " + overlay); + * }); */ OverlayID getOverlayAtPoint(const glm::vec2& point); /**jsdoc - * Get the value of a an overlay's property. - * + * Get the value of a 3D overlay's property. * @function Overlays.getProperty - * @param {Overlays.OverlayID} The ID of the overlay to get the property of. - * @param {string} The name of the property to get the value of. - * @return {Object} The value of the property. If the overlay or the property could - * not be found, `undefined` will be returned. + * @param {Uuid} overlayID - The ID of the overlay. Must be for a 3D {@link Overlays.OverlayType|OverlayType}. + * @param {string} property - The name of the property value to get. + * @returns {object} The value of the property if the 3D overlay and property can be found, otherwise + * undefined. + * @example Create an overlay in front of your avatar then report its alpha property value. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * var alpha = Overlays.getProperty(overlay, "alpha"); + * print("Overlay alpha: " + alpha); */ OverlayPropertyResult getProperty(OverlayID id, const QString& property); + /**jsdoc + * Get the values of an overlay's properties. + * @function Overlays.getProperties + * @param {Uuid} overlayID - The ID of the overlay. + * @param {Array.} properties - An array of names of properties to get the values of. + * @returns {Overlays.OverlayProperties} The values of valid properties if the overlay can be found, otherwise + * undefined. + * @example Create an overlay in front of your avatar then report some of its properties. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * var properties = Overlays.getProperties(overlay, ["color", "alpha", "grabbable"]); + * print("Overlay properties: " + JSON.stringify(properties)); + */ OverlayPropertyResult getProperties(const OverlayID& id, const QStringList& properties); + /**jsdoc + * Get the values of multiple overlays' properties. + * @function Overlays.getOverlaysProperties + * @param propertiesById {object.>} - An object with overlay IDs as keys and arrays of the names of + * properties to get for each as values. + * @returns {object.} An object with overlay IDs as keys and + * {@link Overlays.OverlayProperties|OverlayProperties} as values. + * @example Create two cube overlays in front of your avatar then get some of their properties. + * var overlayA = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: -0.3, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * var overlayB = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0.3, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * var propertiesToGet = {}; + * propertiesToGet[overlayA] = ["color", "alpha"]; + * propertiesToGet[overlayB] = ["dimensions"]; + * var properties = Overlays.getOverlaysProperties(propertiesToGet); + * print("Overlays properties: " + JSON.stringify(properties)); + */ OverlayPropertyResult getOverlaysProperties(const QVariant& overlaysProperties); - /*jsdoc - * Find the closest 3D overlay hit by a pick ray. - * + /**jsdoc + * Find the closest 3D overlay intersected by a {@link PickRay}. * @function Overlays.findRayIntersection - * @param {PickRay} The PickRay to use for finding overlays. - * @param {bool} Unused; Exists to match Entity interface - * @param {List of Overlays.OverlayID} Whitelist for intersection test. - * @param {List of Overlays.OverlayID} Blacklist for intersection test. - * @param {bool} Unused; Exists to match Entity interface - * @param {bool} Unused; Exists to match Entity interface - * @return {Overlays.RayToOverlayIntersectionResult} The result of the ray cast. + * @param {PickRay} pickRay - The PickRay to use for finding overlays. + * @param {boolean} [precisionPicking=false] - Unused; exists to match Entity API. + * @param {Array.} [overlayIDsToInclude=[]] - Whitelist for intersection test. If empty then the result isn't limited + * to overlays in the list. + * @param {Array.} [overlayIDsToExclude=[]] - Blacklist for intersection test. If empty then the result doesn't + * exclude overlays in the list. + * @param {boolean} [visibleOnly=false] - Unused; exists to match Entity API. + * @param {boolean} [collidableOnly=false] - Unused; exists to match Entity API. + * @returns {Overlays.RayToOverlayIntersectionResult} The closest 3D overlay intersected by pickRay, taking + * into account overlayIDsToInclude and overlayIDsToExclude if they're not empty. + * @example Create a cube overlay in front of your avatar. Report 3D overlay intersection details for mouse + * clicks. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * + * Controller.mousePressEvent.connect(function (event) { + * var pickRay = Camera.computePickRay(event.x, event.y); + * var intersection = Overlays.findRayIntersection(pickRay); + * print("Intersection: " + JSON.stringify(intersection)); + * }); */ RayToOverlayIntersectionResult findRayIntersection(const PickRay& ray, bool precisionPicking = false, @@ -213,61 +358,113 @@ public slots: bool visibleOnly = false, bool collidableOnly = false); - // Same as above but with QVectors + // TODO: Apart from the name, this function signature on JavaScript is identical to that of findRayIntersection() and should + // probably be removed from the JavaScript API so as not to cause confusion. + /**jsdoc + * Find the closest 3D overlay intersected by a {@link PickRay}. + * @function Overlays.findRayIntersectionVector + * @deprecated Use {@link Overlays.findRayIntersection} instead; it has identical parameters and results. + * @param {PickRay} pickRay - The PickRay to use for finding overlays. + * @param {boolean} [precisionPicking=false] - Unused; exists to match Entity API. + * @param {Array.} [overlayIDsToInclude=[]] - Whitelist for intersection test. If empty then the result isn't limited + * to overlays in the list. + * @param {Array.} [overlayIDsToExclude=[]] - Blacklist for intersection test. If empty then the result doesn't + * exclude overlays in the list. + * @param {boolean} [visibleOnly=false] - Unused; exists to match Entity API. + * @param {boolean} [collidableOnly=false] - Unused; exists to match Entity API. + * @returns {Overlays.RayToOverlayIntersectionResult} The closest 3D overlay intersected by pickRay, taking + * into account overlayIDsToInclude and overlayIDsToExclude if they're not empty. + */ RayToOverlayIntersectionResult findRayIntersectionVector(const PickRay& ray, bool precisionPicking, const QVector& overlaysToInclude, const QVector& overlaysToDiscard, bool visibleOnly = false, bool collidableOnly = false); /**jsdoc - * Return a list of 3d overlays with bounding boxes that touch the given sphere - * + * Return a list of 3D overlays with bounding boxes that touch a search sphere. * @function Overlays.findOverlays - * @param {Vec3} center the point to search from. - * @param {float} radius search radius - * @return {Overlays.OverlayID[]} list of overlays withing the radius + * @param {Vec3} center - The center of the search sphere. + * @param {number} radius - The radius of the search sphere. + * @returns {Uuid[]} An array of overlay IDs with bounding boxes that touch a search sphere. + * @example Create two cube overlays in front of your avatar then search for overlays near your avatar. + * var overlayA = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: -0.3, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * print("Overlay A: " + overlayA); + * var overlayB = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0.3, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * print("Overlay B: " + overlayB); + * + * var overlaysFound = Overlays.findOverlays(MyAvatar.position, 5.0); + * print("Overlays found: " + JSON.stringify(overlaysFound)); */ QVector findOverlays(const glm::vec3& center, float radius); /**jsdoc - * Check whether an overlay's assets have been loaded. For example, if the - * overlay is an "image" overlay, this will indicate whether the its image - * has loaded. + * Check whether an overlay's assets have been loaded. For example, for an image overlay the result indicates + * whether its image has been loaded. * @function Overlays.isLoaded - * @param {Overlays.OverlayID} The ID of the overlay to check. - * @return {bool} `true` if the overlay's assets have been loaded, otherwise `false`. + * @param {Uuid} overlayID - The ID of the overlay to check. + * @returns {boolean} true if the overlay's assets have been loaded, otherwise false. + * @example Create an image overlay and report whether its image is loaded after 1s. + * var overlay = Overlays.addOverlay("image", { + * bounds: { x: 100, y: 100, width: 200, height: 200 }, + * imageURL: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png" + * }); + * Script.setTimeout(function () { + * var isLoaded = Overlays.isLoaded(overlay); + * print("Image loaded: " + isLoaded); + * }, 1000); */ bool isLoaded(OverlayID id); /**jsdoc * Calculates the size of the given text in the specified overlay if it is a text overlay. - * If it is a 2D text overlay, the size will be in pixels. - * If it is a 3D text overlay, the size will be in meters. - * * @function Overlays.textSize - * @param {Overlays.OverlayID} The ID of the overlay to measure. - * @param {string} The string to measure. - * @return {Vec2} The size of the text. + * @param {Uuid} overlayID - The ID of the overlay to use for calculation. + * @param {string} text - The string to calculate the size of. + * @returns {Size} The size of the text if the overlay is a text overlay, otherwise + * { height: 0, width : 0 }. If the overlay is a 2D overlay, the size is in pixels; if the overlay is a 3D + * overlay, the size is in meters. + * @example Calculate the size of "hello" in a 3D text overlay. + * var overlay = Overlays.addOverlay("text3d", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -2 })), + * rotation: MyAvatar.orientation, + * text: "hello", + * lineHeight: 0.2 + * }); + * var textSize = Overlays.textSize(overlay, "hello"); + * print("Size of \"hello\": " + JSON.stringify(textSize)); */ QSizeF textSize(OverlayID id, const QString& text); /**jsdoc - * Get the width of the virtual 2D HUD. - * + * Get the width of the window or HUD. * @function Overlays.width - * @return {float} The width of the 2D HUD. + * @returns {number} The width, in pixels, of the Interface window if in desktop mode or the HUD if in HMD mode. */ float width(); /**jsdoc - * Get the height of the virtual 2D HUD. - * + * Get the height of the window or HUD. * @function Overlays.height - * @return {float} The height of the 2D HUD. + * @returns {number} The height, in pixels, of the Interface window if in desktop mode or the HUD if in HMD mode. */ float height(); - /// return true if there is an overlay with that id else false + /**jsdoc + * Check if there is an overlay of a given ID. + * @function Overlays.isAddedOverly + * @param {Uuid} overlayID - The ID to check. + * @returns {boolean} true if an overlay with the given ID exists, false otherwise. + */ bool isAddedOverlay(OverlayID id); #if OVERLAY_PANELS @@ -294,36 +491,237 @@ public slots: #endif + /**jsdoc + * Generate a mouse press event on an overlay. + * @function Overlays.sendMousePressOnOverlay + * @param {Uuid} overlayID - The ID of the overlay to generate a mouse press event on. + * @param {PointerEvent} event - The mouse press event details. + * @example Create a 2D rectangle overlay plus a 3D cube overlay and generate mousePressOnOverlay events for the 2D + * overlay. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * print("3D overlay: " + overlay); + * + * var overlay = Overlays.addOverlay("rectangle", { + * bounds: { x: 100, y: 100, width: 200, height: 100 }, + * color: { red: 255, green: 255, blue: 255 } + * }); + * print("2D overlay: " + overlay); + * + * // Overlays.mousePressOnOverlay by default applies only to 3D overlays. + * Overlays.mousePressOnOverlay.connect(function(overlayID, event) { + * print("Clicked: " + overlayID); + * }); + * + * Controller.mousePressEvent.connect(function (event) { + * // Overlays.getOverlayAtPoint applies only to 2D overlays. + * var overlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + * if (overlay) { + * Overlays.sendMousePressOnOverlay(overlay, { + * type: "press", + * id: 0, + * pos2D: event + * }); + * } + * }); + */ void sendMousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + + /**jsdoc + * Generate a mouse release event on an overlay. + * @function Overlays.sendMouseReleaseOnOverlay + * @param {Uuid} overlayID - The ID of the overlay to generate a mouse release event on. + * @param {PointerEvent} event - The mouse release event details. + */ void sendMouseReleaseOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + + /**jsdoc + * Generate a mouse move event on an overlay. + * @function Overlays.sendMouseMoveOnOverlay + * @param {Uuid} overlayID - The ID of the overlay to generate a mouse move event on. + * @param {PointerEvent} event - The mouse move event details. + */ void sendMouseMoveOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + /**jsdoc + * Generate a hover enter event on an overlay. + * @function Overlays.sendHoverEnterOverlay + * @param {Uuid} id - The ID of the overlay to generate a hover enter event on. + * @param {PointerEvent} event - The hover enter event details. + */ void sendHoverEnterOverlay(const OverlayID& overlayID, const PointerEvent& event); + + /**jsdoc + * Generate a hover over event on an overlay. + * @function Overlays.sendHoverOverOverlay + * @param {Uuid} overlayID - The ID of the overlay to generate a hover over event on. + * @param {PointerEvent} event - The hover over event details. + */ void sendHoverOverOverlay(const OverlayID& overlayID, const PointerEvent& event); + + /**jsdoc + * Generate a hover leave event on an overlay. + * @function Overlays.sendHoverLeaveOverlay + * @param {Uuid} overlayID - The ID of the overlay to generate a hover leave event on. + * @param {PointerEvent} event - The hover leave event details. + */ void sendHoverLeaveOverlay(const OverlayID& overlayID, const PointerEvent& event); + /**jsdoc + * Get the ID of the Web3D overlay that has keyboard focus. + * @function Overlays.getKeyboardFocusOverlay + * @returns {Uuid} The ID of the {@link Overlays.OverlayType|web3d} overlay that has focus, if any, otherwise + * null. + */ OverlayID getKeyboardFocusOverlay(); + + /**jsdoc + * Set the Web3D overlay that has keyboard focus. + * @function Overlays.setKeyboardFocusOverlay + * @param {Uuid} overlayID - The ID of the {@link Overlays.OverlayType|web3d} overlay to set keyboard focus to. Use + * {@link Uuid|Uuid.NULL} or null to unset keyboard focus from an overlay. + */ void setKeyboardFocusOverlay(const OverlayID& id); signals: /**jsdoc - * Emitted when an overlay is deleted - * + * Triggered when an overlay is deleted. * @function Overlays.overlayDeleted - * @param {OverlayID} The ID of the overlay that was deleted. + * @param {Uuid} overlayID - The ID of the overlay that was deleted. + * @returns {Signal} + * @example Create an overlay then delete it after 1s. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * print("Overlay: " + overlay); + * + * Overlays.overlayDeleted.connect(function(overlayID) { + * print("Deleted: " + overlayID); + * }); + * Script.setTimeout(function () { + * Overlays.deleteOverlay(overlay); + * }, 1000); */ void overlayDeleted(OverlayID id); - void panelDeleted(OverlayID id); +#if OVERLAY_PANELS + void panelDeleted(OverlayID id); +#endif + + /**jsdoc + * Triggered when a mouse press event occurs on an overlay. Only occurs for 3D overlays (unless you use + * {@link Overlays.sendMousePressOnOverlay|sendMousePressOnOverlay} for a 2D overlay). + * @function Overlays.mousePressOnOverlay + * @param {Uuid} overlayID - The ID of the overlay the mouse press event occurred on. + * @param {PointerEvent} event - The mouse press event details. + * @returns {Signal} + * @example Create a cube overlay in front of your avatar and report mouse clicks on it. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * print("My overlay: " + overlay); + * + * Overlays.mousePressOnOverlay.connect(function(overlayID, event) { + * if (overlayID === overlay) { + * print("Clicked on my overlay"); + * } + * }); + */ void mousePressOnOverlay(OverlayID overlayID, const PointerEvent& event); + + /**jsdoc + * Triggered when a mouse double press event occurs on an overlay. Only occurs for 3D overlays. + * @function Overlays.mouseDoublePressOnOverlay + * @param {Uuid} overlayID - The ID of the overlay the mouse double press event occurred on. + * @param {PointerEvent} event - The mouse double press event details. + * @returns {Signal} + */ void mouseDoublePressOnOverlay(OverlayID overlayID, const PointerEvent& event); + + /**jsdoc + * Triggered when a mouse release event occurs on an overlay. Only occurs for 3D overlays (unless you use + * {@link Overlays.sendMouseReleaseOnOverlay|sendMouseReleaseOnOverlay} for a 2D overlay). + * @function Overlays.mouseReleaseOnOverlay + * @param {Uuid} overlayID - The ID of the overlay the mouse release event occurred on. + * @param {PointerEvent} event - The mouse release event details. + * @returns {Signal} + */ void mouseReleaseOnOverlay(OverlayID overlayID, const PointerEvent& event); + + /**jsdoc + * Triggered when a mouse move event occurs on an overlay. Only occurs for 3D overlays (unless you use + * {@link Overlays.sendMouseMoveOnOverlay|sendMouseMoveOnOverlay} for a 2D overlay). + * @function Overlays.mouseMoveOnOverlay + * @param {Uuid} overlayID - The ID of the overlay the mouse moved event occurred on. + * @param {PointerEvent} event - The mouse move event details. + * @returns {Signal} + */ void mouseMoveOnOverlay(OverlayID overlayID, const PointerEvent& event); + + /**jsdoc + * Triggered when a mouse press event occurs on something other than a 3D overlay. + * @function Overlays.mousePressOffOverlay + * @returns {Signal} + */ void mousePressOffOverlay(); + + /**jsdoc + * Triggered when a mouse double press event occurs on something other than a 3D overlay. + * @function Overlays.mouseDoublePressOffOverlay + * @returns {Signal} + */ void mouseDoublePressOffOverlay(); + /**jsdoc + * Triggered when a mouse cursor starts hovering over an overlay. Only occurs for 3D overlays (unless you use + * {@link Overlays.sendHoverEnterOverlay|sendHoverEnterOverlay} for a 2D overlay). + * @function Overlays.hoverEnterOverlay + * @param {Uuid} overlayID - The ID of the overlay the mouse moved event occurred on. + * @param {PointerEvent} event - The mouse move event details. + * @returns {Signal} + * @example Create a cube overlay in front of your avatar and report when you start hovering your mouse over + * it. + * var overlay = Overlays.addOverlay("cube", { + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -3 })), + * rotation: MyAvatar.orientation, + * dimensions: { x: 0.3, y: 0.3, z: 0.3 }, + * solid: true + * }); + * print("Overlay: " + overlay); + * Overlays.hoverEnterOverlay.connect(function(overlayID, event) { + * print("Hover enter: " + overlayID); + * }); + */ void hoverEnterOverlay(OverlayID overlayID, const PointerEvent& event); + + /**jsdoc + * Triggered when a mouse cursor continues hovering over an overlay. Only occurs for 3D overlays (unless you use + * {@link Overlays.sendHoverOverOverlay|sendHoverOverOverlay} for a 2D overlay). + * @function Overlays.hoverOverOverlay + * @param {Uuid} overlayID - The ID of the overlay the hover over event occurred on. + * @param {PointerEvent} event - The hover over event details. + * @returns {Signal} + */ void hoverOverOverlay(OverlayID overlayID, const PointerEvent& event); + + /**jsdoc + * Triggered when a mouse cursor finishes hovering over an overlay. Only occurs for 3D overlays (unless you use + * {@link Overlays.sendHoverLeaveOverlay|sendHoverLeaveOverlay} for a 2D overlay). + * @function Overlays.hoverLeaveOverlay + * @param {Uuid} overlayID - The ID of the overlay the hover leave event occurred on. + * @param {PointerEvent} event - The hover leave event details. + * @returns {Signal} + */ void hoverLeaveOverlay(OverlayID overlayID, const PointerEvent& event); private: diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index aae8893667..bcd32b2850 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -27,6 +27,8 @@ bool PanelAttachable::getParentVisible() const { #endif } +// JSDoc for copying to @typedefs of overlay types that inherit PanelAttachable. +// No JSDoc because these properties are not actually used. QVariant PanelAttachable::getProperty(const QString& property) { if (property == "offsetPosition") { return vec3toVariant(getOffsetPosition()); diff --git a/interface/src/ui/overlays/Planar3DOverlay.cpp b/interface/src/ui/overlays/Planar3DOverlay.cpp index e6b1359b68..f53d06a239 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.cpp +++ b/interface/src/ui/overlays/Planar3DOverlay.cpp @@ -58,6 +58,10 @@ void Planar3DOverlay::setProperties(const QVariantMap& properties) { } } +// JSDoc for copying to @typedefs of overlay types that inherit Planar3DOverlay. +/**jsdoc + * @property {Vec2} dimensions=1,1 - The dimensions of the overlay. Synonyms: scale, size. + */ QVariant Planar3DOverlay::getProperty(const QString& property) { if (property == "dimensions" || property == "scale" || property == "size") { return vec2toVariant(getDimensions()); diff --git a/interface/src/ui/overlays/Planar3DOverlay.h b/interface/src/ui/overlays/Planar3DOverlay.h index 1360cccfc2..0a0e75696e 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.h +++ b/interface/src/ui/overlays/Planar3DOverlay.h @@ -27,8 +27,8 @@ public: void setDimensions(float value) { setDimensions(glm::vec2(value)); } void setDimensions(const glm::vec2& value); - void setProperties(const QVariantMap& properties) override; - QVariant getProperty(const QString& property) override; + virtual void setProperties(const QVariantMap& properties) override; + virtual QVariant getProperty(const QString& property) override; virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; diff --git a/interface/src/ui/overlays/QmlOverlay.cpp b/interface/src/ui/overlays/QmlOverlay.cpp index 15e72cf1e3..f4a9034187 100644 --- a/interface/src/ui/overlays/QmlOverlay.cpp +++ b/interface/src/ui/overlays/QmlOverlay.cpp @@ -53,6 +53,7 @@ QmlOverlay::~QmlOverlay() { _qmlElement.reset(); } +// QmlOverlay replaces Overlay's properties with those defined in the QML file used but keeps Overlay2D's properties. void QmlOverlay::setProperties(const QVariantMap& properties) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setProperties", Q_ARG(QVariantMap, properties)); diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.cpp b/interface/src/ui/overlays/Rectangle3DOverlay.cpp index f2e324206d..425b7e8155 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.cpp +++ b/interface/src/ui/overlays/Rectangle3DOverlay.cpp @@ -107,6 +107,54 @@ const render::ShapeKey Rectangle3DOverlay::getShapeKey() { return builder.build(); } +/**jsdoc + * These are the properties of a rectangle3d {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.Rectangle3DProperties + * + * @property {string} type=rectangle3d - Has the value "rectangle3d". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Vec2} dimensions=1,1 - The dimensions of the overlay. Synonyms: scale, size. + */ void Rectangle3DOverlay::setProperties(const QVariantMap& properties) { Planar3DOverlay::setProperties(properties); } diff --git a/interface/src/ui/overlays/RectangleOverlay.cpp b/interface/src/ui/overlays/RectangleOverlay.cpp index 1487a4cb63..af37a4ac02 100644 --- a/interface/src/ui/overlays/RectangleOverlay.cpp +++ b/interface/src/ui/overlays/RectangleOverlay.cpp @@ -11,6 +11,29 @@ QString const RectangleOverlay::TYPE = "rectangle"; QUrl const RectangleOverlay::URL(QString("hifi/overlays/RectangleOverlay.qml")); +// RectangleOverlay's properties are defined in the QML file specified above. +/**jsdoc + * These are the properties of a rectangle {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.RectangleProperties + * + * @property {Rect} bounds - The position and size of the rectangle, in pixels. Write-only. + * @property {number} x - Integer left, x-coordinate value = bounds.x. Write-only. + * @property {number} y - Integer top, y-coordinate value = bounds.y. Write-only. + * @property {number} width - Integer width of the rectangle = bounds.width. Write-only. + * @property {number} height - Integer height of the rectangle = bounds.height. Write-only. + * + * @property {Color} color=0,0,0 - The color of the overlay. Write-only. + * @property {number} alpha=1.0 - The opacity of the overlay, 0.0 - 1.0. Write-only. + * @property {number} borderWidth=1 - Integer width of the border, in pixels. The border is drawn within the rectangle's bounds. + * It is not drawn unless either borderColor or borderAlpha are specified. Write-only. + * @property {number} radius=0 - Integer corner radius, in pixels. Write-only. + * @property {Color} borderColor=0,0,0 - The color of the border. Write-only. + * @property {number} borderAlpha=1.0 - The opacity of the border, 0.0 - 1.0. + * Write-only. + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * Write-only. + */ + RectangleOverlay::RectangleOverlay() : QmlOverlay(URL) {} RectangleOverlay::RectangleOverlay(const RectangleOverlay* rectangleOverlay) diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index 07949d5349..92431d7716 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -108,6 +108,57 @@ void Shape3DOverlay::setProperties(const QVariantMap& properties) { } } +/**jsdoc + * These are the properties of a shape {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.ShapeProperties + * + * @property {string} type=shape - Has the value "shape". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Vec3} dimensions - The dimensions of the overlay. Synonyms: scale, size. + * + * @property {Shape} shape=Hexagon - The geometrical shape of the overlay. + * @property {number} borderSize - Not used. + */ QVariant Shape3DOverlay::getProperty(const QString& property) { if (property == "borderSize") { return _borderSize; diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 2013e5689a..8cb7f7757b 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -26,6 +26,56 @@ Sphere3DOverlay::Sphere3DOverlay(const Sphere3DOverlay* Sphere3DOverlay) : { } +// If Sphere3DOverlay had a getProperty() method then it would go here; do JSDoc here. +/**jsdoc + * These are the properties of a sphere {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.SphereProperties + * + * @property {string} type=sphere - Has the value "sphere". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Vec3} dimensions - The dimensions of the overlay. Synonyms: scale, size. + */ + void Sphere3DOverlay::render(RenderArgs* args) { if (!_renderVisible) { return; // do nothing if we're not visible diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 1cbc73304b..a66b57c365 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -204,6 +204,68 @@ void Text3DOverlay::setProperties(const QVariantMap& properties) { } } +/**jsdoc + * These are the properties of a text3d {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.Text3DProperties + * + * @property {string} type=text3d - Has the value "text3d". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {Vec2} dimensions=1,1 - The dimensions of the overlay. Synonyms: scale, size. + * + * @property {boolean} isFacingAvatar - If true, the overlay is rotated to face the user's camera about an axis + * parallel to the user's avatar's "up" direction. + * + * @property {string} text="" - The text to display. Text does not automatically wrap; use \n for a line break. + * @property {number} textAlpha=1 - The text alpha value. + * @property {Color} backgroundColor=0,0,0 - The background color. + * @property {number} backgroundAlpha=0.7 - The background alpha value. + * @property {number} lineHeight=1 - The height of a line of text in meters. + * @property {number} leftMargin=0.1 - The left margin, in meters. + * @property {number} topMargin=0.1 - The top margin, in meters. + * @property {number} rightMargin=0.1 - The right margin, in meters. + * @property {number} bottomMargin=0.1 - The bottom margin, in meters. + */ + QVariant Text3DOverlay::getProperty(const QString& property) { if (property == "text") { return getText(); diff --git a/interface/src/ui/overlays/TextOverlay.cpp b/interface/src/ui/overlays/TextOverlay.cpp index 762d855a11..fba36f0c77 100644 --- a/interface/src/ui/overlays/TextOverlay.cpp +++ b/interface/src/ui/overlays/TextOverlay.cpp @@ -27,6 +27,33 @@ QString const TextOverlay::TYPE = "text"; QUrl const TextOverlay::URL(QString("hifi/overlays/TextOverlay.qml")); +// TextOverlay's properties are defined in the QML file specified above. +/**jsdoc + * These are the properties of a text {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.TextProperties + * + * @property {Rect} bounds - The position and size of the rectangle, in pixels. Write-only. + * @property {number} x - Integer left, x-coordinate value = bounds.x. Write-only. + * @property {number} y - Integer top, y-coordinate value = bounds.y. Write-only. + * @property {number} width - Integer width of the rectangle = bounds.width. Write-only. + * @property {number} height - Integer height of the rectangle = bounds.height. Write-only. + * + * @property {number} margin=0 - Sets the leftMargin and topMargin values, in pixels. + * Write-only. + * @property {number} leftMargin=0 - The left margin's size, in pixels. Write-only. + * @property {number} topMargin=0 - The top margin's size, in pixels. Write-only. + * @property {string} text="" - The text to display. Text does not automatically wrap; use \n for a line break. Text + * is clipped to the bounds. Write-only. + * @property {number} font.size=18 - The size of the text, in pixels. Write-only. + * @property {number} lineHeight=18 - The height of a line of text, in pixels. Write-only. + * @property {Color} color=255,255,255 - The color of the text. Synonym: textColor. Write-only. + * @property {number} alpha=1.0 - The opacity of the overlay, 0.0 - 1.0. Write-only. + * @property {Color} backgroundColor=0,0,0 - The color of the background rectangle. Write-only. + * @property {number} backgroundAlpha=0.7 - The opacity of the background rectangle. Write-only. + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * Write-only. + */ + TextOverlay::TextOverlay() : QmlOverlay(URL) { } TextOverlay::TextOverlay(const TextOverlay* textOverlay) diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index 49c76e6108..3aed2a5b42 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -62,6 +62,11 @@ void Volume3DOverlay::setProperties(const QVariantMap& properties) { } } +// JSDoc for copying to @typedefs of overlay types that inherit Volume3DOverlay. +/**jsdoc + * @typedef + * @property {Vec3} dimensions - The dimensions of the overlay. Synonyms: scale, size. + */ QVariant Volume3DOverlay::getProperty(const QString& property) { if (property == "dimensions" || property == "scale" || property == "size") { return vec3toVariant(getDimensions()); diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 7d5fefcb31..fcbc51b541 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -200,7 +200,6 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); _webSurface->getSurfaceContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); _webSurface->getSurfaceContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Tablet", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Assets", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("LODManager", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("OctreeStats", DependencyManager::get().data()); @@ -220,9 +219,6 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); - // Tablet inteference with Tablet.qml. Need to avoid this in QML space - _webSurface->getSurfaceContext()->setContextProperty("tabletInterface", DependencyManager::get().data()); - tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface.data()); // mark the TabletProxy object as cpp ownership. QObject* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); @@ -466,6 +462,67 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { } } +// Web3DOverlay overrides the meaning of Planar3DOverlay's dimensions property. +/**jsdoc + * These are the properties of a web3d {@link Overlays.OverlayType|OverlayType}. + * @typedef {object} Overlays.Web3DProperties + * + * @property {string} type=web3d - Has the value "web3d". Read-only. + * @property {Color} color=255,255,255 - The color of the overlay. + * @property {number} alpha=0.7 - The opacity of the overlay, 0.0 - 1.0. + * @property {number} pulseMax=0 - The maximum value of the pulse multiplier. + * @property {number} pulseMin=0 - The minimum value of the pulse multiplier. + * @property {number} pulsePeriod=1 - The duration of the color and alpha pulse, in seconds. A pulse multiplier value goes from + * pulseMin to pulseMax, then pulseMax to pulseMin in one period. + * @property {number} alphaPulse=0 - If non-zero, the alpha of the overlay is pulsed: the alpha value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {number} colorPulse=0 - If non-zero, the color of the overlay is pulsed: the color value is multiplied by the + * current pulse multiplier value each frame. If > 0 the pulse multiplier is applied in phase with the pulse period; if < 0 + * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise + * used.) + * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. + * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and + * rotating as you move your avatar. + * + * @property {string} name="" - A friendly name for the overlay. + * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and + * start. + * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as position. + * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. + * @property {Quat} localRotation - The orientation of the overlay relative to its parent if the overlay has a + * parentID set, otherwise the same value as rotation. + * @property {boolean} isSolid=false - Synonyms: solid, isFilled, + * filled, and filed. Antonyms: isWire and wire. + * Deprecated: The erroneous property spelling "filed" is deprecated and support for it will + * be removed. + * @property {boolean} isDashedLine=false - If true, a dashed line is drawn on the overlay's edges. Synonym: + * dashed. + * @property {boolean} ignoreRayIntersection=false - If true, + * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. + * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't + * have drawInFront set to true, and in front of entities. + * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. + * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. + * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if + * parentID is an avatar skeleton. A value of 65535 means "no joint". + * + * @property {boolean} isFacingAvatar - If true, the overlay is rotated to face the user's camera about an axis + * parallel to the user's avatar's "up" direction. + * + * @property {string} url - The URL of the Web page to display. + * @property {string} scriptURL="" - The URL of a JavaScript file to inject into the Web page. + * @property {Vec2} resolution - Deprecated: This property has been removed. + * @property {number} dpi=30 - The dots per inch to display the Web page at, on the overlay. + * @property {Vec2} dimensions=1,1 - The size of the overlay to display the Web page on, in meters. Synonyms: + * scale, size. + * @property {number} maxFPS=10 - The maximum update rate for the Web overlay content, in frames/second. + * @property {boolean} showKeyboardFocusHighlight=true - If true, the Web overlay is highlighted when it has + * keyboard focus. + * @property {string} inputMode=Touch - The user input mode to use - either "Touch" or "Mouse". + */ QVariant Web3DOverlay::getProperty(const QString& property) { if (property == "url") { return _url; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 78aa1f4ba8..44745c5c2d 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1641,9 +1641,17 @@ void Rig::initAnimGraph(const QUrl& url) { // load the anim graph _animLoader.reset(new AnimNodeLoader(url)); _animLoading = true; - connect(_animLoader.get(), &AnimNodeLoader::success, [this](AnimNode::Pointer nodeIn) { + std::weak_ptr weakSkeletonPtr = _animSkeleton; + connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr](AnimNode::Pointer nodeIn) { _animNode = nodeIn; - _animNode->setSkeleton(_animSkeleton); + + // abort load if the previous skeleton was deleted. + auto sharedSkeletonPtr = weakSkeletonPtr.lock(); + if (!sharedSkeletonPtr) { + return; + } + + _animNode->setSkeleton(sharedSkeletonPtr); if (_userAnimState.clipNodeEnum != UserAnimState::None) { // restore the user animation we had before reset. @@ -1651,6 +1659,7 @@ void Rig::initAnimGraph(const QUrl& url) { _userAnimState = { UserAnimState::None, "", 30.0f, false, 0.0f, 0.0f }; overrideAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); } + // restore the role animations we had before reset. for (auto& roleAnimState : _roleAnimStates) { auto roleState = roleAnimState.second; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index e738ad1c19..1ec4d9527f 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -31,7 +31,7 @@ class AnimInverseKinematics; // Rig instances are reentrant. // However only specific methods thread-safe. Noted below. -class Rig : public QObject, public std::enable_shared_from_this { +class Rig : public QObject { Q_OBJECT public: struct StateHandler { diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 476a8d4d88..672c0b69b3 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -96,9 +96,13 @@ void SoundProcessor::run() { QByteArray outputAudioByteArray; int sampleRate = interpretAsWav(rawAudioByteArray, outputAudioByteArray); - if (sampleRate != 0) { - downSample(outputAudioByteArray, sampleRate); + if (sampleRate == 0) { + qCDebug(audio) << "Unsupported WAV file type"; + emit onError(300, "Failed to load sound file, reason: unsupported WAV file type"); + return; } + + downSample(outputAudioByteArray, sampleRate); } else if (fileName.endsWith(RAW_EXTENSION)) { // check if this was a stereo raw file // since it's raw the only way for us to know that is if the file was called .stereo.raw diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 0993daaa8b..e646ba27f5 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -679,17 +679,13 @@ void OpenGLDisplayPlugin::internalPresent() { void OpenGLDisplayPlugin::present() { auto frameId = (uint64_t)presentCount(); PROFILE_RANGE_EX(render, __FUNCTION__, 0xffffff00, frameId) + uint64_t startPresent = usecTimestampNow(); { PROFILE_RANGE_EX(render, "updateFrameData", 0xff00ff00, frameId) updateFrameData(); } incrementPresentCount(); - { - PROFILE_RANGE_EX(render, "recycle", 0xff00ff00, frameId) - _gpuContext->recycle(); - } - if (_currentFrame) { { withPresentThreadLock([&] { @@ -718,6 +714,7 @@ void OpenGLDisplayPlugin::present() { gpu::Backend::freeGPUMemSize.set(gpu::gl::getFreeDedicatedMemory()); } + _movingAveragePresent.addSample((float)(usecTimestampNow() - startPresent)); } float OpenGLDisplayPlugin::newFramePresentRate() const { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 77c538619c..c969facd4e 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -251,10 +251,10 @@ void EntityTreeRenderer::shutdown() { } void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction) { - PROFILE_RANGE_EX(simulation_physics, "Add", 0xffff00ff, (uint64_t)_entitiesToAdd.size()); + PROFILE_RANGE_EX(simulation_physics, "AddToScene", 0xffff00ff, (uint64_t)_entitiesToAdd.size()); PerformanceTimer pt("add"); - // Clear any expired entities - // FIXME should be able to use std::remove_if, but it fails due to some + // Clear any expired entities + // FIXME should be able to use std::remove_if, but it fails due to some // weird compilation error related to EntityItemID assignment operators for (auto itr = _entitiesToAdd.begin(); _entitiesToAdd.end() != itr; ) { if (itr->second.expired()) { @@ -272,7 +272,7 @@ void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, r continue; } - // Path to the parent transforms is not valid, + // Path to the parent transforms is not valid, // don't add to the scene graph yet if (!entity->isParentPathComplete()) { continue; @@ -296,7 +296,7 @@ void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, r } void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, const ViewFrustum& view, render::Transaction& transaction) { - PROFILE_RANGE_EX(simulation_physics, "Change", 0xffff00ff, (uint64_t)_changedEntities.size()); + PROFILE_RANGE_EX(simulation_physics, "ChangeInScene", 0xffff00ff, (uint64_t)_changedEntities.size()); PerformanceTimer pt("change"); std::unordered_set changedEntities; _changedEntitiesGuard.withWriteLock([&] { @@ -402,6 +402,8 @@ void EntityTreeRenderer::update(bool simulate) { PerformanceTimer perfTimer("ETRupdate"); if (_tree && !_shuttingDown) { EntityTreePointer tree = std::static_pointer_cast(_tree); + + // here we update _currentFrame and _lastAnimated and sync with the server properties. tree->update(simulate); // Update the rendereable entities as needed @@ -736,7 +738,7 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); if (rayPickResult.intersects && rayPickResult.entity) { - //qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID; + // qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID; glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Release, PointerManager::MOUSE_POINTER_ID, diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index b4c215c39a..971d803bb5 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -63,13 +63,17 @@ bool ModelEntityWrapper::isModelLoaded() const { EntityItemPointer RenderableModelEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity(new RenderableModelEntityItem(entityID, properties.getDimensionsInitialized()), [](EntityItem* ptr) { ptr->deleteLater(); }); + entity->setProperties(properties); + return entity; } RenderableModelEntityItem::RenderableModelEntityItem(const EntityItemID& entityItemID, bool dimensionsInitialized) : ModelEntityWrapper(entityItemID), _dimensionsInitialized(dimensionsInitialized) { + + } RenderableModelEntityItem::~RenderableModelEntityItem() { } @@ -464,7 +468,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { shapeInfo.setParams(type, dimensions, getCompoundShapeURL()); } 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()); + // assert(_model && _model->isLoaded()); updateModelBounds(); model->updateGeometry(); @@ -974,9 +978,6 @@ void ModelEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entit entity->setModel({}); } -bool operator!=(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b) { - return !(a == b); -} void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { if (!_animation || !_animation->isLoaded()) { @@ -991,27 +992,16 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { return; } - if (!_lastAnimated) { - _lastAnimated = usecTimestampNow(); - return; - } - - auto now = usecTimestampNow(); - auto interval = now - _lastAnimated; - _lastAnimated = now; - float deltaTime = (float)interval / (float)USECS_PER_SECOND; - _currentFrame += (deltaTime * _renderAnimationProperties.getFPS()); - { - int animationCurrentFrame = (int)(glm::floor(_currentFrame)) % frameCount; - if (animationCurrentFrame < 0 || animationCurrentFrame > frameCount) { - animationCurrentFrame = 0; + float currentFrame = fmod(entity->getAnimationCurrentFrame(), (float)(frameCount)); + if (currentFrame < 0.0f) { + currentFrame += (float)frameCount; } - - if (animationCurrentFrame == _lastKnownCurrentFrame) { + int currentIntegerFrame = (int)(glm::floor(currentFrame)); + if (currentIntegerFrame == _lastKnownCurrentFrame) { return; } - _lastKnownCurrentFrame = animationCurrentFrame; + _lastKnownCurrentFrame = currentIntegerFrame; } if (_jointMapping.size() != _model->getJointStateCount()) { @@ -1039,10 +1029,10 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) { glm::mat4 translationMat; if (allowTranslation) { - if(index < translations.size()){ + if (index < translations.size()) { translationMat = glm::translate(translations[index]); } - } else if (index < animationJointNames.size()){ + } else if (index < animationJointNames.size()) { QString jointName = fbxJoints[index].name; // Pushing this here so its not done on every entity, with the exceptions of those allowing for translation if (originalFbxIndices.contains(jointName)) { @@ -1317,25 +1307,17 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce if (model->getRenderItemsNeedUpdate()) { model->updateRenderItems(); } - - { - DETAILED_PROFILE_RANGE(simulation_physics, "CheckAnimation"); - // make a copy of the animation properites - auto newAnimationProperties = entity->getAnimationProperties(); - if (newAnimationProperties != _renderAnimationProperties) { - withWriteLock([&] { - _renderAnimationProperties = newAnimationProperties; - _currentFrame = _renderAnimationProperties.getCurrentFrame(); - }); - } - } - + + // The code to deal with the change of properties is now in ModelEntityItem.cpp + // That is where _currentFrame and _lastAnimated were updated. if (_animating) { DETAILED_PROFILE_RANGE(simulation_physics, "Animate"); if (!jointsMapped()) { mapJoints(entity, model->getJointNames()); } - animate(entity); + if (!(entity->getAnimationFirstFrame() < 0) && !(entity->getAnimationFirstFrame() > entity->getAnimationLastFrame())) { + animate(entity); + } emit requestRenderUpdate(); } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 34c0a8f81d..7af10b09fd 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -33,7 +33,7 @@ namespace render { namespace entities { class ModelEntityRenderer; } } -//#define MODEL_ENTITY_USE_FADE_EFFECT +// #define MODEL_ENTITY_USE_FADE_EFFECT class ModelEntityWrapper : public ModelEntityItem { using Parent = ModelEntityItem; friend class render::entities::ModelEntityRenderer; @@ -133,7 +133,7 @@ class ModelEntityRenderer : public TypedEntityRenderer(new OffscreenQmlSurface(), deleter); _webSurface->create(); } @@ -291,7 +294,6 @@ void WebEntityRenderer::loadSourceURL() { if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || _lastSourceUrl.toLower().endsWith(".htm") || _lastSourceUrl.toLower().endsWith(".html")) { _contentType = htmlContent; - _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "qml/controls/")); // We special case YouTube URLs since we know they are videos that we should play with at least 30 FPS. if (sourceUrl.host().endsWith("youtube.com", Qt::CaseInsensitive)) { @@ -300,12 +302,11 @@ void WebEntityRenderer::loadSourceURL() { _webSurface->setMaxFps(DEFAULT_MAX_FPS); } - _webSurface->load("WebEntityView.qml", [this](QQmlContext* context, QObject* item) { + _webSurface->load("controls/WebEntityView.qml", [this](QQmlContext* context, QObject* item) { item->setProperty("url", _lastSourceUrl); }); } else { _contentType = qmlContent; - _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath())); _webSurface->load(_lastSourceUrl); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get(); diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 784945f0bb..591bccaabe 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -27,6 +27,8 @@ // Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1 // is a half unit sphere. However, the geometry cache renders a UNIT sphere, so we need to scale down. static const float SPHERE_ENTITY_SCALE = 0.5f; +static const unsigned int SUN_SHADOW_CASCADE_COUNT{ 4 }; +static const float SUN_SHADOW_MAX_DISTANCE{ 40.0f }; using namespace render; using namespace render::entities; @@ -116,7 +118,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) { // Do we need to allocate the light in the stage ? if (LightStage::isIndexInvalid(_sunIndex)) { _sunIndex = _stage->addLight(_sunLight); - _shadowIndex = _stage->addShadow(_sunIndex); + _shadowIndex = _stage->addShadow(_sunIndex, SUN_SHADOW_MAX_DISTANCE, SUN_SHADOW_CASCADE_COUNT); } else { _stage->updateLightArrayBuffer(_sunIndex); } diff --git a/libraries/entities/src/AnimationPropertyGroup.cpp b/libraries/entities/src/AnimationPropertyGroup.cpp index d6a2937553..2af56fb6b2 100644 --- a/libraries/entities/src/AnimationPropertyGroup.cpp +++ b/libraries/entities/src/AnimationPropertyGroup.cpp @@ -22,15 +22,28 @@ const float AnimationPropertyGroup::MAXIMUM_POSSIBLE_FRAME = 100000.0f; bool operator==(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b) { return - (a._url == b._url) && + (a._currentFrame == b._currentFrame) && (a._running == b._running) && (a._loop == b._loop) && + (a._hold == b._hold) && (a._firstFrame == b._firstFrame) && (a._lastFrame == b._lastFrame) && - (a._hold == b._hold); + (a._url == b._url); } +bool operator!=(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b) { + return + (a._currentFrame != b._currentFrame) || + (a._running != b._running) || + (a._loop != b._loop) || + (a._hold != b._hold) || + (a._firstFrame != b._firstFrame) || + (a._lastFrame != b._lastFrame) || + (a._url != b._url); +} + + void AnimationPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, QScriptValue& properties, QScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const { COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_URL, Animation, animation, URL, url); COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_ALLOW_TRANSLATION, Animation, animation, AllowTranslation, allowTranslation); @@ -130,6 +143,7 @@ void AnimationPropertyGroup::setFromOldAnimationSettings(const QString& value) { allowTranslation = settingsMap["allowTranslation"].toBool(); } + setAllowTranslation(allowTranslation); setFPS(fps); setCurrentFrame(currentFrame); diff --git a/libraries/entities/src/AnimationPropertyGroup.h b/libraries/entities/src/AnimationPropertyGroup.h index affa960d66..54d4ced92f 100644 --- a/libraries/entities/src/AnimationPropertyGroup.h +++ b/libraries/entities/src/AnimationPropertyGroup.h @@ -89,6 +89,7 @@ public: protected: friend bool operator==(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b); + friend bool operator!=(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b); void setFromOldAnimationSettings(const QString& value); }; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 2b75606093..fcf7519bbd 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include // usecTimestampNow() #include @@ -115,6 +116,7 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_EDITION_NUMBER; requestedProperties += PROP_ENTITY_INSTANCE_NUMBER; requestedProperties += PROP_CERTIFICATE_ID; + requestedProperties += PROP_STATIC_CERTIFICATE_VERSION; requestedProperties += PROP_NAME; requestedProperties += PROP_HREF; @@ -272,6 +274,7 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, getEditionNumber()); APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, getEntityInstanceNumber()); APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, getCertificateID()); + APPEND_ENTITY_PROPERTY(PROP_STATIC_CERTIFICATE_VERSION, getStaticCertificateVersion()); APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, getCollisionSoundURL()); @@ -820,6 +823,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_EDITION_NUMBER, quint32, setEditionNumber); READ_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, quint32, setEntityInstanceNumber); READ_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, QString, setCertificateID); + READ_ENTITY_PROPERTY(PROP_STATIC_CERTIFICATE_VERSION, quint32, setStaticCertificateVersion); READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); @@ -985,6 +989,7 @@ void EntityItem::setCollisionSoundURL(const QString& value) { } void EntityItem::simulate(const quint64& now) { + DETAILED_PROFILE_RANGE(simulation_physics, "Simulate"); if (getLastSimulated() == 0) { setLastSimulated(now); } @@ -1040,6 +1045,7 @@ void EntityItem::simulate(const quint64& now) { } bool EntityItem::stepKinematicMotion(float timeElapsed) { + DETAILED_PROFILE_RANGE(simulation_physics, "StepKinematicMotion"); // get all the data Transform transform; glm::vec3 linearVelocity; @@ -1250,6 +1256,7 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(editionNumber, getEditionNumber); COPY_ENTITY_PROPERTY_TO_PROPERTIES(entityInstanceNumber, getEntityInstanceNumber); COPY_ENTITY_PROPERTY_TO_PROPERTIES(certificateID, getCertificateID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(staticCertificateVersion, getStaticCertificateVersion); COPY_ENTITY_PROPERTY_TO_PROPERTIES(name, getName); COPY_ENTITY_PROPERTY_TO_PROPERTIES(href, getHref); @@ -1357,6 +1364,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(editionNumber, setEditionNumber); SET_ENTITY_PROPERTY_FROM_PROPERTIES(entityInstanceNumber, setEntityInstanceNumber); SET_ENTITY_PROPERTY_FROM_PROPERTIES(certificateID, setCertificateID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(staticCertificateVersion, setStaticCertificateVersion); SET_ENTITY_PROPERTY_FROM_PROPERTIES(name, setName); SET_ENTITY_PROPERTY_FROM_PROPERTIES(href, setHref); @@ -1627,6 +1635,10 @@ void EntityItem::setParentID(const QUuid& value) { newParentNoBootstrapping |= Simulation::NO_BOOTSTRAPPING; } + if (!oldParentID.isNull() && (oldParentID == Physics::getSessionUUID() || oldParentID == AVATAR_SELF_ID)) { + oldParentNoBootstrapping |= Simulation::NO_BOOTSTRAPPING; + } + if ((bool)(oldParentNoBootstrapping ^ newParentNoBootstrapping)) { if ((bool)(newParentNoBootstrapping & Simulation::NO_BOOTSTRAPPING)) { markDirtyFlags(Simulation::NO_BOOTSTRAPPING); @@ -2808,6 +2820,7 @@ DEFINE_PROPERTY_ACCESSOR(QString, MarketplaceID, marketplaceID) DEFINE_PROPERTY_ACCESSOR(quint32, EditionNumber, editionNumber) DEFINE_PROPERTY_ACCESSOR(quint32, EntityInstanceNumber, entityInstanceNumber) DEFINE_PROPERTY_ACCESSOR(QString, CertificateID, certificateID) +DEFINE_PROPERTY_ACCESSOR(quint32, StaticCertificateVersion, staticCertificateVersion) uint32_t EntityItem::getDirtyFlags() const { uint32_t result; @@ -2865,7 +2878,7 @@ void EntityItem::retrieveMarketplacePublicKey() { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); requestURL.setPath("/api/v1/commerce/marketplace_key"); QJsonObject request; networkRequest.setUrl(requestURL); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index d2e3101c1d..0136590ffa 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -331,6 +331,8 @@ public: void setEntityInstanceNumber(const quint32&); QString getCertificateID() const; void setCertificateID(const QString& value); + quint32 getStaticCertificateVersion() const; + void setStaticCertificateVersion(const quint32&); // TODO: get rid of users of getRadius()... float getRadius() const; @@ -552,6 +554,7 @@ protected: quint32 _editionNumber { ENTITY_ITEM_DEFAULT_EDITION_NUMBER }; quint32 _entityInstanceNumber { ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER }; QString _marketplaceID { ENTITY_ITEM_DEFAULT_MARKETPLACE_ID }; + quint32 _staticCertificateVersion { ENTITY_ITEM_DEFAULT_STATIC_CERTIFICATE_VERSION }; // NOTE: Damping is applied like this: v *= pow(1 - damping, dt) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 4dc3aba1a1..3bbd698a01 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -325,6 +324,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_EDITION_NUMBER, editionNumber); CHECK_PROPERTY_CHANGE(PROP_ENTITY_INSTANCE_NUMBER, entityInstanceNumber); CHECK_PROPERTY_CHANGE(PROP_CERTIFICATE_ID, certificateID); + CHECK_PROPERTY_CHANGE(PROP_STATIC_CERTIFICATE_VERSION, staticCertificateVersion); CHECK_PROPERTY_CHANGE(PROP_NAME, name); CHECK_PROPERTY_CHANGE(PROP_BACKGROUND_MODE, backgroundMode); @@ -461,6 +461,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EDITION_NUMBER, editionNumber); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ENTITY_INSTANCE_NUMBER, entityInstanceNumber); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CERTIFICATE_ID, certificateID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_STATIC_CERTIFICATE_VERSION, staticCertificateVersion); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL); @@ -745,6 +746,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(editionNumber, quint32, setEditionNumber); COPY_PROPERTY_FROM_QSCRIPTVALUE(entityInstanceNumber, quint32, setEntityInstanceNumber); COPY_PROPERTY_FROM_QSCRIPTVALUE(certificateID, QString, setCertificateID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(staticCertificateVersion, quint32, setStaticCertificateVersion); COPY_PROPERTY_FROM_QSCRIPTVALUE(name, QString, setName); COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionSoundURL, QString, setCollisionSoundURL); @@ -903,6 +905,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(editionNumber); COPY_PROPERTY_IF_CHANGED(entityInstanceNumber); COPY_PROPERTY_IF_CHANGED(certificateID); + COPY_PROPERTY_IF_CHANGED(staticCertificateVersion); COPY_PROPERTY_IF_CHANGED(name); COPY_PROPERTY_IF_CHANGED(collisionSoundURL); @@ -1094,6 +1097,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_EDITION_NUMBER, EditionNumber, editionNumber, quint32); ADD_PROPERTY_TO_MAP(PROP_ENTITY_INSTANCE_NUMBER, EntityInstanceNumber, entityInstanceNumber, quint32); ADD_PROPERTY_TO_MAP(PROP_CERTIFICATE_ID, CertificateID, certificateID, QString); + ADD_PROPERTY_TO_MAP(PROP_STATIC_CERTIFICATE_VERSION, StaticCertificateVersion, staticCertificateVersion, quint32); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLightColor, keyLightColor, xColor); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_INTENSITY, KeyLightIntensity, keyLightIntensity, float); @@ -1483,6 +1487,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, properties.getEditionNumber()); APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, properties.getEntityInstanceNumber()); APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, properties.getCertificateID()); + APPEND_ENTITY_PROPERTY(PROP_STATIC_CERTIFICATE_VERSION, properties.getStaticCertificateVersion()); } if (propertyCount > 0) { @@ -1834,6 +1839,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_EDITION_NUMBER, quint32, setEditionNumber); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ENTITY_INSTANCE_NUMBER, quint32, setEntityInstanceNumber); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CERTIFICATE_ID, QString, setCertificateID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STATIC_CERTIFICATE_VERSION, quint32, setStaticCertificateVersion); return valid; } @@ -2001,6 +2007,7 @@ void EntityItemProperties::markAllChanged() { _editionNumberChanged = true; _entityInstanceNumberChanged = true; _certificateIDChanged = true; + _staticCertificateVersionChanged = true; _keyLight.markAllChanged(); @@ -2343,6 +2350,9 @@ QList EntityItemProperties::listChangedProperties() { if (certificateIDChanged()) { out += "certificateID"; } + if (staticCertificateVersionChanged()) { + out += "staticCertificateVersion"; + } if (backgroundModeChanged()) { out += "backgroundMode"; @@ -2501,6 +2511,9 @@ QByteArray EntityItemProperties::getStaticCertificateJSON() const { // of the entity as reviewed during the certification submission. QJsonObject json; + + quint32 staticCertificateVersion = getStaticCertificateVersion(); + if (!getAnimation().getURL().isEmpty()) { json["animationURL"] = getAnimation().getURL(); } @@ -2517,7 +2530,11 @@ QByteArray EntityItemProperties::getStaticCertificateJSON() const { ADD_STRING_PROPERTY(marketplaceID, MarketplaceID); ADD_STRING_PROPERTY(modelURL, ModelURL); ADD_STRING_PROPERTY(script, Script); + if (staticCertificateVersion >= 1) { + ADD_STRING_PROPERTY(serverScripts, ServerScripts); + } ADD_ENUM_PROPERTY(shapeType, ShapeType); + ADD_INT_PROPERTY(staticCertificateVersion, StaticCertificateVersion); json["type"] = EntityTypes::getEntityTypeName(getType()); return QJsonDocument(json).toJson(QJsonDocument::Compact); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index a1118e8441..4887b42138 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -221,6 +221,7 @@ public: DEFINE_PROPERTY_REF(PROP_EDITION_NUMBER, EditionNumber, editionNumber, quint32, ENTITY_ITEM_DEFAULT_EDITION_NUMBER); DEFINE_PROPERTY_REF(PROP_ENTITY_INSTANCE_NUMBER, EntityInstanceNumber, entityInstanceNumber, quint32, ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER); DEFINE_PROPERTY_REF(PROP_CERTIFICATE_ID, CertificateID, certificateID, QString, ENTITY_ITEM_DEFAULT_CERTIFICATE_ID); + DEFINE_PROPERTY_REF(PROP_STATIC_CERTIFICATE_VERSION, StaticCertificateVersion, staticCertificateVersion, quint32, ENTITY_ITEM_DEFAULT_STATIC_CERTIFICATE_VERSION); // these are used when bouncing location data into and out of scripts DEFINE_PROPERTY_REF(PROP_LOCAL_POSITION, LocalPosition, localPosition, glmVec3, ENTITY_ITEM_ZERO_VEC3); @@ -475,6 +476,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, EditionNumber, editionNumber, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, EntityInstanceNumber, entityInstanceNumber, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, CertificateID, certificateID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, StaticCertificateVersion, staticCertificateVersion, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, BackgroundMode, backgroundMode, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, HazeMode, hazeMode, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index f4e8a18012..49bce37fbd 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -41,6 +41,7 @@ const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); const quint32 ENTITY_ITEM_DEFAULT_EDITION_NUMBER = 0; const quint32 ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER = 0; const QString ENTITY_ITEM_DEFAULT_CERTIFICATE_ID = QString(""); +const quint32 ENTITY_ITEM_DEFAULT_STATIC_CERTIFICATE_VERSION = 0; const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; const float ENTITY_ITEM_DEFAULT_LOCAL_RENDER_ALPHA = 1.0f; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index c44f8091e0..d515d60535 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -200,6 +200,7 @@ enum EntityPropertyList { PROP_EDITION_NUMBER, PROP_ENTITY_INSTANCE_NUMBER, PROP_CERTIFICATE_ID, + PROP_STATIC_CERTIFICATE_VERSION, PROP_HAZE_MODE, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 25eb126704..206065beeb 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1859,6 +1859,19 @@ glm::mat4 EntityScriptingInterface::getEntityLocalTransform(const QUuid& entityI return result; } +QString EntityScriptingInterface::getStaticCertificateJSON(const QUuid& entityID) { + QByteArray result; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + result = entity->getProperties().getStaticCertificateJSON(); + } + }); + } + return result; +} + bool EntityScriptingInterface::verifyStaticCertificateProperties(const QUuid& entityID) { bool result = false; if (_entityTree) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index cc5d8ff16f..095a821482 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -423,6 +423,12 @@ public slots: */ Q_INVOKABLE glm::mat4 getEntityLocalTransform(const QUuid& entityID); + + /**jsdoc + * Return the Static Certificate JSON for the specified {EntityID}. + * @return {QByteArray} The Static Certificate JSON for the specified entity. + */ + Q_INVOKABLE QString getStaticCertificateJSON(const QUuid& entityID); Q_INVOKABLE bool verifyStaticCertificateProperties(const QUuid& entityID); signals: diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index f91d728d78..36b0d8ab2d 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -29,7 +29,6 @@ void EntitySimulation::setEntityTree(EntityTreePointer tree) { } void EntitySimulation::updateEntities() { - PROFILE_RANGE(simulation_physics, "ES::updateEntities"); QMutexLocker lock(&_mutex); quint64 now = usecTimestampNow(); @@ -38,12 +37,7 @@ void EntitySimulation::updateEntities() { callUpdateOnEntitiesThatNeedIt(now); moveSimpleKinematics(now); updateEntitiesInternal(now); - - { - PROFILE_RANGE(simulation_physics, "Sort"); - PerformanceTimer perfTimer("sortingEntities"); - sortEntitiesThatMoved(); - } + sortEntitiesThatMoved(); } void EntitySimulation::takeEntitiesToDelete(VectorOfEntities& entitiesToDelete) { @@ -101,6 +95,7 @@ void EntitySimulation::changeEntityInternal(EntityItemPointer entity) { // protected void EntitySimulation::expireMortalEntities(const quint64& now) { if (now > _nextExpiry) { + PROFILE_RANGE_EX(simulation_physics, "ExpireMortals", 0xffff00ff, (uint64_t)_mortalEntities.size()); // only search for expired entities if we expect to find one _nextExpiry = quint64(-1); QMutexLocker lock(&_mutex); @@ -146,6 +141,7 @@ void EntitySimulation::callUpdateOnEntitiesThatNeedIt(const quint64& now) { // protected void EntitySimulation::sortEntitiesThatMoved() { + PROFILE_RANGE_EX(simulation_physics, "SortTree", 0xffff00ff, (uint64_t)_entitiesToSort.size()); // NOTE: this is only for entities that have been moved by THIS EntitySimulation. // External changes to entity position/shape are expected to be sorted outside of the EntitySimulation. MovingEntitiesOperator moveOperator; @@ -265,7 +261,7 @@ void EntitySimulation::clearEntities() { } void EntitySimulation::moveSimpleKinematics(const quint64& now) { - PROFILE_RANGE_EX(simulation_physics, "Kinematics", 0xffff00ff, (uint64_t)_simpleKinematicEntities.size()); + PROFILE_RANGE_EX(simulation_physics, "MoveSimples", 0xffff00ff, (uint64_t)_simpleKinematicEntities.size()); SetOfEntities::iterator itemItr = _simpleKinematicEntities.begin(); while (itemItr != _simpleKinematicEntities.end()) { EntityItemPointer entity = *itemItr; diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index e62399ce95..b5765bb44b 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1308,7 +1308,7 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL; + QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL(); requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/transfer"); QJsonObject request; request["certificate_id"] = certID; @@ -1770,24 +1770,26 @@ void EntityTree::addToNeedsParentFixupList(EntityItemPointer entity) { } void EntityTree::update(bool simulate) { - PROFILE_RANGE(simulation_physics, "ET::update"); + PROFILE_RANGE(simulation_physics, "UpdateTree"); fixupNeedsParentFixups(); if (simulate && _simulation) { withWriteLock([&] { _simulation->updateEntities(); - VectorOfEntities pendingDeletes; - _simulation->takeEntitiesToDelete(pendingDeletes); + { + PROFILE_RANGE(simulation_physics, "Deletes"); + VectorOfEntities pendingDeletes; + _simulation->takeEntitiesToDelete(pendingDeletes); + if (pendingDeletes.size() > 0) { + // translate into list of ID's + QSet idsToDelete; - if (pendingDeletes.size() > 0) { - // translate into list of ID's - QSet idsToDelete; + for (auto entity : pendingDeletes) { + idsToDelete.insert(entity->getEntityItemID()); + } - for (auto entity : pendingDeletes) { - idsToDelete.insert(entity->getEntityItemID()); + // delete these things the roundabout way + deleteEntities(idsToDelete, true); } - - // delete these things the roundabout way - deleteEntities(idsToDelete, true); } }); } diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 5ec86e2f76..3e6d19f430 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -33,6 +33,8 @@ EntityItemPointer ModelEntityItem::factory(const EntityItemID& entityID, const E ModelEntityItem::ModelEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { + _lastAnimated = usecTimestampNow(); + // set the last animated when interface (re)starts _type = EntityTypes::Model; _lastKnownCurrentFrame = -1; _color[0] = _color[1] = _color[2] = 0; @@ -186,6 +188,103 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit } + +// added update function back for property fix +void ModelEntityItem::update(const quint64& now) { + + { + auto currentAnimationProperties = this->getAnimationProperties(); + + if (_previousAnimationProperties != currentAnimationProperties) { + withWriteLock([&] { + // if we hit start animation or change the first or last frame then restart the animation + if ((currentAnimationProperties.getFirstFrame() != _previousAnimationProperties.getFirstFrame()) || + (currentAnimationProperties.getLastFrame() != _previousAnimationProperties.getLastFrame()) || + (currentAnimationProperties.getRunning() && !_previousAnimationProperties.getRunning())) { + + // when we start interface and the property is are set then the current frame is initialized to -1 + if (_currentFrame < 0) { + // don't reset _lastAnimated here because we need the timestamp from the ModelEntityItem constructor for when the properties were set + _currentFrame = currentAnimationProperties.getCurrentFrame(); + setAnimationCurrentFrame(_currentFrame); + } else { + _lastAnimated = usecTimestampNow(); + _currentFrame = currentAnimationProperties.getFirstFrame(); + setAnimationCurrentFrame(currentAnimationProperties.getFirstFrame()); + } + } else if (!currentAnimationProperties.getRunning() && _previousAnimationProperties.getRunning()) { + _currentFrame = currentAnimationProperties.getFirstFrame(); + setAnimationCurrentFrame(_currentFrame); + } else if (currentAnimationProperties.getCurrentFrame() != _previousAnimationProperties.getCurrentFrame()) { + // don't reset _lastAnimated here because the currentFrame was set with the previous setting of _lastAnimated + _currentFrame = currentAnimationProperties.getCurrentFrame(); + } + + }); + _previousAnimationProperties = this->getAnimationProperties(); + + } + + if (isAnimatingSomething()) { + if (!(getAnimationFirstFrame() < 0) && !(getAnimationFirstFrame() > getAnimationLastFrame())) { + updateFrameCount(); + } + } + } + + EntityItem::update(now); +} + +bool ModelEntityItem::needsToCallUpdate() const { + + return true; +} + +void ModelEntityItem::updateFrameCount() { + + if (_currentFrame < 0.0f) { + return; + } + + if (!_lastAnimated) { + _lastAnimated = usecTimestampNow(); + return; + } + + auto now = usecTimestampNow(); + + // update the interval since the last animation. + auto interval = now - _lastAnimated; + _lastAnimated = now; + + // if fps is negative then increment timestamp and return. + if (getAnimationFPS() < 0.0f) { + return; + } + + int updatedFrameCount = getAnimationLastFrame() - getAnimationFirstFrame() + 1; + + if (!getAnimationHold() && getAnimationIsPlaying()) { + float deltaTime = (float)interval / (float)USECS_PER_SECOND; + _currentFrame += (deltaTime * getAnimationFPS()); + if (_currentFrame > getAnimationLastFrame()) { + if (getAnimationLoop()) { + _currentFrame = getAnimationFirstFrame() + (int)(glm::floor(_currentFrame - getAnimationFirstFrame())) % (updatedFrameCount - 1); + } else { + _currentFrame = getAnimationLastFrame(); + } + } else if (_currentFrame < getAnimationFirstFrame()) { + if (getAnimationFirstFrame() < 0) { + _currentFrame = 0; + } else { + _currentFrame = getAnimationFirstFrame(); + } + } + // qCDebug(entities) << "in update frame " << _currentFrame; + setAnimationCurrentFrame(_currentFrame); + } +} + void ModelEntityItem::debugDump() const { qCDebug(entities) << "ModelEntityItem id:" << getEntityItemID(); qCDebug(entities) << " edited ago:" << getEditedAgo(); @@ -538,6 +637,13 @@ void ModelEntityItem::setAnimationLoop(bool loop) { }); } +bool ModelEntityItem::getAnimationLoop() const { + return resultWithReadLock([&] { + return _animationProperties.getLoop(); + }); +} + + void ModelEntityItem::setAnimationHold(bool hold) { withWriteLock([&] { _animationProperties.setHold(hold); @@ -573,8 +679,9 @@ float ModelEntityItem::getAnimationLastFrame() const { return _animationProperties.getLastFrame(); }); } + bool ModelEntityItem::getAnimationIsPlaying() const { - return resultWithReadLock([&] { + return resultWithReadLock([&] { return _animationProperties.getRunning(); }); } @@ -585,8 +692,15 @@ float ModelEntityItem::getAnimationCurrentFrame() const { }); } -bool ModelEntityItem::isAnimatingSomething() const { +float ModelEntityItem::getAnimationFPS() const { return resultWithReadLock([&] { + return _animationProperties.getFPS(); + }); +} + + +bool ModelEntityItem::isAnimatingSomething() const { + return resultWithReadLock([&] { return !_animationProperties.getURL().isEmpty() && _animationProperties.getRunning() && (_animationProperties.getFPS() != 0.0f); diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 2c3ef3aa2d..7fee022011 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -17,6 +17,7 @@ #include #include "AnimationPropertyGroup.h" + class ModelEntityItem : public EntityItem { public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); @@ -46,8 +47,11 @@ public: EntityPropertyFlags& propertyFlags, bool overwriteLocalData, bool& somethingChanged) override; - //virtual void update(const quint64& now) override; - //virtual bool needsToCallUpdate() const override; + // update() and needstocallupdate() added back for the entity property fix + virtual void update(const quint64& now) override; + virtual bool needsToCallUpdate() const override; + void updateFrameCount(); + virtual void debugDump() const override; void setShapeType(ShapeType type) override; @@ -90,6 +94,7 @@ public: bool getAnimationAllowTranslation() const { return _animationProperties.getAllowTranslation(); }; void setAnimationLoop(bool loop); + bool getAnimationLoop() const; void setAnimationHold(bool hold); bool getAnimationHold() const; @@ -102,6 +107,7 @@ public: bool getAnimationIsPlaying() const; float getAnimationCurrentFrame() const; + float getAnimationFPS() const; bool isAnimatingSomething() const; static const QString DEFAULT_TEXTURES; @@ -147,7 +153,7 @@ protected: }; QVector _localJointData; - int _lastKnownCurrentFrame; + int _lastKnownCurrentFrame{-1}; rgbColor _color; QString _modelURL; @@ -160,6 +166,11 @@ protected: QString _textures; ShapeType _shapeType = SHAPE_TYPE_NONE; + +private: + uint64_t _lastAnimated{ 0 }; + AnimationPropertyGroup _previousAnimationProperties; + float _currentFrame{ -1.0f }; }; #endif // hifi_ModelEntityItem_h diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index e0c2efd72e..315c6a86d2 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -56,7 +56,7 @@ float OBJTokenizer::getFloat() { return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); } -int OBJTokenizer::nextToken() { +int OBJTokenizer::nextToken(bool allowSpaceChar /*= false*/) { if (_pushedBackToken != NO_PUSHBACKED_TOKEN) { int token = _pushedBackToken; _pushedBackToken = NO_PUSHBACKED_TOKEN; @@ -93,7 +93,7 @@ int OBJTokenizer::nextToken() { _datum = ""; _datum.append(ch); while (_device->getChar(&ch)) { - if (QChar(ch).isSpace() || ch == '\"') { + if ((QChar(ch).isSpace() || ch == '\"') && (!allowSpaceChar || ch != ' ')) { ungetChar(ch); // read until we encounter a special character, then replace it break; } @@ -399,7 +399,7 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi currentMaterialName = QString("part-") + QString::number(_partCounter++); } } else if (token == "mtllib" && !_url.isEmpty()) { - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { + if (tokenizer.nextToken(true) != OBJTokenizer::DATUM_TOKEN) { break; } QByteArray libraryName = tokenizer.getDatum(); diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index fb250833cf..45e3f79480 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -11,7 +11,7 @@ public: DATUM_TOKEN = 0x100, COMMENT_TOKEN = 0x101 }; - int nextToken(); + int nextToken(bool allowSpaceChar = false); const QByteArray& getDatum() const { return _datum; } bool isNextTokenFloat(); const QByteArray getLineAsDatum(); // some "filenames" have spaces in them diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index f138244fa2..f93d430152 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -40,10 +40,6 @@ static bool disableOpenGL45 = QProcessEnvironment::systemEnvironment().contains( static GLBackend* INSTANCE{ nullptr }; BackendPointer GLBackend::createBackend() { - // The ATI memory info extension only exposes 'free memory' so we want to force it to - // cache the value as early as possible - getDedicatedMemory(); - // FIXME provide a mechanism to override the backend for testing // Where the gpuContext is initialized and where the TRUE Backend is created and assigned auto version = QOpenGLContextWrapper::currentContextVersion(); @@ -776,7 +772,7 @@ void GLBackend::recycle() const { GLVariableAllocationSupport::manageMemory(); GLVariableAllocationSupport::_frameTexturesCreated = 0; - + Texture::KtxStorage::releaseOpenKtxFiles(); } void GLBackend::setCameraCorrection(const Mat4& correction) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendShader.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendShader.cpp index 0c1b6880cb..9adfd550ef 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendShader.cpp @@ -318,7 +318,10 @@ int GLBackend::makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slot if (requestedBinding != slotBindings.end()) { if (binding != (*requestedBinding)._location) { binding = (*requestedBinding)._location; - glProgramUniform1i(glprogram, location, binding); + for (auto i = 0; i < size; i++) { + // If we are working with an array of textures, reserve for each elemet + glProgramUniform1i(glprogram, location+i, binding+i); + } } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp index 7f46a0aa2b..5a015cd550 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -88,53 +88,6 @@ gpu::Size getFreeDedicatedMemory() { return result; } -gpu::Size getDedicatedMemory() { - static Size dedicatedMemory { 0 }; - static std::once_flag once; - std::call_once(once, [&] { -#ifdef Q_OS_WIN - if (!dedicatedMemory && wglGetGPUIDsAMD && wglGetGPUInfoAMD) { - UINT maxCount = wglGetGPUIDsAMD(0, 0); - std::vector ids; - ids.resize(maxCount); - wglGetGPUIDsAMD(maxCount, &ids[0]); - GLuint memTotal; - wglGetGPUInfoAMD(ids[0], WGL_GPU_RAM_AMD, GL_UNSIGNED_INT, sizeof(GLuint), &memTotal); - dedicatedMemory = MB_TO_BYTES(memTotal); - } -#endif - - if (!dedicatedMemory) { - GLint atiGpuMemory[4]; - // not really total memory, but close enough if called early enough in the application lifecycle - glGetIntegerv(GL_TEXTURE_FREE_MEMORY_ATI, atiGpuMemory); - if (GL_NO_ERROR == glGetError()) { - dedicatedMemory = KB_TO_BYTES(atiGpuMemory[0]); - } - } - - if (!dedicatedMemory) { - GLint nvGpuMemory { 0 }; - glGetIntegerv(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX, &nvGpuMemory); - if (GL_NO_ERROR == glGetError()) { - dedicatedMemory = KB_TO_BYTES(nvGpuMemory); - } - } - - if (!dedicatedMemory) { - auto gpuIdent = GPUIdent::getInstance(); - if (gpuIdent && gpuIdent->isValid()) { - dedicatedMemory = MB_TO_BYTES(gpuIdent->getMemory()); - } - } - }); - - return dedicatedMemory; -} - - - - ComparisonFunction comparisonFuncFromGL(GLenum func) { if (func == GL_NEVER) { return NEVER; diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.h b/libraries/gpu-gl/src/gpu/gl/GLShared.h index a1cf27afa6..d3ad7c028b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.h +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.h @@ -28,7 +28,6 @@ void serverWait(); // Create a fence and synchronously wait on the fence void clientWait(); -gpu::Size getDedicatedMemory(); gpu::Size getFreeDedicatedMemory(); ComparisonFunction comparisonFuncFromGL(GLenum func); State::StencilOp stencilOpFromGL(GLenum stencilOp); diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 1877b494cf..06208179e0 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -321,13 +321,18 @@ public: void reset() override { } + // Don't keep files open forever. We close them at the beginning of each frame (GLBackend::recycle) + static void releaseOpenKtxFiles(); + protected: std::shared_ptr maybeOpenFile() const; - mutable std::mutex _cacheFileCreateMutex; - mutable std::mutex _cacheFileWriteMutex; + mutable std::shared_ptr _cacheFileMutex { std::make_shared() }; mutable std::weak_ptr _cacheFile; + static std::vector, std::shared_ptr>> _cachedKtxFiles; + static std::mutex _cachedKtxFilesMutex; + std::string _filename; cache::FilePointer _cacheEntry; std::atomic _minMipLevelAvailable; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index 08fc4ec101..883d9abf15 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -23,6 +23,9 @@ using namespace gpu; using PixelsPointer = Texture::PixelsPointer; using KtxStorage = Texture::KtxStorage; +std::vector, std::shared_ptr>> KtxStorage::_cachedKtxFiles; +std::mutex KtxStorage::_cachedKtxFilesMutex; + struct GPUKTXPayload { using Version = uint8; @@ -187,36 +190,44 @@ KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) { } } +// maybeOpenFile should be called with _cacheFileMutex already held to avoid modifying the file from multiple threads std::shared_ptr KtxStorage::maybeOpenFile() const { - // 1. Try to get the shared ptr - // 2. If it doesn't exist, grab the mutex around its creation - // 3. If it was created before we got the mutex, return it - // 4. Otherwise, create it - + // Try to get the shared_ptr std::shared_ptr file = _cacheFile.lock(); if (file) { return file; } + // If the file isn't open, create it and save a weak_ptr to it + file = std::make_shared(_filename.c_str()); + _cacheFile = file; + { - std::lock_guard lock{ _cacheFileCreateMutex }; - - file = _cacheFile.lock(); - if (file) { - return file; - } - - file = std::make_shared(_filename.c_str()); - _cacheFile = file; + // Add the shared_ptr to the global list of open KTX files, to be released at the beginning of the next present thread frame + std::lock_guard lock(_cachedKtxFilesMutex); + _cachedKtxFiles.emplace_back(file, _cacheFileMutex); } return file; } +void KtxStorage::releaseOpenKtxFiles() { + std::vector, std::shared_ptr>> localKtxFiles; + { + std::lock_guard lock(_cachedKtxFilesMutex); + localKtxFiles.swap(_cachedKtxFiles); + } + for (auto& cacheFileAndMutex : localKtxFiles) { + std::lock_guard lock(*(cacheFileAndMutex.second)); + cacheFileAndMutex.first.reset(); + } +} + PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const { auto faceOffset = _ktxDescriptor->getMipFaceTexelsOffset(level, face); auto faceSize = _ktxDescriptor->getMipFaceTexelsSize(level, face); if (faceSize != 0 && faceOffset != 0) { + std::lock_guard lock(*_cacheFileMutex); auto file = maybeOpenFile(); if (file) { auto storageView = file->createView(faceSize, faceOffset); @@ -262,6 +273,7 @@ void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& stor return; } + std::lock_guard lock(*_cacheFileMutex); auto file = maybeOpenFile(); if (!file) { qWarning() << "Failed to open file to assign mip data " << QString::fromStdString(_filename); @@ -279,8 +291,6 @@ void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& stor imageData += ktx::IMAGE_SIZE_WIDTH; { - std::lock_guard lock { _cacheFileWriteMutex }; - if (level != _minMipLevelAvailable - 1) { qWarning() << "Invalid level to be stored"; return; diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 9e4ab2e43f..f78ed1a583 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -981,7 +981,7 @@ public: static QImage extractEquirectangularFace(const QImage& source, gpu::Texture::CubeFace face, int faceWidth) { QImage image(faceWidth, faceWidth, source.format()); - glm::vec2 dstInvSize(1.0f / (float)source.width(), 1.0f / (float)source.height()); + glm::vec2 dstInvSize(1.0f / faceWidth); struct CubeToXYZ { gpu::Texture::CubeFace _face; diff --git a/libraries/midi/src/Midi.cpp b/libraries/midi/src/Midi.cpp index ad650cf067..6c2471f680 100644 --- a/libraries/midi/src/Midi.cpp +++ b/libraries/midi/src/Midi.cpp @@ -177,9 +177,10 @@ void Midi::MidiCleanup() { #endif void Midi::noteReceived(int status, int note, int velocity) { - if (((status & MIDI_STATUS_MASK) != MIDI_NOTE_OFF) && - ((status & MIDI_STATUS_MASK) != MIDI_NOTE_ON)) { - return; // NOTE: only sending note-on and note-off to Javascript + if (((status & MIDI_STATUS_MASK) != MIDI_NOTE_OFF) && + ((status & MIDI_STATUS_MASK) != MIDI_NOTE_ON) && + ((status & MIDI_STATUS_MASK) != MIDI_CONTROL_CHANGE)) { + return; // NOTE: only sending note-on, note-off, and control-change to Javascript } QVariantMap eventData; diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index b8f81dc7a4..8153dba3a5 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -217,8 +217,6 @@ gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, co if (!result) { _texturesByHashes[hash] = texture; result = texture; - } else { - qCWarning(modelnetworking) << "QQQ Swapping out texture with previous live texture in hash " << hash.c_str(); } } return result; diff --git a/libraries/model/src/model/Haze.h b/libraries/model/src/model/Haze.h index a0cc7c3bc7..2a575eb151 100644 --- a/libraries/model/src/model/Haze.h +++ b/libraries/model/src/model/Haze.h @@ -128,7 +128,7 @@ namespace model { Parameters() {} }; - UniformBufferView _hazeParametersBuffer; + UniformBufferView _hazeParametersBuffer { nullptr }; }; using HazePointer = std::shared_ptr; diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 7d97687d0b..87b17d00d5 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -97,7 +97,7 @@ public: void setTemporaryDomain(const QUuid& domainID, const QString& key); const QString& getTemporaryDomainKey(const QUuid& domainID) { return _accountInfo.getTemporaryDomainKey(domainID); } - QUrl getMetaverseServerURL() { return NetworkingConstants::METAVERSE_SERVER_URL; } + QUrl getMetaverseServerURL() { return NetworkingConstants::METAVERSE_SERVER_URL(); } public slots: void requestAccessToken(const QString& login, const QString& password); diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 27d4a31ccf..58a4446aa6 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -127,7 +127,11 @@ void Assignment::swap(Assignment& otherAssignment) { } const char* Assignment::getTypeName() const { - switch (_type) { + return typeToString(_type); +} + +const char* Assignment::typeToString(Assignment::Type type) { + switch (type) { case Assignment::AudioMixerType: return "audio-mixer"; case Assignment::AvatarMixerType: diff --git a/libraries/networking/src/Assignment.h b/libraries/networking/src/Assignment.h index bbec40682f..e958c84d87 100644 --- a/libraries/networking/src/Assignment.h +++ b/libraries/networking/src/Assignment.h @@ -28,6 +28,7 @@ class Assignment : public QObject { public: enum Type : uint8_t { + FirstType = 0, AudioMixerType = 0, AvatarMixerType = 1, AgentType = 2, @@ -89,6 +90,7 @@ public: const QString& getNodeVersion() const { return _nodeVersion; } const char* getTypeName() const; + static const char* typeToString(Assignment::Type type); friend QDebug operator<<(QDebug debug, const Assignment& assignment); friend QDataStream& operator<<(QDataStream &out, const Assignment& assignment); diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 0c210e4360..a4726f9b1a 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -12,15 +12,31 @@ #ifndef hifi_NetworkingConstants_h #define hifi_NetworkingConstants_h +#include #include namespace NetworkingConstants { // If you want to use STAGING instead of STABLE, - // don't forget to ALSO change the Domain Server Metaverse Server URL inside of: + // links from the Domain Server web interface (like the connect account token generation) + // will still point at stable unless you ALSO change the Domain Server Metaverse Server URL inside of: // \domain-server\resources\web\js\shared.js + + // You can avoid changing that and still effectively use a connected domain on staging + // if you manually generate a personal access token for the domains scope + // at https://staging.highfidelity.com/user/tokens/new?for_domain_server=true + const QUrl METAVERSE_SERVER_URL_STABLE("https://metaverse.highfidelity.com"); const QUrl METAVERSE_SERVER_URL_STAGING("https://staging.highfidelity.com"); - const QUrl METAVERSE_SERVER_URL = METAVERSE_SERVER_URL_STABLE; + + // You can change the return of this function if you want to use a custom metaverse URL at compile time + // or you can pass a custom URL via the env variable + static const QUrl METAVERSE_SERVER_URL() { + static const QString HIFI_METAVERSE_URL_ENV = "HIFI_METAVERSE_URL"; + static const QUrl serverURL = QProcessEnvironment::systemEnvironment().contains(HIFI_METAVERSE_URL_ENV) + ? QUrl(QProcessEnvironment::systemEnvironment().value(HIFI_METAVERSE_URL_ENV)) + : METAVERSE_SERVER_URL_STABLE; + return serverURL; + }; } #endif // hifi_NetworkingConstants_h diff --git a/libraries/networking/src/OAuthNetworkAccessManager.cpp b/libraries/networking/src/OAuthNetworkAccessManager.cpp index 15d5acbc67..a30786efa4 100644 --- a/libraries/networking/src/OAuthNetworkAccessManager.cpp +++ b/libraries/networking/src/OAuthNetworkAccessManager.cpp @@ -35,7 +35,7 @@ QNetworkReply* OAuthNetworkAccessManager::createRequest(QNetworkAccessManager::O auto accountManager = DependencyManager::get(); if (accountManager->hasValidAccessToken() - && req.url().host() == NetworkingConstants::METAVERSE_SERVER_URL.host()) { + && req.url().host() == NetworkingConstants::METAVERSE_SERVER_URL().host()) { QNetworkRequest authenticatedRequest(req); authenticatedRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); authenticatedRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index 0965c9834f..aec6df4f14 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -141,3 +141,16 @@ void UserActivityLoggerScriptingInterface::commerceWalletSetupFinished(int times payload["secondsToComplete"] = secondsToComplete; doLogAction("commerceWalletSetupFinished", payload); } + +void UserActivityLoggerScriptingInterface::commercePassphraseEntry(QString source) { + QJsonObject payload; + payload["source"] = source; + doLogAction("commercePassphraseEntry", payload); +} + +void UserActivityLoggerScriptingInterface::commercePassphraseAuthenticationStatus(QString status) { + QJsonObject payload; + payload["status"] = status; + doLogAction("commercePassphraseAuthenticationStatus", payload); + +} diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index e71723f03c..0e08b050d7 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -39,6 +39,8 @@ public: Q_INVOKABLE void commerceWalletSetupStarted(int timestamp, QString setupAttemptID, int setupFlowVersion, QString referrer, QString currentDomain); Q_INVOKABLE void commerceWalletSetupProgress(int timestamp, QString setupAttemptID, int secondsElapsed, int currentStepNumber, QString currentStepName); Q_INVOKABLE void commerceWalletSetupFinished(int timestamp, QString setupAttemptID, int secondsToComplete); + Q_INVOKABLE void commercePassphraseEntry(QString source); + Q_INVOKABLE void commercePassphraseAuthenticationStatus(QString status); private: void doLogAction(QString action, QJsonObject details = {}); }; diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index f42049f107..2f57523f79 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -11,6 +11,8 @@ #include "Connection.h" +#include + #include #include @@ -60,6 +62,15 @@ Connection::Connection(Socket* parentSocket, HifiSockAddr destination, std::uniq _ack2Packet = ControlPacket::create(ControlPacket::ACK2, ACK2_PAYLOAD_BYTES); _lossReport = ControlPacket::create(ControlPacket::NAK, NAK_PACKET_PAYLOAD_BYTES); _handshakeACK = ControlPacket::create(ControlPacket::HandshakeACK, HANDSHAKE_ACK_PAYLOAD_BYTES); + + + // setup psuedo-random number generation shared by all connections + static std::random_device rd; + static std::mt19937 generator(rd()); + static std::uniform_int_distribution<> distribution(0, SequenceNumber::MAX); + + // randomize the intial sequence number + _initialSequenceNumber = SequenceNumber(distribution(generator)); } Connection::~Connection() { @@ -79,11 +90,11 @@ void Connection::stopSendQueue() { // tell the send queue to stop and be deleted sendQueue->stop(); + + _lastMessageNumber = sendQueue->getCurrentMessageNumber(); + sendQueue->deleteLater(); - // since we're stopping the send queue we should consider our handshake ACK not receieved - _hasReceivedHandshakeACK = false; - // wait on the send queue thread so we know the send queue is gone sendQueueThread->quit(); sendQueueThread->wait(); @@ -101,13 +112,19 @@ void Connection::setMaxBandwidth(int maxBandwidth) { SendQueue& Connection::getSendQueue() { if (!_sendQueue) { - // we may have a sequence number from the previous inactive queue - re-use that so that the // receiver is getting the sequence numbers it expects (given that the connection must still be active) // Lasily create send queue - _sendQueue = SendQueue::create(_parentSocket, _destination); - _lastReceivedACK = _sendQueue->getCurrentSequenceNumber(); + + if (!_hasReceivedHandshakeACK) { + // First time creating a send queue for this connection + _sendQueue = SendQueue::create(_parentSocket, _destination, _initialSequenceNumber - 1, _lastMessageNumber, _hasReceivedHandshakeACK); + _lastReceivedACK = _sendQueue->getCurrentSequenceNumber(); + } else { + // Connection already has a handshake from a previous send queue + _sendQueue = SendQueue::create(_parentSocket, _destination, _lastReceivedACK, _lastMessageNumber, _hasReceivedHandshakeACK); + } #ifdef UDT_CONNECTION_DEBUG qCDebug(networking) << "Created SendQueue for connection to" << _destination; @@ -142,14 +159,6 @@ void Connection::queueInactive() { #ifdef UDT_CONNECTION_DEBUG qCDebug(networking) << "Connection to" << _destination << "has stopped its SendQueue."; #endif - - if (!_hasReceivedHandshake || !_isReceivingData) { -#ifdef UDT_CONNECTION_DEBUG - qCDebug(networking) << "Connection SendQueue to" << _destination << "stopped and no data is being received - stopping connection."; -#endif - - deactivate(); - } } void Connection::queueTimeout() { @@ -184,11 +193,16 @@ void Connection::queueReceivedMessagePacket(std::unique_ptr packet) { while (pendingMessage.hasAvailablePackets()) { auto packet = pendingMessage.removeNextPacket(); - _parentSocket->messageReceived(std::move(packet)); - } - if (pendingMessage.isComplete()) { - _pendingReceivedMessages.erase(messageNumber); + auto packetPosition = packet->getPacketPosition(); + + _parentSocket->messageReceived(std::move(packet)); + + // if this was the last or only packet, then we can remove the pending message from our hash + if (packetPosition == Packet::PacketPosition::LAST || + packetPosition == Packet::PacketPosition::ONLY) { + _pendingReceivedMessages.erase(messageNumber); + } } } @@ -208,19 +222,6 @@ void Connection::sync() { && duration_cast(sincePacketReceive).count() >= MIN_SECONDS_BEFORE_EXPIRY ) { // the receive side of this connection is expired _isReceivingData = false; - - // if we don't have a send queue that means the whole connection has expired and we can emit our signal - // otherwise we'll wait for it to also timeout before cleaning up - if (!_sendQueue) { - -#ifdef UDT_CONNECTION_DEBUG - qCDebug(networking) << "Connection to" << _destination << "no longer receiving any data and there is currently no send queue - stopping connection."; -#endif - - deactivate(); - - return; - } } // reset the number of light ACKs or non SYN ACKs during this sync interval @@ -242,26 +243,6 @@ void Connection::sync() { sendTimeoutNAK(); } } - } else if (!_sendQueue) { - // we haven't received a packet and we're not sending - // this most likely means we were started erroneously - // check the start time for this connection and auto expire it after 5 seconds of not receiving or sending any data - static const int CONNECTION_NOT_USED_EXPIRY_SECONDS = 5; - auto secondsSinceStart = duration_cast(p_high_resolution_clock::now() - _connectionStart).count(); - - if (secondsSinceStart >= CONNECTION_NOT_USED_EXPIRY_SECONDS) { - // it's been CONNECTION_NOT_USED_EXPIRY_SECONDS and nothing has actually happened with this connection - // consider it inactive and emit our inactivity signal - -#ifdef UDT_CONNECTION_DEBUG - qCDebug(networking) << "Connection to" << _destination << "did not receive or send any data in last" - << CONNECTION_NOT_USED_EXPIRY_SECONDS << "seconds - stopping connection."; -#endif - - deactivate(); - - return; - } } } @@ -444,7 +425,6 @@ void Connection::sendHandshakeRequest() { } bool Connection::processReceivedSequenceNumber(SequenceNumber sequenceNumber, int packetSize, int payloadSize) { - if (!_hasReceivedHandshake) { // Refuse to process any packets until we've received the handshake // Send handshake request to re-request a handshake @@ -536,7 +516,7 @@ bool Connection::processReceivedSequenceNumber(SequenceNumber sequenceNumber, in } else { _stats.recordReceivedPackets(payloadSize, packetSize); } - + return !wasDuplicate; } @@ -827,11 +807,13 @@ void Connection::processHandshakeACK(ControlPacketPointer controlPacket) { SequenceNumber initialSequenceNumber; controlPacket->readPrimitive(&initialSequenceNumber); - // hand off this handshake ACK to the send queue so it knows it can start sending - getSendQueue().handshakeACK(initialSequenceNumber); - - // indicate that handshake ACK was received - _hasReceivedHandshakeACK = true; + if (initialSequenceNumber == _initialSequenceNumber) { + // hand off this handshake ACK to the send queue so it knows it can start sending + getSendQueue().handshakeACK(); + + // indicate that handshake ACK was received + _hasReceivedHandshakeACK = true; + } } } diff --git a/libraries/networking/src/udt/Connection.h b/libraries/networking/src/udt/Connection.h index c134081dde..0017eb204a 100644 --- a/libraries/networking/src/udt/Connection.h +++ b/libraries/networking/src/udt/Connection.h @@ -37,7 +37,6 @@ class Socket; class PendingReceivedMessage { public: void enqueuePacket(std::unique_ptr packet); - bool isComplete() const { return _hasLastPacket && _numPackets == _packets.size(); } bool hasAvailablePackets() const; std::unique_ptr removeNextPacket(); @@ -72,8 +71,6 @@ public: void queueReceivedMessagePacket(std::unique_ptr packet); ConnectionStats::Stats sampleStats() { return _stats.sample(); } - - bool isActive() const { return _isActive; } HifiSockAddr getDestination() const { return _destination; } @@ -83,7 +80,6 @@ public: signals: void packetSent(); - void connectionInactive(const HifiSockAddr& sockAddr); void receiverHandshakeRequestComplete(const HifiSockAddr& sockAddr); private slots: @@ -112,8 +108,6 @@ private: void resetReceiveState(); void resetRTT(); - void deactivate() { _isActive = false; emit connectionInactive(_destination); } - SendQueue& getSendQueue(); SequenceNumber nextACK() const; void updateRTT(int rtt); @@ -138,9 +132,11 @@ private: p_high_resolution_clock::time_point _lastReceiveTime; // holds the last time we received anything from sender bool _isReceivingData { false }; // flag used for expiry of receipt portion of connection - bool _isActive { true }; // flag used for inactivity of connection - SequenceNumber _initialReceiveSequenceNumber; // Randomized by peer SendQueue on creation, identifies connection during re-connect requests + SequenceNumber _initialSequenceNumber; // Randomized on Connection creation, identifies connection during re-connect requests + SequenceNumber _initialReceiveSequenceNumber; // Randomized by peer Connection on creation, identifies connection during re-connect requests + + MessageNumber _lastMessageNumber { 0 }; LossList _lossList; // List of all missing packets SequenceNumber _lastReceivedSequenceNumber; // The largest sequence number received from the peer diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 207ddf6bbb..62354da11a 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -30,7 +30,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::HazeEffect); + return static_cast(EntityVersion::StaticCertJsonVersionOne); case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::ConnectionIdentifier); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e5cb87c379..618ac2de0c 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -199,7 +199,8 @@ QDebug operator<<(QDebug debug, const PacketType& type); enum class EntityVersion : PacketVersion { StrokeColorProperty = 77, HasDynamicOwnershipTests, - HazeEffect + HazeEffect, + StaticCertJsonVersionOne }; enum class EntityScriptCallMethodVersion : PacketVersion { diff --git a/libraries/networking/src/udt/PacketQueue.cpp b/libraries/networking/src/udt/PacketQueue.cpp index 9560f2f187..0560855ecb 100644 --- a/libraries/networking/src/udt/PacketQueue.cpp +++ b/libraries/networking/src/udt/PacketQueue.cpp @@ -15,7 +15,7 @@ using namespace udt; -PacketQueue::PacketQueue() { +PacketQueue::PacketQueue(MessageNumber messageNumber) : _currentMessageNumber(messageNumber) { _channels.emplace_back(new std::list()); } diff --git a/libraries/networking/src/udt/PacketQueue.h b/libraries/networking/src/udt/PacketQueue.h index 2b3d3a4b5b..bc4c5e3432 100644 --- a/libraries/networking/src/udt/PacketQueue.h +++ b/libraries/networking/src/udt/PacketQueue.h @@ -34,7 +34,7 @@ class PacketQueue { using Channels = std::vector; public: - PacketQueue(); + PacketQueue(MessageNumber messageNumber = 0); void queuePacket(PacketPointer packet); void queuePacketList(PacketListPointer packetList); @@ -42,6 +42,8 @@ public: PacketPointer takePacket(); Mutex& getLock() { return _packetsLock; } + + MessageNumber getCurrentMessageNumber() const { return _currentMessageNumber; } private: MessageNumber getNextMessageNumber(); diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index 0c029751aa..b62624aab9 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -12,7 +12,6 @@ #include "SendQueue.h" #include -#include #include #include @@ -62,10 +61,12 @@ private: Mutex2& _mutex2; }; -std::unique_ptr SendQueue::create(Socket* socket, HifiSockAddr destination) { +std::unique_ptr SendQueue::create(Socket* socket, HifiSockAddr destination, SequenceNumber currentSequenceNumber, + MessageNumber currentMessageNumber, bool hasReceivedHandshakeACK) { Q_ASSERT_X(socket, "SendQueue::create", "Must be called with a valid Socket*"); - auto queue = std::unique_ptr(new SendQueue(socket, destination)); + auto queue = std::unique_ptr(new SendQueue(socket, destination, currentSequenceNumber, + currentMessageNumber, hasReceivedHandshakeACK)); // Setup queue private thread QThread* thread = new QThread; @@ -84,25 +85,18 @@ std::unique_ptr SendQueue::create(Socket* socket, HifiSockAddr destin return queue; } -SendQueue::SendQueue(Socket* socket, HifiSockAddr dest) : +SendQueue::SendQueue(Socket* socket, HifiSockAddr dest, SequenceNumber currentSequenceNumber, + MessageNumber currentMessageNumber, bool hasReceivedHandshakeACK) : + _packets(currentMessageNumber), _socket(socket), _destination(dest) { - // setup psuedo-random number generation for all instances of SendQueue - static std::random_device rd; - static std::mt19937 generator(rd()); - static std::uniform_int_distribution<> distribution(0, SequenceNumber::MAX); - - // randomize the intial sequence number - _initialSequenceNumber = SequenceNumber(distribution(generator)); - - // set our member variables from randomized initial number - _currentSequenceNumber = _initialSequenceNumber - 1; + // set our member variables from current sequence number + _currentSequenceNumber = currentSequenceNumber; _atomicCurrentSequenceNumber = uint32_t(_currentSequenceNumber); - _lastACKSequenceNumber = uint32_t(_currentSequenceNumber) - 1; + _lastACKSequenceNumber = uint32_t(_currentSequenceNumber); - // default the last receiver response to the current time - _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); + _hasReceivedHandshakeACK = hasReceivedHandshakeACK; } SendQueue::~SendQueue() { @@ -114,8 +108,8 @@ void SendQueue::queuePacket(std::unique_ptr packet) { // call notify_one on the condition_variable_any in case the send thread is sleeping waiting for packets _emptyCondition.notify_one(); - if (!this->thread()->isRunning() && _state == State::NotStarted) { - this->thread()->start(); + if (!thread()->isRunning() && _state == State::NotStarted) { + thread()->start(); } } @@ -125,8 +119,8 @@ void SendQueue::queuePacketList(std::unique_ptr packetList) { // call notify_one on the condition_variable_any in case the send thread is sleeping waiting for packets _emptyCondition.notify_one(); - if (!this->thread()->isRunning() && _state == State::NotStarted) { - this->thread()->start(); + if (!thread()->isRunning() && _state == State::NotStarted) { + thread()->start(); } } @@ -144,9 +138,6 @@ int SendQueue::sendPacket(const Packet& packet) { } void SendQueue::ack(SequenceNumber ack) { - // this is a response from the client, re-set our timeout expiry and our last response time - _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); - if (_lastACKSequenceNumber == (uint32_t) ack) { return; } @@ -173,10 +164,7 @@ void SendQueue::ack(SequenceNumber ack) { _emptyCondition.notify_one(); } -void SendQueue::nak(SequenceNumber start, SequenceNumber end) { - // this is a response from the client, re-set our timeout expiry - _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); - +void SendQueue::nak(SequenceNumber start, SequenceNumber end) { { std::lock_guard nakLocker(_naksLock); _naks.insert(start, end); @@ -197,9 +185,6 @@ void SendQueue::fastRetransmit(udt::SequenceNumber ack) { } void SendQueue::overrideNAKListFromPacket(ControlPacket& packet) { - // this is a response from the client, re-set our timeout expiry - _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); - { std::lock_guard nakLocker(_naksLock); _naks.clear(); @@ -225,8 +210,11 @@ void SendQueue::sendHandshake() { std::unique_lock handshakeLock { _handshakeMutex }; if (!_hasReceivedHandshakeACK) { // we haven't received a handshake ACK from the client, send another now + // if the handshake hasn't been completed, then the initial sequence number + // should be the current sequence number + 1 + SequenceNumber initialSequenceNumber = _currentSequenceNumber + 1; auto handshakePacket = ControlPacket::create(ControlPacket::Handshake, sizeof(SequenceNumber)); - handshakePacket->writePrimitive(_initialSequenceNumber); + handshakePacket->writePrimitive(initialSequenceNumber); _socket->writeBasePacket(*handshakePacket, _destination); // we wait for the ACK or the re-send interval to expire @@ -235,18 +223,14 @@ void SendQueue::sendHandshake() { } } -void SendQueue::handshakeACK(SequenceNumber initialSequenceNumber) { - if (initialSequenceNumber == _initialSequenceNumber) { - { - std::lock_guard locker { _handshakeMutex }; - _hasReceivedHandshakeACK = true; - } - - _lastReceiverResponse = QDateTime::currentMSecsSinceEpoch(); - - // Notify on the handshake ACK condition - _handshakeACKCondition.notify_one(); +void SendQueue::handshakeACK() { + { + std::lock_guard locker { _handshakeMutex }; + _hasReceivedHandshakeACK = true; } + + // Notify on the handshake ACK condition + _handshakeACKCondition.notify_one(); } SequenceNumber SendQueue::getNextSequenceNumber() { @@ -540,28 +524,6 @@ bool SendQueue::maybeResendPacket() { bool SendQueue::isInactive(bool attemptedToSendPacket) { // check for connection timeout first - // that will be the case if we have had 16 timeouts since hearing back from the client, and it has been - // at least 5 seconds - static const int NUM_TIMEOUTS_BEFORE_INACTIVE = 16; - static const int MIN_MS_BEFORE_INACTIVE = 5 * 1000; - - auto sinceLastResponse = (QDateTime::currentMSecsSinceEpoch() - _lastReceiverResponse); - - if (sinceLastResponse > 0 && - sinceLastResponse >= int64_t(NUM_TIMEOUTS_BEFORE_INACTIVE * (_estimatedTimeout / USECS_PER_MSEC)) && - sinceLastResponse > MIN_MS_BEFORE_INACTIVE) { - // If the flow window has been full for over CONSIDER_INACTIVE_AFTER, - // then signal the queue is inactive and return so it can be cleaned up - -#ifdef UDT_CONNECTION_DEBUG - qCDebug(networking) << "SendQueue to" << _destination << "reached" << NUM_TIMEOUTS_BEFORE_INACTIVE << "timeouts" - << "and" << MIN_MS_BEFORE_INACTIVE << "milliseconds before receiving any ACK/NAK and is now inactive. Stopping."; -#endif - - deactivate(); - return true; - } - if (!attemptedToSendPacket) { // During our processing above we didn't send any packets diff --git a/libraries/networking/src/udt/SendQueue.h b/libraries/networking/src/udt/SendQueue.h index 484afcb88e..a11aacdb91 100644 --- a/libraries/networking/src/udt/SendQueue.h +++ b/libraries/networking/src/udt/SendQueue.h @@ -50,7 +50,9 @@ public: Stopped }; - static std::unique_ptr create(Socket* socket, HifiSockAddr destination); + static std::unique_ptr create(Socket* socket, HifiSockAddr destination, + SequenceNumber currentSequenceNumber, MessageNumber currentMessageNumber, + bool hasReceivedHandshakeACK); virtual ~SendQueue(); @@ -58,6 +60,7 @@ public: void queuePacketList(std::unique_ptr packetList); SequenceNumber getCurrentSequenceNumber() const { return SequenceNumber(_atomicCurrentSequenceNumber); } + MessageNumber getCurrentMessageNumber() const { return _packets.getCurrentMessageNumber(); } void setFlowWindowSize(int flowWindowSize) { _flowWindowSize = flowWindowSize; } @@ -76,7 +79,7 @@ public slots: void nak(SequenceNumber start, SequenceNumber end); void fastRetransmit(SequenceNumber ack); void overrideNAKListFromPacket(ControlPacket& packet); - void handshakeACK(SequenceNumber initialSequenceNumber); + void handshakeACK(); signals: void packetSent(int wireSize, int payloadSize, SequenceNumber seqNum, p_high_resolution_clock::time_point timePoint); @@ -91,7 +94,8 @@ private slots: void run(); private: - SendQueue(Socket* socket, HifiSockAddr dest); + SendQueue(Socket* socket, HifiSockAddr dest, SequenceNumber currentSequenceNumber, + MessageNumber currentMessageNumber, bool hasReceivedHandshakeACK); SendQueue(SendQueue& other) = delete; SendQueue(SendQueue&& other) = delete; @@ -115,8 +119,6 @@ private: Socket* _socket { nullptr }; // Socket to send packet on HifiSockAddr _destination; // Destination addr - - SequenceNumber _initialSequenceNumber; // Randomized on SendQueue creation, identifies connection during re-connect requests std::atomic _lastACKSequenceNumber { 0 }; // Last ACKed sequence number @@ -128,7 +130,6 @@ private: std::atomic _estimatedTimeout { 0 }; // Estimated timeout, set from CC std::atomic _syncInterval { udt::DEFAULT_SYN_INTERVAL_USECS }; // Sync interval, set from CC - std::atomic _lastReceiverResponse { 0 }; // Timestamp for the last time we got new data from the receiver (ACK/NAK) std::atomic _flowWindowSize { 0 }; // Flow control window size (number of packets that can be on wire) - set from CC diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index a3374a0f47..55643985c8 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -257,9 +257,6 @@ Connection* Socket::findOrCreateConnection(const HifiSockAddr& sockAddr) { congestionControl->setMaxBandwidth(_maxBandwidth); auto connection = std::unique_ptr(new Connection(this, sockAddr, std::move(congestionControl))); - // we queue the connection to cleanup connection in case it asks for it during its own rate control sync - QObject::connect(connection.get(), &Connection::connectionInactive, this, &Socket::cleanupConnection); - // allow higher-level classes to find out when connections have completed a handshake QObject::connect(connection.get(), &Connection::receiverHandshakeRequestComplete, this, &Socket::clientHandshakeRequestComplete); diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 7563122290..3190f25d7e 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -9,10 +9,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifdef _WIN32 -#define _USE_MATH_DEFINES -#endif - #include #include #include diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 7e8b431ceb..a688d521d6 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -14,8 +14,9 @@ #include #include #include -#include #include +#include +#include #include "BulletUtil.h" #include "EntityMotionState.h" @@ -325,6 +326,7 @@ bool EntityMotionState::isCandidateForOwnership() const { } bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { + DETAILED_PROFILE_RANGE(simulation_physics, "CheckOutOfSync"); // NOTE: we only get here if we think we own the simulation assert(_body); @@ -476,6 +478,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { } bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { + DETAILED_PROFILE_RANGE(simulation_physics, "ShouldSend"); // NOTE: we expect _entity and _body to be valid in this context, since shouldSendUpdate() is only called // after doesNotNeedToSendUpdate() returns false and that call should return 'true' if _entity or _body are NULL. assert(_entity); @@ -516,6 +519,7 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { } void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_t step) { + DETAILED_PROFILE_RANGE(simulation_physics, "Send"); assert(_entity); assert(entityTreeIsLocked()); @@ -731,6 +735,7 @@ void EntityMotionState::resetMeasuredBodyAcceleration() { } void EntityMotionState::measureBodyAcceleration() { + DETAILED_PROFILE_RANGE(simulation_physics, "MeasureAccel"); // try to manually measure the true acceleration of the object uint32_t thisStep = ObjectMotionState::getWorldSimulationStep(); uint32_t numSubsteps = thisStep - _lastMeasureStep; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 3e87b9437d..e4ba47e205 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -10,12 +10,14 @@ // +#include "PhysicalEntitySimulation.h" + +#include #include "PhysicsHelpers.h" #include "PhysicsLogging.h" #include "ShapeManager.h" -#include "PhysicalEntitySimulation.h" PhysicalEntitySimulation::PhysicalEntitySimulation() { } @@ -274,20 +276,24 @@ void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotio } void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { + PROFILE_RANGE_EX(simulation_physics, "ChangedEntities", 0x00000000, (uint64_t)motionStates.size()); QMutexLocker lock(&_mutex); // walk the motionStates looking for those that correspond to entities - for (auto stateItr : motionStates) { - ObjectMotionState* state = &(*stateItr); - assert(state); - if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { - EntityMotionState* entityState = static_cast(state); - EntityItemPointer entity = entityState->getEntity(); - assert(entity.get()); - if (entityState->isCandidateForOwnership()) { - _outgoingChanges.insert(entityState); + { + PROFILE_RANGE_EX(simulation_physics, "Filter", 0x00000000, (uint64_t)motionStates.size()); + for (auto stateItr : motionStates) { + ObjectMotionState* state = &(*stateItr); + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { + EntityMotionState* entityState = static_cast(state); + EntityItemPointer entity = entityState->getEntity(); + assert(entity.get()); + if (entityState->isCandidateForOwnership()) { + _outgoingChanges.insert(entityState); + } + _entitiesToSort.insert(entity); } - _entitiesToSort.insert(entity); } } @@ -302,6 +308,7 @@ void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionSta } // look for entities to prune or update + PROFILE_RANGE_EX(simulation_physics, "Prune/Send", 0x00000000, (uint64_t)_outgoingChanges.size()); QSet::iterator stateItr = _outgoingChanges.begin(); while (stateItr != _outgoingChanges.end()) { EntityMotionState* state = *stateItr; diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index a64796308e..fe794772e2 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -11,7 +11,12 @@ #include +#include + +#include + #include +#include #include "CharacterController.h" #include "ObjectMotionState.h" @@ -290,6 +295,7 @@ void PhysicsEngine::stepSimulation() { float timeStep = btMin(dt, MAX_TIMESTEP); if (_myAvatarController) { + DETAILED_PROFILE_RANGE(simulation_physics, "avatarController"); BT_PROFILE("avatarController"); // TODO: move this stuff outside and in front of stepSimulation, because // the updateShapeIfNecessary() call needs info from MyAvatar and should @@ -328,45 +334,107 @@ void PhysicsEngine::stepSimulation() { } } +class CProfileOperator { +public: + CProfileOperator() {} + void recurse(CProfileIterator* itr, QString context) { + // The context string will get too long if we accumulate it properly + //QString newContext = context + QString("/") + itr->Get_Current_Parent_Name(); + // so we use this four-character indentation + QString newContext = context + QString(".../"); + process(itr, newContext); + + // count the children + int32_t numChildren = 0; + itr->First(); + while (!itr->Is_Done()) { + itr->Next(); + ++numChildren; + } + + // recurse the children + if (numChildren > 0) { + // recurse the children + for (int32_t i = 0; i < numChildren; ++i) { + itr->Enter_Child(i); + recurse(itr, newContext); + } + } + // retreat back to parent + itr->Enter_Parent(); + } + virtual void process(CProfileIterator*, QString context) = 0; +}; + +class StatsHarvester : public CProfileOperator { +public: + StatsHarvester() {} + void process(CProfileIterator* itr, QString context) override { + QString name = context + itr->Get_Current_Parent_Name(); + uint64_t time = (uint64_t)((btScalar)MSECS_PER_SECOND * itr->Get_Current_Parent_Total_Time()); + PerformanceTimer::addTimerRecord(name, time); + }; +}; + +class StatsWriter : public CProfileOperator { +public: + StatsWriter(QString filename) : _file(filename) { + _file.open(QFile::WriteOnly); + if (_file.error() != QFileDevice::NoError) { + qCDebug(physics) << "unable to open file " << _file.fileName() << " to save stepSimulation() stats"; + } + } + ~StatsWriter() { + _file.close(); + } + void process(CProfileIterator* itr, QString context) override { + QString name = context + itr->Get_Current_Parent_Name(); + float time = (btScalar)MSECS_PER_SECOND * itr->Get_Current_Parent_Total_Time(); + if (_file.error() == QFileDevice::NoError) { + QTextStream s(&_file); + s << name << " = " << time << "\n"; + } + } +protected: + QFile _file; +}; + void PhysicsEngine::harvestPerformanceStats() { // unfortunately the full context names get too long for our stats presentation format //QString contextName = PerformanceTimer::getContextName(); // TODO: how to show full context name? QString contextName("..."); - CProfileIterator* profileIterator = CProfileManager::Get_Iterator(); - if (profileIterator) { + CProfileIterator* itr = CProfileManager::Get_Iterator(); + if (itr) { // hunt for stepSimulation context - profileIterator->First(); - for (int32_t childIndex = 0; !profileIterator->Is_Done(); ++childIndex) { - if (QString(profileIterator->Get_Current_Name()) == "stepSimulation") { - profileIterator->Enter_Child(childIndex); - recursivelyHarvestPerformanceStats(profileIterator, contextName); + itr->First(); + for (int32_t childIndex = 0; !itr->Is_Done(); ++childIndex) { + if (QString(itr->Get_Current_Name()) == "stepSimulation") { + itr->Enter_Child(childIndex); + StatsHarvester harvester; + harvester.recurse(itr, "step/"); break; } - profileIterator->Next(); + itr->Next(); } } } -void PhysicsEngine::recursivelyHarvestPerformanceStats(CProfileIterator* profileIterator, QString contextName) { - QString parentContextName = contextName + QString("/") + QString(profileIterator->Get_Current_Parent_Name()); - // get the stats for the children - int32_t numChildren = 0; - profileIterator->First(); - while (!profileIterator->Is_Done()) { - QString childContextName = parentContextName + QString("/") + QString(profileIterator->Get_Current_Name()); - uint64_t time = (uint64_t)((btScalar)MSECS_PER_SECOND * profileIterator->Get_Current_Total_Time()); - PerformanceTimer::addTimerRecord(childContextName, time); - profileIterator->Next(); - ++numChildren; +void PhysicsEngine::printPerformanceStatsToFile(const QString& filename) { + CProfileIterator* itr = CProfileManager::Get_Iterator(); + if (itr) { + // hunt for stepSimulation context + itr->First(); + for (int32_t childIndex = 0; !itr->Is_Done(); ++childIndex) { + if (QString(itr->Get_Current_Name()) == "stepSimulation") { + itr->Enter_Child(childIndex); + StatsWriter writer(filename); + writer.recurse(itr, ""); + break; + } + itr->Next(); + } } - // recurse the children - for (int32_t i = 0; i < numChildren; ++i) { - profileIterator->Enter_Child(i); - recursivelyHarvestPerformanceStats(profileIterator, contextName); - } - // retreat back to parent - profileIterator->Enter_Parent(); } void PhysicsEngine::doOwnershipInfection(const btCollisionObject* objectA, const btCollisionObject* objectB) { @@ -399,6 +467,7 @@ void PhysicsEngine::doOwnershipInfection(const btCollisionObject* objectA, const } void PhysicsEngine::updateContactMap() { + DETAILED_PROFILE_RANGE(simulation_physics, "updateContactMap"); BT_PROFILE("updateContactMap"); ++_numContactFrames; @@ -515,10 +584,21 @@ const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() { void PhysicsEngine::dumpStatsIfNecessary() { if (_dumpNextStats) { _dumpNextStats = false; + CProfileManager::Increment_Frame_Counter(); + if (_saveNextStats) { + _saveNextStats = false; + printPerformanceStatsToFile(_statsFilename); + } CProfileManager::dumpAll(); } } +void PhysicsEngine::saveNextPhysicsStats(QString filename) { + _saveNextStats = true; + _dumpNextStats = true; + _statsFilename = filename; +} + // Bullet collision flags are as follows: // CF_STATIC_OBJECT= 1, // CF_KINEMATIC_OBJECT= 2, diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index 3063a4a89a..6619a5489d 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -62,6 +62,7 @@ public: void stepSimulation(); void harvestPerformanceStats(); + void printPerformanceStatsToFile(const QString& filename); void updateContactMap(); bool hasOutgoingChanges() const { return _hasOutgoingChanges; } @@ -76,6 +77,9 @@ public: /// \brief prints timings for last frame if stats have been requested. void dumpStatsIfNecessary(); + /// \brief saves timings for last frame in filename + void saveNextPhysicsStats(QString filename); + /// \param offset position of simulation origin in domain-frame void setOriginOffset(const glm::vec3& offset) { _originOffset = offset; } @@ -94,7 +98,6 @@ public: private: QList removeDynamicsForBody(btRigidBody* body); void addObjectToDynamicsWorld(ObjectMotionState* motionState); - void recursivelyHarvestPerformanceStats(CProfileIterator* profileIterator, QString contextName); /// \brief bump any objects that touch this one, then remove contact info void bumpAndPruneContacts(ObjectMotionState* motionState); @@ -116,6 +119,7 @@ private: QHash _objectDynamics; QHash> _objectDynamicsByBody; std::set _activeStaticBodies; + QString _statsFilename; glm::vec3 _originOffset; @@ -124,8 +128,9 @@ private: uint32_t _numContactFrames = 0; uint32_t _numSubsteps; - bool _dumpNextStats = false; - bool _hasOutgoingChanges = false; + bool _dumpNextStats { false }; + bool _saveNextStats { false }; + bool _hasOutgoingChanges { false }; }; diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 24cfbc2609..5b8c0d5843 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -18,6 +18,7 @@ #include #include "ThreadSafeDynamicsWorld.h" +#include "Profile.h" ThreadSafeDynamicsWorld::ThreadSafeDynamicsWorld( btDispatcher* dispatcher, @@ -29,6 +30,7 @@ ThreadSafeDynamicsWorld::ThreadSafeDynamicsWorld( int ThreadSafeDynamicsWorld::stepSimulationWithSubstepCallback(btScalar timeStep, int maxSubSteps, btScalar fixedTimeStep, SubStepCallback onSubStep) { + DETAILED_PROFILE_RANGE(simulation_physics, "stepWithCB"); BT_PROFILE("stepSimulationWithSubstepCallback"); int subSteps = 0; if (maxSubSteps) { @@ -68,11 +70,13 @@ int ThreadSafeDynamicsWorld::stepSimulationWithSubstepCallback(btScalar timeStep saveKinematicState(fixedTimeStep*clampedSimulationSteps); { + DETAILED_PROFILE_RANGE(simulation_physics, "applyGravity"); BT_PROFILE("applyGravity"); applyGravity(); } for (int i=0;i #include +#include #include #include +#include #include #include "Plugin.h" @@ -203,6 +205,7 @@ public: virtual void cycleDebugOutput() {} void waitForPresent(); + float getAveragePresentTime() { return _movingAveragePresent.average / (float)USECS_PER_MSEC; } // in msec std::function getHUDOperator(); @@ -220,6 +223,8 @@ protected: std::function _hudOperator { std::function() }; + MovingAverage _movingAveragePresent; + private: QMutex _presentMutex; QWaitCondition _presentCondition; diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 02a5496151..fe03ead4e1 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -39,7 +39,7 @@ void DebugDeferredBufferConfig::setMode(int newMode) { emit dirty(); } -enum Slot { +enum TextureSlot { Albedo = 0, Normal, Specular, @@ -56,7 +56,11 @@ enum Slot { AmbientOcclusionBlurred }; - +enum ParamSlot { + CameraCorrection = 0, + DeferredFrameTransform, + ShadowTransform +}; static const std::string DEFAULT_ALBEDO_SHADER { "vec4 getFragmentColor() {" @@ -127,12 +131,14 @@ static const std::string DEFAULT_DEPTH_SHADER { " return vec4(vec3(texture(depthMap, uv).x), 1.0);" " }" }; + static const std::string DEFAULT_LIGHTING_SHADER { "vec4 getFragmentColor() {" " return vec4(pow(texture(lightingMap, uv).xyz, vec3(1.0 / 2.2)), 1.0);" " }" }; -static const std::string DEFAULT_SHADOW_SHADER { + +static const std::string DEFAULT_SHADOW_SHADER{ "uniform sampler2DShadow shadowMap;" "vec4 getFragmentColor() {" " for (int i = 255; i >= 0; --i) {" @@ -145,10 +151,31 @@ static const std::string DEFAULT_SHADOW_SHADER { " }" }; +static const std::string DEFAULT_SHADOW_CASCADE_SHADER{ + "vec3 cascadeColors[4] = vec3[4]( vec3(0,1,0), vec3(0,0,1), vec3(1,0,0), vec3(1) );" + "vec4 getFragmentColor() {" + " DeferredFrameTransform deferredTransform = getDeferredFrameTransform();" + " DeferredFragment frag = unpackDeferredFragment(deferredTransform, uv);" + " vec4 viewPosition = vec4(frag.position.xyz, 1.0);" + " float viewDepth = -viewPosition.z;" + " vec4 worldPosition = getViewInverse() * viewPosition;" + " vec4 cascadeShadowCoords[2];" + " ivec2 cascadeIndices;" + " float cascadeMix = determineShadowCascadesOnPixel(worldPosition, viewDepth, cascadeShadowCoords, cascadeIndices);" + " vec3 firstCascadeColor = cascadeColors[cascadeIndices.x];" + " vec3 secondCascadeColor = cascadeColors[cascadeIndices.x];" + " if (cascadeMix > 0.0 && cascadeIndices.y < getShadowCascadeCount()) {" + " secondCascadeColor = cascadeColors[cascadeIndices.y];" + " }" + " vec3 color = mix(firstCascadeColor, secondCascadeColor, cascadeMix);" + " return vec4(mix(vec3(0.0), color, evalShadowFalloff(viewDepth)), 1.0);" + "}" +}; + static const std::string DEFAULT_LINEAR_DEPTH_SHADER { "vec4 getFragmentColor() {" " return vec4(vec3(1.0 - texture(linearDepthMap, uv).x * 0.01), 1.0);" - " }" + "}" }; static const std::string DEFAULT_HALF_LINEAR_DEPTH_SHADER{ @@ -285,8 +312,13 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_SCATTERING_SHADER; case LightingMode: return DEFAULT_LIGHTING_SHADER; - case ShadowMode: + case ShadowCascade0Mode: + case ShadowCascade1Mode: + case ShadowCascade2Mode: + case ShadowCascade3Mode: return DEFAULT_SHADOW_SHADER; + case ShadowCascadeIndicesMode: + return DEFAULT_SHADOW_CASCADE_SHADER; case LinearDepthMode: return DEFAULT_LINEAR_DEPTH_SHADER; case HalfLinearDepthMode: @@ -353,6 +385,10 @@ const gpu::PipelinePointer& DebugDeferredBuffer::getPipeline(Mode mode, std::str const auto program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding("cameraCorrectionBuffer", CameraCorrection)); + slotBindings.insert(gpu::Shader::Binding("deferredFrameTransformBuffer", DeferredFrameTransform)); + slotBindings.insert(gpu::Shader::Binding("shadowTransformBuffer", ShadowTransform)); + slotBindings.insert(gpu::Shader::Binding("albedoMap", Albedo)); slotBindings.insert(gpu::Shader::Binding("normalMap", Normal)); slotBindings.insert(gpu::Shader::Binding("specularMap", Specular)); @@ -404,6 +440,7 @@ void DebugDeferredBuffer::run(const RenderContextPointer& renderContext, const I auto& linearDepthTarget = inputs.get1(); auto& surfaceGeometryFramebuffer = inputs.get2(); auto& ambientOcclusionFramebuffer = inputs.get3(); + auto& frameTransform = inputs.get4(); gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); @@ -422,8 +459,8 @@ void DebugDeferredBuffer::run(const RenderContextPointer& renderContext, const I // TODO REMOVE: Temporary until UI auto first = _customPipelines.begin()->first; - - batch.setPipeline(getPipeline(_mode, first)); + auto pipeline = getPipeline(_mode, first); + batch.setPipeline(pipeline); if (deferredFramebuffer) { batch.setResourceTexture(Albedo, deferredFramebuffer->getDeferredColorTexture()); @@ -439,7 +476,10 @@ void DebugDeferredBuffer::run(const RenderContextPointer& renderContext, const I auto lightAndShadow = lightStage->getCurrentKeyLightAndShadow(); const auto& globalShadow = lightAndShadow.second; if (globalShadow) { - batch.setResourceTexture(Shadow, globalShadow->map); + const auto cascadeIndex = glm::clamp(_mode - Mode::ShadowCascade0Mode, 0, (int)globalShadow->getCascadeCount() - 1); + batch.setResourceTexture(Shadow, globalShadow->getCascade(cascadeIndex).map); + batch.setUniformBuffer(ShadowTransform, globalShadow->getBuffer()); + batch.setUniformBuffer(DeferredFrameTransform, frameTransform->getFrameTransformBuffer()); } if (linearDepthTarget) { @@ -460,7 +500,6 @@ void DebugDeferredBuffer::run(const RenderContextPointer& renderContext, const I const glm::vec2 topRight(_size.z, _size.w); geometryBuffer->renderQuad(batch, bottomLeft, topRight, color, _geometryId); - batch.setResourceTexture(Albedo, nullptr); batch.setResourceTexture(Normal, nullptr); batch.setResourceTexture(Specular, nullptr); diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index bd5618f5be..8227c4f7a3 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -15,6 +15,7 @@ #include #include +#include "DeferredFrameTransform.h" #include "DeferredFramebuffer.h" #include "SurfaceGeometryPass.h" #include "AmbientOcclusionEffect.h" @@ -37,7 +38,7 @@ signals: class DebugDeferredBuffer { public: - using Inputs = render::VaryingSet4; + using Inputs = render::VaryingSet5; using Config = DebugDeferredBufferConfig; using JobModel = render::Job::ModelI; @@ -64,7 +65,11 @@ protected: LightmapMode, ScatteringMode, LightingMode, - ShadowMode, + ShadowCascade0Mode, + ShadowCascade1Mode, + ShadowCascade2Mode, + ShadowCascade3Mode, + ShadowCascadeIndicesMode, LinearDepthMode, HalfLinearDepthMode, HalfNormalMode, diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index f70daf1e77..de2d41be6b 100644 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -91,6 +91,7 @@ vec3 albedo, vec3 fresnel, float metallic, float roughness <@endfunc@> +<@include Haze.slh@> <@func declareEvalSkyboxGlobalColor(supportScattering)@> @@ -101,8 +102,6 @@ vec3 albedo, vec3 fresnel, float metallic, float roughness <$declareDeferredCurvature()$> <@endif@> -<@include Haze.slh@> - vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec3 fresnel, float metallic, float roughness <@if supportScattering@> @@ -122,7 +121,6 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu color += ambientDiffuse; color += ambientSpecular; - // Directional vec3 directionalDiffuse; vec3 directionalSpecular; evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation @@ -135,52 +133,7 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu // Attenuate the light if haze effect selected if ((hazeParams.hazeMode & HAZE_MODE_IS_KEYLIGHT_ATTENUATED) == HAZE_MODE_IS_KEYLIGHT_ATTENUATED) { - // Directional light attenuation is simulated by assuming the light source is at a fixed height above the - // fragment. This height is where the haze density is reduced by 95% from the haze at the fragment's height - // - // The distance is computed from the height and the directional light orientation - // The distance is limited to height * 1,000, which gives an angle of ~0.057 degrees - - // Get directional light - Light light = getLight(); - vec3 lightDirection = getLightDirection(light); - - // Height at which haze density is reduced by 95% (default set to 2000.0 for safety ,this should never happen) - float height_95p = 2000.0; - if (hazeParams.hazeKeyLightAltitudeFactor > 0.0f) { - height_95p = -log(0.05) / hazeParams.hazeKeyLightAltitudeFactor; - } - - // Note that the sine will always be positive - float sin_pitch = sqrt(1.0 - lightDirection.y * lightDirection.y); - - float distance; - const float minimumSinPitch = 0.001; - if (sin_pitch < minimumSinPitch) { - distance = height_95p / minimumSinPitch; - } else { - distance = height_95p / sin_pitch; - } - - // Position of fragment in world coordinates - vec4 worldFragPos = invViewMat * vec4(position, 0.0); - - // Integration is from the fragment towards the light source - // Note that the haze base reference affects only the haze density as function of altitude - float hazeDensityDistribution = - hazeParams.hazeKeyLightRangeFactor * - exp(-hazeParams.hazeKeyLightAltitudeFactor * (worldFragPos.y - hazeParams.hazeBaseReference)); - - float hazeIntegral = hazeDensityDistribution * distance; - - // Note that t is constant and equal to -log(0.05) - // float t = hazeParams.hazeAltitudeFactor * height_95p; - // hazeIntegral *= (1.0 - exp (-t)) / t; - hazeIntegral *= 0.3171178; - - float hazeAmount = 1.0 - exp(-hazeIntegral); - - color = mix(color, vec3(0.0, 0.0, 0.0), hazeAmount); + color = computeHazeColorKeyLightAttenuation(color, lightDirection, position); } return color; @@ -213,9 +166,6 @@ vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscur } <@endfunc@> - - - <@func declareEvalGlobalLightingAlphaBlended()@> <$declareLightingAmbient(1, 1, 1)$> @@ -233,7 +183,6 @@ vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, fl color += ambientDiffuse; color += ambientSpecular / opacity; - // Directional vec3 directionalDiffuse; vec3 directionalSpecular; @@ -244,6 +193,43 @@ vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, fl return color; } +vec3 evalGlobalLightingAlphaBlendedWithHaze( + mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, + vec3 albedo, vec3 fresnel, float metallic, vec3 emissive, float roughness, float opacity) +{ + <$prepareGlobalLight()$> + + color += emissive * isEmissiveEnabled(); + + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbient(ambientDiffuse, ambientSpecular, lightAmbient, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, obscurance); + color += ambientDiffuse; + color += ambientSpecular / opacity; + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, fragEyeDir, fragNormal, roughness, metallic, fresnel, albedo, shadowAttenuation); + color += directionalDiffuse; + color += directionalSpecular / opacity; + + // Haze + if ((hazeParams.hazeMode & HAZE_MODE_IS_ACTIVE) == HAZE_MODE_IS_ACTIVE) { + vec4 colorV4 = computeHazeColor( + vec4(color, 1.0), // fragment original color + position, // fragment position in eye coordinates + fragEyeVector, // fragment position in world coordinates + invViewMat[3].y, // eye height in world coordinates + lightDirection // keylight direction vector + ); + + color = colorV4.rgb; + } + + return color; +} <@endfunc@> diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 646b19198b..3286531643 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -58,7 +58,7 @@ enum DeferredShader_MapSlot { DEFERRED_BUFFER_DEPTH_UNIT = 3, DEFERRED_BUFFER_OBSCURANCE_UNIT = 4, SHADOW_MAP_UNIT = 5, - SKYBOX_MAP_UNIT = 6, + SKYBOX_MAP_UNIT = SHADOW_MAP_UNIT + SHADOW_CASCADE_MAX_COUNT, DEFERRED_BUFFER_LINEAR_DEPTH_UNIT, DEFERRED_BUFFER_CURVATURE_UNIT, DEFERRED_BUFFER_DIFFUSED_CURVATURE_UNIT, @@ -156,7 +156,7 @@ static gpu::ShaderPointer makeLightProgram(const char* vertSource, const char* f slotBindings.insert(gpu::Shader::Binding(std::string("specularMap"), DEFERRED_BUFFER_EMISSIVE_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), DEFERRED_BUFFER_DEPTH_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("obscuranceMap"), DEFERRED_BUFFER_OBSCURANCE_UNIT)); - slotBindings.insert(gpu::Shader::Binding(std::string("shadowMap"), SHADOW_MAP_UNIT)); + slotBindings.insert(gpu::Shader::Binding(std::string("shadowMaps"), SHADOW_MAP_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), SKYBOX_MAP_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("linearZeyeMap"), DEFERRED_BUFFER_LINEAR_DEPTH_UNIT)); @@ -501,9 +501,11 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, auto lightAndShadow = lightStage->getCurrentKeyLightAndShadow(); const auto& globalShadow = lightAndShadow.second; - // Bind the shadow buffer + // Bind the shadow buffers if (globalShadow) { - batch.setResourceTexture(SHADOW_MAP_UNIT, globalShadow->map); + for (unsigned int i = 0; i < globalShadow->getCascadeCount(); i++) { + batch.setResourceTexture(SHADOW_MAP_UNIT+i, globalShadow->getCascade(i).map); + } } auto& program = deferredLightingEffect->_directionalSkyboxLight; @@ -567,8 +569,9 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, deferredLightingEffect->unsetKeyLightBatch(batch, locations->lightBufferUnit, locations->ambientBufferUnit, SKYBOX_MAP_UNIT); - - batch.setResourceTexture(SHADOW_MAP_UNIT, nullptr); + for (auto i = 0; i < SHADOW_CASCADE_MAX_COUNT; i++) { + batch.setResourceTexture(SHADOW_MAP_UNIT+i, nullptr); + } } } diff --git a/libraries/render-utils/src/DrawHaze.cpp b/libraries/render-utils/src/DrawHaze.cpp index 4431c1bbc3..da07f5bd9b 100644 --- a/libraries/render-utils/src/DrawHaze.cpp +++ b/libraries/render-utils/src/DrawHaze.cpp @@ -169,7 +169,12 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu auto hazeStage = args->_scene->getStage(); if (hazeStage && hazeStage->_currentFrame._hazes.size() > 0) { model::HazePointer hazePointer = hazeStage->getHaze(hazeStage->_currentFrame._hazes.front()); - batch.setUniformBuffer(HazeEffect_ParamsSlot, hazePointer->getHazeParametersBuffer()); + if (hazePointer) { + batch.setUniformBuffer(HazeEffect_ParamsSlot, hazePointer->getHazeParametersBuffer()); + } else { + // Something is wrong, so just quit Haze + return; + } } batch.setUniformBuffer(HazeEffect_TransformBufferSlot, transformBuffer->getFrameTransformBuffer()); @@ -178,7 +183,7 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu if (lightStage) { model::LightPointer keyLight; keyLight = lightStage->getCurrentKeyLight(); - if (keyLight != nullptr) { + if (keyLight) { batch.setUniformBuffer(HazeEffect_LightingMapSlot, keyLight->getLightSchemaBuffer()); } } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index ebf0f13d97..76c354bdf8 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -74,6 +74,32 @@ static std::array MAPPING GeometryCache::Cylinder, } }; +/**jsdoc +*

{@link Entities} and {@link Overlays} may have the following geometrical shapes:

+* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +* +*
ValueDescription
LineA 1D line oriented in 3 dimensions.
TriangleA triangular prism.
QuadA 2D square oriented in 3 dimensions.
HexagonA hexagonal prism.
OctagonAn octagonal prism.
CircleA 2D circle oriented in 3 dimensions.
CubeA cube.
SphereA sphere.
TetrahedronA tetrahedron.
OctahedronAn octahedron.
DodecahedronA dodecahedron.
IcosahedronAn icosahedron.
TorusA torus. Not implemented.
ConeA cone.
CylinderA cylinder.
+* @typedef {string} Shape +*/ static const std::array GEOCACHE_SHAPE_STRINGS{ { "Line", "Triangle", diff --git a/libraries/render-utils/src/Haze.slf b/libraries/render-utils/src/Haze.slf index b366e6d639..0270aa58f0 100644 --- a/libraries/render-utils/src/Haze.slf +++ b/libraries/render-utils/src/Haze.slf @@ -23,6 +23,7 @@ <@include Haze.slh@> uniform sampler2D colorMap; +uniform sampler2D linearDepthMap; vec4 unpackPositionFromZeye(vec2 texcoord) { float Zeye = -texture(linearDepthMap, texcoord).x; @@ -45,104 +46,15 @@ void main(void) { discard; } - // Distance to fragment - vec4 eyeFragPos = unpackPositionFromZeye(varTexCoord0); - float distance = length(eyeFragPos.xyz); - vec4 fragColor = texture(colorMap, varTexCoord0); - vec4 hazeColor = vec4(hazeParams.hazeColor, 1.0); + vec4 eyeFragPos = unpackPositionFromZeye(varTexCoord0); - // Directional light component is a function of the angle from the eye, between the fragment and the sun - DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); - vec4 worldFragPos = getViewInverse() * eyeFragPos; - vec3 eyeFragDir = normalize(worldFragPos.xyz); + mat4 viewInverse = getViewInverse(); + vec4 worldFragPos = viewInverse * eyeFragPos; + vec4 worldEyePos = viewInverse[3]; Light light = getLight(); vec3 lightDirection = getLightDirection(light); - float glareComponent = max(0.0, dot(eyeFragDir, -lightDirection)); - float power = min(1.0, pow(glareComponent, hazeParams.hazeGlareBlend)); - - vec4 glareColor = vec4(hazeParams.hazeGlareColor, 1.0); - - // Use the haze colour for the glare colour, if blend is not enabled - vec4 blendedHazeColor; - if ((hazeParams.hazeMode & HAZE_MODE_IS_ENABLE_LIGHT_BLEND) == HAZE_MODE_IS_ENABLE_LIGHT_BLEND) { - blendedHazeColor = mix(hazeColor, glareColor, power); - } else { - blendedHazeColor = hazeColor; - } - - vec4 potentialFragColor; - - if ((hazeParams.hazeMode & HAZE_MODE_IS_MODULATE_COLOR) == HAZE_MODE_IS_MODULATE_COLOR) { - // Compute separately for each colour - // Haze is based on both range and altitude - // Taken from www.crytek.com/download/GDC2007_RealtimeAtmoFxInGamesRev.ppt - - // The eyepoint position is in the last column of the matrix - vec3 worldEyePos = getViewInverse()[3].xyz; - - // Note that the haze base reference affects only the haze density as function of altitude - vec3 hazeDensityDistribution = - hazeParams.colorModulationFactor * - exp(-hazeParams.hazeHeightFactor * (worldEyePos.y - hazeParams.hazeBaseReference)); - - vec3 hazeIntegral = hazeDensityDistribution * distance; - - const float slopeThreshold = 0.01; - float deltaHeight = worldFragPos.y - worldEyePos.y; - if (abs(deltaHeight) > slopeThreshold) { - float t = hazeParams.hazeHeightFactor * deltaHeight; - hazeIntegral *= (1.0 - exp (-t)) / t; - } - - vec3 hazeAmount = 1.0 - exp(-hazeIntegral); - - // Compute color after haze effect - potentialFragColor = mix(fragColor, vec4(1.0, 1.0, 1.0, 1.0), vec4(hazeAmount, 1.0)); - } else if ((hazeParams.hazeMode & HAZE_MODE_IS_ALTITUDE_BASED) != HAZE_MODE_IS_ALTITUDE_BASED) { - // Haze is based only on range - float hazeAmount = 1.0 - exp(-distance * hazeParams.hazeRangeFactor); - - // Compute color after haze effect - potentialFragColor = mix(fragColor, blendedHazeColor, hazeAmount); - } else { - // Haze is based on both range and altitude - // Taken from www.crytek.com/download/GDC2007_RealtimeAtmoFxInGamesRev.ppt - - // The eyepoint position is in the last column of the matrix - vec3 worldEyePos = getViewInverse()[3].xyz; - - // Note that the haze base reference affects only the haze density as function of altitude - float hazeDensityDistribution = - hazeParams.hazeRangeFactor * - exp(-hazeParams.hazeHeightFactor * (worldEyePos.y - hazeParams.hazeBaseReference)); - - float hazeIntegral = hazeDensityDistribution * distance; - - const float slopeThreshold = 0.01; - float deltaHeight = worldFragPos.y - worldEyePos.y; - if (abs(deltaHeight) > slopeThreshold) { - float t = hazeParams.hazeHeightFactor * deltaHeight; - // Protect from wild values - if (abs(t) > 0.0000001) { - hazeIntegral *= (1.0 - exp (-t)) / t; - } - } - - float hazeAmount = 1.0 - exp(-hazeIntegral); - - // Compute color after haze effect - potentialFragColor = mix(fragColor, blendedHazeColor, hazeAmount); - } - - // Mix with background at far range - const float BLEND_DISTANCE = 27000.0; - if (distance > BLEND_DISTANCE) { - outFragColor = mix(potentialFragColor, fragColor, hazeParams.backgroundBlend); - } else { - outFragColor = potentialFragColor; - } + outFragColor = computeHazeColor(fragColor, eyeFragPos.xyz, worldFragPos.xyz, worldEyePos.y, lightDirection); } - diff --git a/libraries/render-utils/src/Haze.slh b/libraries/render-utils/src/Haze.slh index de7f0ac246..15e484e055 100644 --- a/libraries/render-utils/src/Haze.slh +++ b/libraries/render-utils/src/Haze.slh @@ -40,7 +40,160 @@ layout(std140) uniform hazeBuffer { HazeParams hazeParams; }; -uniform sampler2D linearDepthMap; + +// Input: +// color - fragment original color +// lightDirection - parameters of the keylight +// worldFragPos - fragment position in world coordinates +// Output: +// fragment colour after haze effect +// +// General algorithm taken from http://www.iquilezles.org/www/articles/fog/fog.htm, with permission +// +vec3 computeHazeColorKeyLightAttenuation(vec3 color, vec3 lightDirection, vec3 worldFragPos) { + // Directional light attenuation is simulated by assuming the light source is at a fixed height above the + // fragment. This height is where the haze density is reduced by 95% from the haze at the fragment's height + // + // The distance is computed from the height and the directional light orientation + // The distance is limited to height * 1,000, which gives an angle of ~0.057 degrees + + // Height at which haze density is reduced by 95% (default set to 2000.0 for safety ,this should never happen) + float height_95p = 2000.0; + const float log_p_005 = log(0.05); + if (hazeParams.hazeKeyLightAltitudeFactor > 0.0f) { + height_95p = -log_p_005 / hazeParams.hazeKeyLightAltitudeFactor; + } + + // Note that we need the sine to be positive + float sin_pitch = abs(lightDirection.y); + + float distance; + const float minimumSinPitch = 0.001; + if (sin_pitch < minimumSinPitch) { + distance = height_95p / minimumSinPitch; + } else { + distance = height_95p / sin_pitch; + } + + // Integration is from the fragment towards the light source + // Note that the haze base reference affects only the haze density as function of altitude + float hazeDensityDistribution = + hazeParams.hazeKeyLightRangeFactor * + exp(-hazeParams.hazeKeyLightAltitudeFactor * (worldFragPos.y - hazeParams.hazeBaseReference)); + + float hazeIntegral = hazeDensityDistribution * distance; + + // Note that t is constant and equal to -log(0.05) + // float t = hazeParams.hazeAltitudeFactor * height_95p; + // hazeIntegral *= (1.0 - exp (-t)) / t; + hazeIntegral *= 0.3171178; + + return color * exp(-hazeIntegral); +} + +// Input: +// fragColor - fragment original color +// eyeFragPos - fragment position in eye coordinates +// worldFragPos - fragment position in world coordinates +// worldEyeHeight - eye height in world coordinates +// Output: +// fragment colour after haze effect +// +// General algorithm taken from http://www.iquilezles.org/www/articles/fog/fog.htm, with permission +// +vec4 computeHazeColor(vec4 fragColor, vec3 eyeFragPos, vec3 worldFragPos, float worldEyeHeight, vec3 lightDirection) { + // Distance to fragment + float distance = length(eyeFragPos); + + // Convert haze colour from uniform into a vec4 + vec4 hazeColor = vec4(hazeParams.hazeColor, 1.0); + + // Directional light component is a function of the angle from the eye, between the fragment and the sun + vec3 eyeFragDir = normalize(worldFragPos); + + float glareComponent = max(0.0, dot(eyeFragDir, -lightDirection)); + float power = min(1.0, pow(glareComponent, hazeParams.hazeGlareBlend)); + + vec4 glareColor = vec4(hazeParams.hazeGlareColor, 1.0); + + // Use the haze colour for the glare colour, if blend is not enabled + vec4 blendedHazeColor; + if ((hazeParams.hazeMode & HAZE_MODE_IS_ENABLE_LIGHT_BLEND) == HAZE_MODE_IS_ENABLE_LIGHT_BLEND) { + blendedHazeColor = mix(hazeColor, glareColor, power); + } else { + blendedHazeColor = hazeColor; + } + + vec4 potentialFragColor; + + if ((hazeParams.hazeMode & HAZE_MODE_IS_MODULATE_COLOR) == HAZE_MODE_IS_MODULATE_COLOR) { + // Compute separately for each colour + // Haze is based on both range and altitude + // Taken from www.crytek.com/download/GDC2007_RealtimeAtmoFxInGamesRev.ppt + + // Note that the haze base reference affects only the haze density as function of altitude + vec3 hazeDensityDistribution = + hazeParams.colorModulationFactor * + exp(-hazeParams.hazeHeightFactor * (worldEyeHeight - hazeParams.hazeBaseReference)); + + vec3 hazeIntegral = hazeDensityDistribution * distance; + + const float slopeThreshold = 0.01; + float deltaHeight = worldFragPos.y - worldEyeHeight; + if (abs(deltaHeight) > slopeThreshold) { + float t = hazeParams.hazeHeightFactor * deltaHeight; + hazeIntegral *= (1.0 - exp (-t)) / t; + } + + vec3 hazeAmount = 1.0 - exp(-hazeIntegral); + + // Compute color after haze effect + potentialFragColor = mix(fragColor, vec4(1.0, 1.0, 1.0, 1.0), vec4(hazeAmount, 1.0)); + } else if ((hazeParams.hazeMode & HAZE_MODE_IS_ALTITUDE_BASED) != HAZE_MODE_IS_ALTITUDE_BASED) { + // Haze is based only on range + float hazeAmount = 1.0 - exp(-distance * hazeParams.hazeRangeFactor); + + // Compute color after haze effect + potentialFragColor = mix(fragColor, blendedHazeColor, hazeAmount); + } else { + // Haze is based on both range and altitude + // Taken from www.crytek.com/download/GDC2007_RealtimeAtmoFxInGamesRev.ppt + + // Note that the haze base reference affects only the haze density as function of altitude + float hazeDensityDistribution = + hazeParams.hazeRangeFactor * + exp(-hazeParams.hazeHeightFactor * (worldEyeHeight - hazeParams.hazeBaseReference)); + + float hazeIntegral = hazeDensityDistribution * distance; + + const float slopeThreshold = 0.01; + float deltaHeight = worldFragPos.y - worldEyeHeight; + if (abs(deltaHeight) > slopeThreshold) { + float t = hazeParams.hazeHeightFactor * deltaHeight; + // Protect from wild values + const float EPSILON = 0.0000001f; + if (abs(t) > EPSILON) { + hazeIntegral *= (1.0 - exp (-t)) / t; + } + } + + float hazeAmount = 1.0 - exp(-hazeIntegral); + + // Compute color after haze effect + potentialFragColor = mix(fragColor, blendedHazeColor, hazeAmount); + } + + // Mix with background at far range + const float BLEND_DISTANCE = 27000.0f; + vec4 outFragColor; + if (distance > BLEND_DISTANCE) { + outFragColor = mix(potentialFragColor, fragColor, hazeParams.backgroundBlend); + } else { + outFragColor = potentialFragColor; + } + + return outFragColor; +} <@endif@> diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index bf2dce85e8..e568554452 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -13,65 +13,202 @@ #include "LightStage.h" +#include + std::string LightStage::_stageName { "LIGHT_STAGE"}; +const glm::mat4 LightStage::Shadow::_biasMatrix{ + 0.5, 0.0, 0.0, 0.0, + 0.0, 0.5, 0.0, 0.0, + 0.0, 0.0, 0.5, 0.0, + 0.5, 0.5, 0.5, 1.0 }; +const int LightStage::Shadow::MAP_SIZE = 1024; + +static const auto MAX_BIAS = 0.006f; + const LightStage::Index LightStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX }; LightStage::LightStage() { } -LightStage::Shadow::Schema::Schema() : - bias{ 0.005f }, - scale{ 1.0f / MAP_SIZE } { - +LightStage::Shadow::Schema::Schema() { + ShadowTransform defaultTransform; + defaultTransform.bias = MAX_BIAS; + std::fill(cascades, cascades + SHADOW_CASCADE_MAX_COUNT, defaultTransform); + invMapSize = 1.0f / MAP_SIZE; + cascadeCount = 1; + invCascadeBlendWidth = 1.0f / 0.2f; + invFalloffDistance = 1.0f / 2.0f; + maxDistance = 20.0f; } -gpu::FramebufferPointer LightStage::Shadow::framebuffer; -gpu::TexturePointer LightStage::Shadow::map; +LightStage::Shadow::Cascade::Cascade() : + _frustum{ std::make_shared() }, + _minDistance{ 0.0f }, + _maxDistance{ 20.0f } { + framebuffer = gpu::FramebufferPointer(gpu::Framebuffer::createShadowmap(MAP_SIZE)); + map = framebuffer->getDepthStencilBuffer(); +} -LightStage::Shadow::Shadow(model::LightPointer light) : _light{ light}, _frustum{ std::make_shared() } { - Schema schema; - _schemaBuffer = std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema); +const glm::mat4& LightStage::Shadow::Cascade::getView() const { + return _frustum->getView(); +} - if (!framebuffer) { - framebuffer = gpu::FramebufferPointer(gpu::Framebuffer::createShadowmap(MAP_SIZE)); - map = framebuffer->getDepthStencilBuffer(); +const glm::mat4& LightStage::Shadow::Cascade::getProjection() const { + return _frustum->getProjection(); +} + +float LightStage::Shadow::Cascade::computeFarDistance(const ViewFrustum& viewFrustum, const Transform& shadowViewInverse, + float left, float right, float bottom, float top, float viewMaxShadowDistance) const { + // Far distance should be extended to the intersection of the infinitely extruded shadow frustum + // with the view frustum side planes. To do so, we generate 10 triangles in shadow space which are the result of + // tesselating the side and far faces of the view frustum and clip them with the 4 side planes of the + // shadow frustum. The resulting clipped triangle vertices with the farthest Z gives the desired + // shadow frustum far distance. + std::array viewFrustumTriangles; + Plane shadowClipPlanes[4] = { + Plane(glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(0.0f, top, 0.0f)), + Plane(glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, bottom, 0.0f)), + Plane(glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(left, 0.0f, 0.0f)), + Plane(glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(right, 0.0f, 0.0f)) + }; + + viewFrustum.tesselateSidesAndFar(shadowViewInverse, viewFrustumTriangles.data(), viewMaxShadowDistance); + + static const int MAX_TRIANGLE_COUNT = 16; + auto far = 0.0f; + + for (auto& triangle : viewFrustumTriangles) { + Triangle clippedTriangles[MAX_TRIANGLE_COUNT]; + auto clippedTriangleCount = clipTriangleWithPlanes(triangle, shadowClipPlanes, 4, clippedTriangles, MAX_TRIANGLE_COUNT); + + for (auto i = 0; i < clippedTriangleCount; i++) { + const auto& clippedTriangle = clippedTriangles[i]; + far = glm::max(far, -clippedTriangle.v0.z); + far = glm::max(far, -clippedTriangle.v1.z); + far = glm::max(far, -clippedTriangle.v2.z); + } } + + return far; } -void LightStage::Shadow::setKeylightFrustum(const ViewFrustum& viewFrustum, - float viewMinShadowDistance, float viewMaxShadowDistance, - float nearDepth, float farDepth) { - assert(viewMinShadowDistance < viewMaxShadowDistance); - assert(nearDepth < farDepth); +LightStage::Shadow::Shadow(model::LightPointer light, float maxDistance, unsigned int cascadeCount) : + _light{ light } { + cascadeCount = std::min(cascadeCount, (unsigned int)SHADOW_CASCADE_MAX_COUNT); + Schema schema; + schema.cascadeCount = cascadeCount; + _schemaBuffer = std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema); + _cascades.resize(cascadeCount); + setMaxDistance(maxDistance); +} + +void LightStage::Shadow::setMaxDistance(float value) { + // This overlaping factor isn't really used directly for blending of shadow cascades. It + // just there to be sure the cascades do overlap. The blending width used is relative + // to the UV space and is set in the Schema with invCascadeBlendWidth. + static const auto OVERLAP_FACTOR = 1.0f / 5.0f; + + _maxDistance = std::max(0.0f, value); + + if (_cascades.size() == 1) { + _cascades.front().setMinDistance(0.0f); + _cascades.front().setMaxDistance(_maxDistance); + } else { + // Distribute the cascades along that distance + // TODO : these parameters should be exposed to the user as part of the light entity parameters, no? + static const auto LOW_MAX_DISTANCE = 2.0f; + static const auto MAX_RESOLUTION_LOSS = 0.6f; // Between 0 and 1, 0 giving tighter distributions + + // The max cascade distance is computed by multiplying the previous cascade's max distance by a certain + // factor. There is a "user" factor that is computed from a desired max resolution loss in the shadow + // and an optimal one based on the global min and max shadow distance, all cascades considered. The final + // distance is a gradual blend between the two + const auto userDistanceScale = 1.0f / (1.0f - MAX_RESOLUTION_LOSS); + const auto optimalDistanceScale = powf(_maxDistance / LOW_MAX_DISTANCE, 1.0f / (_cascades.size() - 1)); + + float maxCascadeUserDistance = LOW_MAX_DISTANCE; + float maxCascadeOptimalDistance = LOW_MAX_DISTANCE; + float minCascadeDistance = 0.0f; + + for (size_t cascadeIndex = 0; cascadeIndex < _cascades.size(); ++cascadeIndex) { + float blendFactor = cascadeIndex / float(_cascades.size() - 1); + float maxCascadeDistance; + + if (cascadeIndex == size_t(_cascades.size() - 1)) { + maxCascadeDistance = _maxDistance; + } else { + maxCascadeDistance = maxCascadeUserDistance + (maxCascadeOptimalDistance - maxCascadeUserDistance)*blendFactor*blendFactor; + } + + float shadowOverlapDistance = maxCascadeDistance * OVERLAP_FACTOR; + + _cascades[cascadeIndex].setMinDistance(minCascadeDistance); + _cascades[cascadeIndex].setMaxDistance(maxCascadeDistance + shadowOverlapDistance); + + // Compute distances for next cascade + minCascadeDistance = maxCascadeDistance; + maxCascadeUserDistance = maxCascadeUserDistance * userDistanceScale; + maxCascadeOptimalDistance = maxCascadeOptimalDistance * optimalDistanceScale; + maxCascadeUserDistance = std::min(maxCascadeUserDistance, _maxDistance); + } + } + + // Update the buffer + const auto& lastCascade = _cascades.back(); + auto& schema = _schemaBuffer.edit(); + schema.maxDistance = _maxDistance; + schema.invFalloffDistance = 1.0f / (OVERLAP_FACTOR*lastCascade.getMaxDistance()); +} + +void LightStage::Shadow::setKeylightFrustum(const ViewFrustum& viewFrustum, + float nearDepth, float farDepth) { + assert(nearDepth < farDepth); // Orient the keylight frustum - const auto& direction = glm::normalize(_light->getDirection()); + auto lightDirection = glm::normalize(_light->getDirection()); glm::quat orientation; - if (direction == IDENTITY_UP) { + if (lightDirection == IDENTITY_UP) { orientation = glm::quat(glm::mat3(-IDENTITY_RIGHT, IDENTITY_FORWARD, -IDENTITY_UP)); - } else if (direction == -IDENTITY_UP) { + } else if (lightDirection == -IDENTITY_UP) { orientation = glm::quat(glm::mat3(IDENTITY_RIGHT, IDENTITY_FORWARD, IDENTITY_UP)); } else { - auto side = glm::normalize(glm::cross(direction, IDENTITY_UP)); - auto up = glm::normalize(glm::cross(side, direction)); - orientation = glm::quat_cast(glm::mat3(side, up, -direction)); + auto side = glm::normalize(glm::cross(lightDirection, IDENTITY_UP)); + auto up = glm::normalize(glm::cross(side, lightDirection)); + orientation = glm::quat_cast(glm::mat3(side, up, -lightDirection)); } - _frustum->setOrientation(orientation); // Position the keylight frustum - _frustum->setPosition(viewFrustum.getPosition() - (nearDepth + farDepth)*direction); + auto position = viewFrustum.getPosition() - (nearDepth + farDepth)*lightDirection; + for (auto& cascade : _cascades) { + cascade._frustum->setOrientation(orientation); + cascade._frustum->setPosition(position); + } + // Update the buffer + auto& schema = _schemaBuffer.edit(); + schema.lightDirInViewSpace = glm::inverse(viewFrustum.getView()) * glm::vec4(lightDirection, 0.f); +} - const Transform view{ _frustum->getView()}; - const Transform viewInverse{ view.getInverseMatrix() }; +void LightStage::Shadow::setKeylightCascadeFrustum(unsigned int cascadeIndex, const ViewFrustum& viewFrustum, + float nearDepth, float farDepth) { + assert(nearDepth < farDepth); + assert(cascadeIndex < _cascades.size()); - auto nearCorners = viewFrustum.getCorners(viewMinShadowDistance); - auto farCorners = viewFrustum.getCorners(viewMaxShadowDistance); + auto& cascade = _cascades[cascadeIndex]; + const auto viewMinCascadeShadowDistance = std::max(viewFrustum.getNearClip(), cascade.getMinDistance()); + const auto viewMaxCascadeShadowDistance = std::min(viewFrustum.getFarClip(), cascade.getMaxDistance()); + const auto viewMaxShadowDistance = _cascades.back().getMaxDistance(); - vec3 min{ viewInverse.transform(nearCorners.bottomLeft) }; + const Transform shadowView{ cascade._frustum->getView()}; + const Transform shadowViewInverse{ shadowView.getInverseMatrix() }; + + auto nearCorners = viewFrustum.getCorners(viewMinCascadeShadowDistance); + auto farCorners = viewFrustum.getCorners(viewMaxCascadeShadowDistance); + + vec3 min{ shadowViewInverse.transform(nearCorners.bottomLeft) }; vec3 max{ min }; // Expand keylight frustum to fit view frustum - auto fitFrustum = [&min, &max, &viewInverse](const vec3& viewCorner) { - const auto corner = viewInverse.transform(viewCorner); + auto fitFrustum = [&min, &max, &shadowViewInverse](const vec3& viewCorner) { + const auto corner = shadowViewInverse.transform(viewCorner); min.x = glm::min(min.x, corner.x); min.y = glm::min(min.y, corner.y); @@ -89,36 +226,35 @@ void LightStage::Shadow::setKeylightFrustum(const ViewFrustum& viewFrustum, fitFrustum(farCorners.topLeft); fitFrustum(farCorners.topRight); - // Re-adjust near shadow distance - auto near = glm::max(max.z, -nearDepth); - auto far = -min.z; + // Re-adjust near and far shadow distance + auto near = glm::min(-max.z, nearDepth); + auto far = cascade.computeFarDistance(viewFrustum, shadowViewInverse, min.x, max.x, min.y, max.y, viewMaxShadowDistance); + glm::mat4 ortho = glm::ortho(min.x, max.x, min.y, max.y, near, far); - _frustum->setProjection(ortho); + cascade._frustum->setProjection(ortho); // Calculate the frustum's internal state - _frustum->calculate(); + cascade._frustum->calculate(); // Update the buffer - _schemaBuffer.edit().projection = ortho; - _schemaBuffer.edit().viewInverse = viewInverse.getMatrix(); + auto& schema = _schemaBuffer.edit(); + schema.cascades[cascadeIndex].reprojection = _biasMatrix * ortho * shadowViewInverse.getMatrix(); + // Adapt shadow bias to shadow resolution with a totally empirical formula + const auto maxShadowFrustumDim = std::max(fabsf(min.x - max.x), fabsf(min.y - max.y)); + const auto REFERENCE_TEXEL_DENSITY = 7.5f; + const auto cascadeTexelDensity = MAP_SIZE / maxShadowFrustumDim; + schema.cascades[cascadeIndex].bias = MAX_BIAS * std::min(1.0f, REFERENCE_TEXEL_DENSITY / cascadeTexelDensity); } -void LightStage::Shadow::setFrustum(const ViewFrustum& shadowFrustum) { +void LightStage::Shadow::setCascadeFrustum(unsigned int cascadeIndex, const ViewFrustum& shadowFrustum) { + assert(cascadeIndex < _cascades.size()); const Transform view{ shadowFrustum.getView() }; const Transform viewInverse{ view.getInverseMatrix() }; + auto& cascade = _cascades[cascadeIndex]; - *_frustum = shadowFrustum; + *cascade._frustum = shadowFrustum; // Update the buffer - _schemaBuffer.edit().projection = shadowFrustum.getProjection(); - _schemaBuffer.edit().viewInverse = viewInverse.getMatrix(); -} - -const glm::mat4& LightStage::Shadow::getView() const { - return _frustum->getView(); -} - -const glm::mat4& LightStage::Shadow::getProjection() const { - return _frustum->getProjection(); + _schemaBuffer.edit().cascades[cascadeIndex].reprojection = _biasMatrix * shadowFrustum.getProjection() * viewInverse.getMatrix(); } LightStage::Index LightStage::findLight(const LightPointer& light) const { @@ -142,7 +278,7 @@ LightStage::Index LightStage::addLight(const LightPointer& light) { _descs.emplace_back(Desc()); } else { assert(_descs[lightId].shadowId == INVALID_INDEX); - _descs.emplace(_descs.begin() + lightId, Desc()); + _descs[lightId] = Desc(); } // INsert the light and its index in the reverese map @@ -156,12 +292,12 @@ LightStage::Index LightStage::addLight(const LightPointer& light) { } } -LightStage::Index LightStage::addShadow(Index lightIndex) { +LightStage::Index LightStage::addShadow(Index lightIndex, float maxDistance, unsigned int cascadeCount) { auto light = getLight(lightIndex); Index shadowId = INVALID_INDEX; if (light) { assert(_descs[lightIndex].shadowId == INVALID_INDEX); - shadowId = _shadows.newElement(std::make_shared(light)); + shadowId = _shadows.newElement(std::make_shared(light, maxDistance, cascadeCount)); _descs[lightIndex].shadowId = shadowId; } return shadowId; diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h index fa581c8315..508e67ec17 100644 --- a/libraries/render-utils/src/LightStage.h +++ b/libraries/render-utils/src/LightStage.h @@ -44,45 +44,74 @@ public: class Shadow { public: using UniformBufferView = gpu::BufferView; - static const int MAP_SIZE = 1024; + static const int MAP_SIZE; - Shadow(model::LightPointer light); + class Cascade { + friend Shadow; + public: - void setKeylightFrustum(const ViewFrustum& viewFrustum, float viewMinShadowDistance, float viewMaxShadowDistance, float nearDepth = 1.0f, float farDepth = 1000.0f); + Cascade(); - void setFrustum(const ViewFrustum& shadowFrustum); - const std::shared_ptr getFrustum() const { return _frustum; } + gpu::FramebufferPointer framebuffer; + gpu::TexturePointer map; - const glm::mat4& getView() const; - const glm::mat4& getProjection() const; + const std::shared_ptr& getFrustum() const { return _frustum; } + + const glm::mat4& getView() const; + const glm::mat4& getProjection() const; + + void setMinDistance(float value) { _minDistance = value; } + void setMaxDistance(float value) { _maxDistance = value; } + float getMinDistance() const { return _minDistance; } + float getMaxDistance() const { return _maxDistance; } + + private: + + std::shared_ptr _frustum; + float _minDistance; + float _maxDistance; + + float computeFarDistance(const ViewFrustum& viewFrustum, const Transform& shadowViewInverse, + float left, float right, float bottom, float top, float viewMaxShadowDistance) const; + }; + + Shadow(model::LightPointer light, float maxDistance, unsigned int cascadeCount = 1); + + void setKeylightFrustum(const ViewFrustum& viewFrustum, + float nearDepth = 1.0f, float farDepth = 1000.0f); + void setKeylightCascadeFrustum(unsigned int cascadeIndex, const ViewFrustum& viewFrustum, + float nearDepth = 1.0f, float farDepth = 1000.0f); + void setCascadeFrustum(unsigned int cascadeIndex, const ViewFrustum& shadowFrustum); const UniformBufferView& getBuffer() const { return _schemaBuffer; } - // Shadow maps are shared among all lights for the moment as only one key light - // is used. - static gpu::FramebufferPointer framebuffer; - static gpu::TexturePointer map; + unsigned int getCascadeCount() const { return (unsigned int)_cascades.size(); } + const Cascade& getCascade(unsigned int index) const { return _cascades[index]; } + + float getMaxDistance() const { return _maxDistance; } + void setMaxDistance(float value); const model::LightPointer& getLight() const { return _light; } protected: - model::LightPointer _light; - std::shared_ptr _frustum; +#include "Shadows_shared.slh" - class Schema { + using Cascades = std::vector; + + static const glm::mat4 _biasMatrix; + + model::LightPointer _light; + float _maxDistance; + Cascades _cascades; + + class Schema : public ShadowParameters { public: Schema(); - glm::mat4 projection; - glm::mat4 viewInverse; - - glm::float32 bias; - glm::float32 scale; }; UniformBufferView _schemaBuffer = nullptr; - }; using ShadowPointer = std::shared_ptr; @@ -91,7 +120,7 @@ public: Index findLight(const LightPointer& light) const; Index addLight(const LightPointer& light); - Index addShadow(Index lightIndex); + Index addShadow(Index lightIndex, float maxDistance = 20.0f, unsigned int cascadeCount = 1U); LightPointer removeLight(Index index); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index a395136978..f860c0494e 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -163,6 +163,9 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren // Similar to light stage, background stage has been filled by several potential render items and resolved for the frame in this job task.addJob("DrawBackgroundDeferred", lightingModel); + const auto drawHazeInputs = render::Varying(DrawHaze::Inputs(hazeModel, lightingFramebuffer, linearDepthTarget, deferredFrameTransform, lightingFramebuffer)); + task.addJob("DrawHazeDeferred", drawHazeInputs); + // Render transparent objects forward in LightingBuffer const auto transparentsInputs = DrawDeferred::Inputs(transparents, lightingModel).asVarying(); task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); @@ -173,9 +176,6 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DebugLightClusters", debugLightClustersInputs); } - const auto drawHazeInputs = render::Varying(DrawHaze::Inputs(hazeModel, lightingFramebuffer, linearDepthTarget, deferredFrameTransform, lightingFramebuffer)); - task.addJob("DrawHaze", drawHazeInputs); - const auto toneAndPostRangeTimer = task.addJob("BeginToneAndPostRangeTimer", "PostToneOverlaysAntialiasing"); // Add bloom @@ -205,34 +205,23 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawZones", zones); const auto frustums = task.addJob("ExtractFrustums"); const auto viewFrustum = frustums.getN(ExtractFrustums::VIEW_FRUSTUM); - const auto shadowFrustum = frustums.getN(ExtractFrustums::SHADOW_FRUSTUM); task.addJob("DrawViewFrustum", viewFrustum, glm::vec3(1.0f, 1.0f, 0.0f)); - task.addJob("DrawShadowFrustum", shadowFrustum, glm::vec3(0.0f, 0.0f, 1.0f)); + for (auto i = 0; i < ExtractFrustums::SHADOW_CASCADE_FRUSTUM_COUNT; i++) { + const auto shadowFrustum = frustums.getN(ExtractFrustums::SHADOW_CASCADE0_FRUSTUM+i); + float tint = 1.0f - i / float(ExtractFrustums::SHADOW_CASCADE_FRUSTUM_COUNT - 1); + char jobName[64]; + sprintf(jobName, "DrawShadowFrustum%d", i); + task.addJob(jobName, shadowFrustum, glm::vec3(0.0f, tint, 1.0f)); + } // Render.getConfig("RenderMainView.DrawSelectionBounds").enabled = true task.addJob("DrawSelectionBounds", selectedItems); } - // Layered Overlays - const auto filteredOverlaysOpaque = task.addJob("FilterOverlaysLayeredOpaque", overlayOpaques, Item::LAYER_3D_FRONT); - const auto filteredOverlaysTransparent = task.addJob("FilterOverlaysLayeredTransparent", overlayTransparents, Item::LAYER_3D_FRONT); - const auto overlaysInFrontOpaque = filteredOverlaysOpaque.getN(0); - const auto overlaysInFrontTransparent = filteredOverlaysTransparent.getN(0); - - const auto overlayInFrontOpaquesInputs = DrawOverlay3D::Inputs(overlaysInFrontOpaque, lightingModel).asVarying(); - const auto overlayInFrontTransparentsInputs = DrawOverlay3D::Inputs(overlaysInFrontTransparent, lightingModel).asVarying(); - task.addJob("DrawOverlayInFrontOpaque", overlayInFrontOpaquesInputs, true); - task.addJob("DrawOverlayInFrontTransparent", overlayInFrontTransparentsInputs, false); - - { // Debug the bounds of the rendered Overlay items that are marked drawInFront, still look at the zbuffer - task.addJob("DrawOverlayInFrontOpaqueBounds", overlaysInFrontOpaque); - task.addJob("DrawOverlayInFrontTransparentBounds", overlaysInFrontTransparent); - } - // Debugging stages { // Debugging Deferred buffer job - const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, linearDepthTarget, surfaceGeometryFramebuffer, ambientOcclusionFramebuffer)); + const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, linearDepthTarget, surfaceGeometryFramebuffer, ambientOcclusionFramebuffer, deferredFrameTransform)); task.addJob("DebugDeferredBuffer", debugFramebuffers); const auto debugSubsurfaceScatteringInputs = DebugSubsurfaceScattering::Inputs(deferredFrameTransform, deferredFramebuffer, lightingModel, @@ -259,6 +248,22 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawZoneStack", deferredFrameTransform); } + // Layered Overlays + const auto filteredOverlaysOpaque = task.addJob("FilterOverlaysLayeredOpaque", overlayOpaques, Item::LAYER_3D_FRONT); + const auto filteredOverlaysTransparent = task.addJob("FilterOverlaysLayeredTransparent", overlayTransparents, Item::LAYER_3D_FRONT); + const auto overlaysInFrontOpaque = filteredOverlaysOpaque.getN(0); + const auto overlaysInFrontTransparent = filteredOverlaysTransparent.getN(0); + + const auto overlayInFrontOpaquesInputs = DrawOverlay3D::Inputs(overlaysInFrontOpaque, lightingModel).asVarying(); + const auto overlayInFrontTransparentsInputs = DrawOverlay3D::Inputs(overlaysInFrontTransparent, lightingModel).asVarying(); + task.addJob("DrawOverlayInFrontOpaque", overlayInFrontOpaquesInputs, true); + task.addJob("DrawOverlayInFrontTransparent", overlayInFrontTransparentsInputs, false); + + { // Debug the bounds of the rendered Overlay items that are marked drawInFront, still look at the zbuffer + task.addJob("DrawOverlayInFrontOpaqueBounds", overlaysInFrontOpaque); + task.addJob("DrawOverlayInFrontTransparentBounds", overlaysInFrontTransparent); + } + // AA job to be revisited task.addJob("Antialiasing", primaryFramebuffer); @@ -330,6 +335,13 @@ void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); + // Setup haze iff curretn zone has haze + auto hazeStage = args->_scene->getStage(); + if (hazeStage && hazeStage->_currentFrame._hazes.size() > 0) { + model::HazePointer hazePointer = hazeStage->getHaze(hazeStage->_currentFrame._hazes.front()); + batch.setUniformBuffer(render::ShapePipeline::Slot::HAZE_MODEL, hazePointer->getHazeParametersBuffer()); + } + // From the lighting model define a global shapKey ORED with individiual keys ShapeKey::Builder keyBuilder; if (lightingModel->isWireframeEnabled()) { @@ -555,17 +567,20 @@ void ExtractFrustums::run(const render::RenderContextPointer& renderContext, Out } // Return shadow frustum - auto& shadowFrustum = output[SHADOW_FRUSTUM].edit(); auto lightStage = args->_scene->getStage(LightStage::getName()); - if (lightStage) { - auto globalShadow = lightStage->getCurrentKeyShadow(); + for (auto i = 0; i < SHADOW_CASCADE_FRUSTUM_COUNT; i++) { + auto& shadowFrustum = output[SHADOW_CASCADE0_FRUSTUM+i].edit(); + if (lightStage) { + auto globalShadow = lightStage->getCurrentKeyShadow(); - if (globalShadow) { - shadowFrustum = globalShadow->getFrustum(); + if (globalShadow && i<(int)globalShadow->getCascadeCount()) { + auto& cascade = globalShadow->getCascade(i); + shadowFrustum = cascade.getFrustum(); + } else { + shadowFrustum.reset(); + } } else { shadowFrustum.reset(); } - } else { - shadowFrustum.reset(); } } diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 40ae503fb7..e41b7edc77 100644 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -16,7 +16,6 @@ #include #include "LightingModel.h" - class BeginGPURangeTimer { public: using JobModel = render::Job::ModelO; @@ -174,8 +173,14 @@ class ExtractFrustums { public: enum Frustum { - VIEW_FRUSTUM, - SHADOW_FRUSTUM, + SHADOW_CASCADE0_FRUSTUM = 0, + SHADOW_CASCADE1_FRUSTUM, + SHADOW_CASCADE2_FRUSTUM, + SHADOW_CASCADE3_FRUSTUM, + + SHADOW_CASCADE_FRUSTUM_COUNT, + + VIEW_FRUSTUM = SHADOW_CASCADE_FRUSTUM_COUNT, FRUSTUM_COUNT }; diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index 7a6e3dc74f..2e5b7132e4 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -22,6 +22,8 @@ #include "DeferredLightingEffect.h" #include "FramebufferCache.h" +#include "RenderUtilsLogging.h" + // These values are used for culling the objects rendered in the shadow map // but are readjusted afterwards #define SHADOW_FRUSTUM_NEAR 1.0f @@ -89,31 +91,13 @@ static void adjustNearFar(const AABox& inShapeBounds, ViewFrustum& shadowFrustum for (i = 0; i < 8; i++) { sceneBoundVertices[i] = shadowViewInverse.transform(inShapeBounds.getVertex(static_cast(i))); } - // This indirection array is just a protection in case the ViewFrustum::PlaneIndex enum - // changes order especially as we don't need to test the NEAR and FAR planes. - static const ViewFrustum::PlaneIndex planeIndices[4] = { - ViewFrustum::TOP_PLANE, - ViewFrustum::BOTTOM_PLANE, - ViewFrustum::LEFT_PLANE, - ViewFrustum::RIGHT_PLANE - }; - // Same goes for the shadow frustum planes. - for (i = 0; i < 4; i++) { - const auto& worldPlane = shadowFrustum.getPlanes()[planeIndices[i]]; - // We assume the transform doesn't have a non uniform scale component to apply the - // transform to the normal without using the correct transpose of inverse, which should be the - // case for a view matrix. - auto planeNormal = shadowViewInverse.transformDirection(worldPlane.getNormal()); - auto planePoint = shadowViewInverse.transform(worldPlane.getPoint()); - shadowClipPlanes[i].setNormalAndPoint(planeNormal, planePoint); - } + shadowFrustum.getUniformlyTransformedSidePlanes(shadowViewInverse, shadowClipPlanes); float near = std::numeric_limits::max(); float far = 0.0f; computeNearFar(sceneBoundVertices, shadowClipPlanes, near, far); - // Limit the far range to the one used originally. There's no point in rendering objects - // that are not in the view frustum. + // Limit the far range to the one used originally. far = glm::min(far, shadowFrustum.getFarClip()); const auto depthEpsilon = 0.1f; @@ -137,9 +121,12 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con assert(lightStage); auto shadow = lightStage->getCurrentKeyShadow(); - if (!shadow) return; + if (!shadow || _cascadeIndex >= shadow->getCascadeCount()) { + return; + } - const auto& fbo = shadow->framebuffer; + auto& cascade = shadow->getCascade(_cascadeIndex); + auto& fbo = cascade.framebuffer; RenderArgs* args = renderContext->args; ShapeKey::Builder defaultKeyBuilder; @@ -149,7 +136,7 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con // the minimal Z range. adjustNearFar(inShapeBounds, adjustedShadowFrustum); // Reapply the frustum as it has been adjusted - shadow->setFrustum(adjustedShadowFrustum); + shadow->setCascadeFrustum(_cascadeIndex, adjustedShadowFrustum); args->popViewFrustum(); args->pushViewFrustum(adjustedShadowFrustum); @@ -178,6 +165,7 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con auto shadowSkinnedPipeline = _shapePlumber->pickPipeline(args, defaultKeyBuilder.withSkinned()); std::vector skinnedShapeKeys{}; + std::vector ownPipelineShapeKeys{}; // Iterate through all inShapes and render the unskinned args->_shapePipeline = shadowPipeline; @@ -185,8 +173,10 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con for (auto items : inShapes) { if (items.first.isSkinned()) { skinnedShapeKeys.push_back(items.first); - } else { + } else if (!items.first.hasOwnPipeline()) { renderItems(renderContext, items.second); + } else { + ownPipelineShapeKeys.push_back(items.first); } } @@ -197,7 +187,15 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con renderItems(renderContext, inShapes.at(key)); } + // Finally render the items with their own pipeline last to prevent them from breaking the + // render state. This is probably a temporary code as there is probably something better + // to do in the render call of objects that have their own pipeline. args->_shapePipeline = nullptr; + for (const auto& key : ownPipelineShapeKeys) { + args->_itemShapeKey = key._flags.to_ulong(); + renderItems(renderContext, inShapes.at(key)); + } + args->_batch = nullptr; }); } @@ -215,22 +213,26 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende initZPassPipelines(*shapePlumber, state); } - const auto cachedMode = task.addJob("ShadowSetup"); + task.addJob("ShadowSetup"); - // CPU jobs: - // Fetch and cull the items from the scene - auto shadowFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered(); - const auto shadowSelection = task.addJob("FetchShadowSelection", shadowFilter); - const auto culledShadowSelection = task.addJob("CullShadowSelection", shadowSelection, cullFunctor, RenderDetails::SHADOW, shadowFilter); + for (auto i = 0; i < SHADOW_CASCADE_MAX_COUNT; i++) { + const auto setupOutput = task.addJob("ShadowCascadeSetup", i); + const auto shadowFilter = setupOutput.getN(1); - // Sort - const auto sortedPipelines = task.addJob("PipelineSortShadowSort", culledShadowSelection); - const auto sortedShapesAndBounds = task.addJob("DepthSortShadowMap", sortedPipelines, true); + // CPU jobs: + // Fetch and cull the items from the scene + const auto shadowSelection = task.addJob("FetchShadowSelection", shadowFilter); + const auto cullInputs = CullSpatialSelection::Inputs(shadowSelection, shadowFilter).asVarying(); + const auto culledShadowSelection = task.addJob("CullShadowSelection", cullInputs, cullFunctor, RenderDetails::SHADOW); - // GPU jobs: Render to shadow map - task.addJob("RenderShadowMap", sortedShapesAndBounds, shapePlumber); + // Sort + const auto sortedPipelines = task.addJob("PipelineSortShadowSort", culledShadowSelection); + const auto sortedShapesAndBounds = task.addJob("DepthSortShadowMap", sortedPipelines, true); - task.addJob("ShadowTeardown", cachedMode); + // GPU jobs: Render to shadow map + task.addJob("RenderShadowMap", sortedShapesAndBounds, shapePlumber, i); + task.addJob("ShadowCascadeTeardown", setupOutput); + } } void RenderShadowTask::configure(const Config& configuration) { @@ -239,31 +241,57 @@ void RenderShadowTask::configure(const Config& configuration) { // Task::configure(configuration); } -void RenderShadowSetup::run(const render::RenderContextPointer& renderContext, Output& output) { +void RenderShadowSetup::run(const render::RenderContextPointer& renderContext) { auto lightStage = renderContext->_scene->getStage(); assert(lightStage); + // Cache old render args + RenderArgs* args = renderContext->args; const auto globalShadow = lightStage->getCurrentKeyShadow(); if (globalShadow) { - // Cache old render args - RenderArgs* args = renderContext->args; - output = args->_renderMode; - - auto nearClip = args->getViewFrustum().getNearClip(); - float nearDepth = -args->_boomOffset.z; - const float SHADOW_MAX_DISTANCE = 20.0f; - globalShadow->setKeylightFrustum(args->getViewFrustum(), nearDepth, nearClip + SHADOW_MAX_DISTANCE, SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR); - - // Set the keylight render args - args->pushViewFrustum(*(globalShadow->getFrustum())); - args->_renderMode = RenderArgs::SHADOW_RENDER_MODE; + globalShadow->setKeylightFrustum(args->getViewFrustum(), SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR); } } -void RenderShadowTeardown::run(const render::RenderContextPointer& renderContext, const Input& input) { +void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderContext, Outputs& output) { + auto lightStage = renderContext->_scene->getStage(); + assert(lightStage); + // Cache old render args RenderArgs* args = renderContext->args; + output.edit0() = args->_renderMode; + output.edit2() = args->_sizeScale; + + const auto globalShadow = lightStage->getCurrentKeyShadow(); + if (globalShadow && _cascadeIndexgetCascadeCount()) { + output.edit1() = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered(); + + globalShadow->setKeylightCascadeFrustum(_cascadeIndex, args->getViewFrustum(), SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR); + + // Set the keylight render args + args->pushViewFrustum(*(globalShadow->getCascade(_cascadeIndex).getFrustum())); + args->_renderMode = RenderArgs::SHADOW_RENDER_MODE; + if (lightStage->getCurrentKeyLight()->getType() == model::Light::SUN) { + const float shadowSizeScale = 1e16f; + // Set the size scale to a ridiculously high value to prevent small object culling which assumes + // the view frustum is a perspective projection. But this isn't the case for the sun which + // is an orthographic projection. + args->_sizeScale = shadowSizeScale; + } + + } else { + output.edit1() = ItemFilter::Builder::nothing(); + } +} + +void RenderShadowCascadeTeardown::run(const render::RenderContextPointer& renderContext, const Input& input) { + RenderArgs* args = renderContext->args; + + if (args->_renderMode == RenderArgs::SHADOW_RENDER_MODE && !input.get1().selectsNothing()) { + args->popViewFrustum(); + } + assert(args->hasViewFrustum()); // Reset the render args - args->popViewFrustum(); - args->_renderMode = input; + args->_renderMode = input.get0(); + args->_sizeScale = input.get2(); }; diff --git a/libraries/render-utils/src/RenderShadowTask.h b/libraries/render-utils/src/RenderShadowTask.h index 7b2bbeb306..d8d4c624e7 100644 --- a/libraries/render-utils/src/RenderShadowTask.h +++ b/libraries/render-utils/src/RenderShadowTask.h @@ -24,11 +24,12 @@ public: using Inputs = render::VaryingSet2; using JobModel = render::Job::ModelI; - RenderShadowMap(render::ShapePlumberPointer shapePlumber) : _shapePlumber{ shapePlumber } {} + RenderShadowMap(render::ShapePlumberPointer shapePlumber, unsigned int cascadeIndex) : _shapePlumber{ shapePlumber }, _cascadeIndex{ cascadeIndex } {} void run(const render::RenderContextPointer& renderContext, const Inputs& inputs); protected: render::ShapePlumberPointer _shapePlumber; + unsigned int _cascadeIndex; }; class RenderShadowTaskConfig : public render::Task::Config::Persistent { @@ -54,15 +55,30 @@ public: class RenderShadowSetup { public: - using Output = RenderArgs::RenderMode; - using JobModel = render::Job::ModelO; - void run(const render::RenderContextPointer& renderContext, Output& output); + using JobModel = render::Job::Model; + + RenderShadowSetup() {} + void run(const render::RenderContextPointer& renderContext); + }; -class RenderShadowTeardown { +class RenderShadowCascadeSetup { public: - using Input = RenderArgs::RenderMode; - using JobModel = render::Job::ModelI; + using Outputs = render::VaryingSet3; + using JobModel = render::Job::ModelO; + + RenderShadowCascadeSetup(unsigned int cascadeIndex) : _cascadeIndex{ cascadeIndex } {} + void run(const render::RenderContextPointer& renderContext, Outputs& output); + +private: + + unsigned int _cascadeIndex; +}; + +class RenderShadowCascadeTeardown { +public: + using Input = RenderShadowCascadeSetup::Outputs; + using JobModel = render::Job::ModelI; void run(const render::RenderContextPointer& renderContext, const Input& input); }; diff --git a/libraries/render-utils/src/RenderViewTask.cpp b/libraries/render-utils/src/RenderViewTask.cpp index 1085a1148c..dc6c66e058 100644 --- a/libraries/render-utils/src/RenderViewTask.cpp +++ b/libraries/render-utils/src/RenderViewTask.cpp @@ -14,15 +14,21 @@ #include "RenderDeferredTask.h" #include "RenderForwardTask.h" - - void RenderViewTask::build(JobModel& task, const render::Varying& input, render::Varying& output, render::CullFunctor cullFunctor, bool isDeferred) { // auto items = input.get(); // Shadows use an orthographic projection because they are linked to sunlights // but the cullFunctor passed is probably tailored for perspective projection and culls too much. - // TODO : create a special cull functor for this. - task.addJob("RenderShadowTask", nullptr); + task.addJob("RenderShadowTask", [](const RenderArgs* args, const AABox& bounds) { + // Cull only objects that are too small relatively to shadow frustum + auto& frustum = args->getViewFrustum(); + auto frustumSize = std::max(frustum.getHeight(), frustum.getWidth()); + const auto boundsRadius = bounds.getDimensions().length(); + const auto relativeBoundRadius = boundsRadius / frustumSize; + const auto threshold = 1e-3f; + return relativeBoundRadius > threshold; + return true; + }); const auto items = task.addJob("FetchCullSort", cullFunctor); assert(items.canCast()); diff --git a/libraries/render-utils/src/Shadow.slh b/libraries/render-utils/src/Shadow.slh index e844db43dd..a12dd0f4a4 100644 --- a/libraries/render-utils/src/Shadow.slh +++ b/libraries/render-utils/src/Shadow.slh @@ -11,53 +11,17 @@ <@if not SHADOW_SLH@> <@def SHADOW_SLH@> +<@include ShadowCore.slh@> + +#define SHADOW_NOISE_ENABLED 0 +#define SHADOW_SCREEN_SPACE_DITHER 1 + // the shadow texture -uniform sampler2DShadow shadowMap; - -struct ShadowTransform { - mat4 projection; - mat4 viewInverse; - - float bias; - float scale; -}; - -uniform shadowTransformBuffer { - ShadowTransform _shadowTransform; -}; - -mat4 getShadowViewInverse() { - return _shadowTransform.viewInverse; -} - -mat4 getShadowProjection() { - return _shadowTransform.projection; -} - -float getShadowScale() { - return _shadowTransform.scale; -} - -float getShadowBias() { - return _shadowTransform.bias; -} - -// Compute the texture coordinates from world coordinates -vec4 evalShadowTexcoord(vec4 position) { - mat4 biasMatrix = mat4( - 0.5, 0.0, 0.0, 0.0, - 0.0, 0.5, 0.0, 0.0, - 0.0, 0.0, 0.5, 0.0, - 0.5, 0.5, 0.5, 1.0); - float bias = -getShadowBias(); - - vec4 shadowCoord = biasMatrix * getShadowProjection() * getShadowViewInverse() * position; - return vec4(shadowCoord.xy, shadowCoord.z + bias, 1.0); -} +uniform sampler2DShadow shadowMaps[SHADOW_CASCADE_MAX_COUNT]; // Sample the shadowMap with PCF (built-in) -float fetchShadow(vec3 shadowTexcoord) { - return texture(shadowMap, shadowTexcoord); +float fetchShadow(int cascadeIndex, vec3 shadowTexcoord) { + return texture(shadowMaps[cascadeIndex], shadowTexcoord); } vec2 PCFkernel[4] = vec2[4]( @@ -67,38 +31,83 @@ vec2 PCFkernel[4] = vec2[4]( vec2(0.5, -1.5) ); -float evalShadowAttenuationPCF(vec4 position, vec4 shadowTexcoord) { - // PCF is buggy so disable it for the time being -#if 0 - float pcfRadius = 3.0; +float evalShadowNoise(vec4 seed) { + float dot_product = dot(seed, vec4(12.9898,78.233,45.164,94.673)); + return fract(sin(dot_product) * 43758.5453); +} + +struct ShadowSampleOffsets { + vec3 points[4]; +}; + +ShadowSampleOffsets evalShadowFilterOffsets(vec4 position) { float shadowScale = getShadowScale(); + ShadowSampleOffsets offsets; + +#if SHADOW_SCREEN_SPACE_DITHER + // Pattern dithering in screen space + ivec2 coords = ivec2(gl_FragCoord.xy); +#else + // Pattern dithering in world space (mm resolution) + ivec2 coords = ivec2(position.x, position.y+position.z); +#endif + +#if SHADOW_NOISE_ENABLED + // Add some noise to break dithering + int index = int(4.0*evalShadowNoise(gl_FragCoord.xyyx))%4; + coords.x += index & 1; + coords.y += (index & 2) >> 1; +#endif // Offset for efficient PCF, see http://http.developer.nvidia.com/GPUGems/gpugems_ch11.html - vec2 offset = pcfRadius * step(fract(position.xy), vec2(0.5, 0.5)); + ivec2 offset = coords & ivec2(1,1); + offset.y = (offset.x+offset.y) & 1; - float shadowAttenuation = (0.25 * ( - fetchShadow(shadowTexcoord.xyz + shadowScale * vec3(offset + PCFkernel[0], 0.0)) + - fetchShadow(shadowTexcoord.xyz + shadowScale * vec3(offset + PCFkernel[1], 0.0)) + - fetchShadow(shadowTexcoord.xyz + shadowScale * vec3(offset + PCFkernel[2], 0.0)) + - fetchShadow(shadowTexcoord.xyz + shadowScale * vec3(offset + PCFkernel[3], 0.0)) - )); -#else - float shadowAttenuation = fetchShadow(shadowTexcoord.xyz); -#endif + offsets.points[0] = shadowScale * vec3(offset + PCFkernel[0], 0.0); + offsets.points[1] = shadowScale * vec3(offset + PCFkernel[1], 0.0); + offsets.points[2] = shadowScale * vec3(offset + PCFkernel[2], 0.0); + offsets.points[3] = shadowScale * vec3(offset + PCFkernel[3], 0.0); + + return offsets; +} + +float evalShadowAttenuationPCF(int cascadeIndex, ShadowSampleOffsets offsets, vec4 shadowTexcoord, float bias) { + shadowTexcoord.z -= bias; + float shadowAttenuation = 0.25 * ( + fetchShadow(cascadeIndex, shadowTexcoord.xyz + offsets.points[0]) + + fetchShadow(cascadeIndex, shadowTexcoord.xyz + offsets.points[1]) + + fetchShadow(cascadeIndex, shadowTexcoord.xyz + offsets.points[2]) + + fetchShadow(cascadeIndex, shadowTexcoord.xyz + offsets.points[3]) + ); return shadowAttenuation; } -float evalShadowAttenuation(vec4 position) { - vec4 shadowTexcoord = evalShadowTexcoord(position); - if (shadowTexcoord.x < 0.0 || shadowTexcoord.x > 1.0 || - shadowTexcoord.y < 0.0 || shadowTexcoord.y > 1.0 || - shadowTexcoord.z < 0.0 || shadowTexcoord.z > 1.0) { +float evalShadowCascadeAttenuation(int cascadeIndex, vec3 viewNormal, ShadowSampleOffsets offsets, vec4 shadowTexcoord) { + if (!isShadowCascadeProjectedOnPixel(shadowTexcoord)) { // If a point is not in the map, do not attenuate return 1.0; } + // Multiply bias if we are at a grazing angle with light + float tangentFactor = abs(dot(getShadowDirInViewSpace(), viewNormal)); + float bias = getShadowBias(cascadeIndex) * (5.0-4.0*tangentFactor); + return evalShadowAttenuationPCF(cascadeIndex, offsets, shadowTexcoord, bias); +} - return evalShadowAttenuationPCF(position, shadowTexcoord); +float evalShadowAttenuation(vec4 worldPosition, float viewDepth, vec3 viewNormal) { + ShadowSampleOffsets offsets = evalShadowFilterOffsets(worldPosition); + vec4 cascadeShadowCoords[2]; + ivec2 cascadeIndices; + float cascadeMix = determineShadowCascadesOnPixel(worldPosition, viewDepth, cascadeShadowCoords, cascadeIndices); + + vec2 cascadeAttenuations = vec2(1.0, 1.0); + cascadeAttenuations.x = evalShadowCascadeAttenuation(cascadeIndices.x, viewNormal, offsets, cascadeShadowCoords[0]); + if (cascadeMix > 0.0 && cascadeIndices.y < getShadowCascadeCount()) { + cascadeAttenuations.y = evalShadowCascadeAttenuation(cascadeIndices.y, viewNormal, offsets, cascadeShadowCoords[1]); + } + float attenuation = mix(cascadeAttenuations.x, cascadeAttenuations.y, cascadeMix); + // Falloff to max distance + return mix(1.0, attenuation, evalShadowFalloff(viewDepth)); } <@endif@> diff --git a/libraries/render-utils/src/ShadowCore.slh b/libraries/render-utils/src/ShadowCore.slh new file mode 100644 index 0000000000..9b3b086598 --- /dev/null +++ b/libraries/render-utils/src/ShadowCore.slh @@ -0,0 +1,96 @@ + +<@if not SHADOW_CORE_SLH@> +<@def SHADOW_CORE_SLH@> + +<@include Shadows_shared.slh@> + +layout(std140) uniform shadowTransformBuffer { + ShadowParameters shadow; +}; + +int getShadowCascadeCount() { + return shadow.cascadeCount; +} + +float getShadowCascadeInvBlendWidth() { + return shadow.invCascadeBlendWidth; +} + +float evalShadowFalloff(float depth) { + return clamp((shadow.maxDistance-depth) * shadow.invFalloffDistance, 0.0, 1.0); +} + +mat4 getShadowReprojection(int cascadeIndex) { + return shadow.cascades[cascadeIndex].reprojection; +} + +float getShadowScale() { + return shadow.invMapSize; +} + +float getShadowBias(int cascadeIndex) { + return shadow.cascades[cascadeIndex].bias; +} + +vec3 getShadowDirInViewSpace() { + return shadow.lightDirInViewSpace; +} + +// Compute the texture coordinates from world coordinates +vec4 evalShadowTexcoord(int cascadeIndex, vec4 position) { + vec4 shadowCoord = getShadowReprojection(cascadeIndex) * position; + return vec4(shadowCoord.xyz, 1.0); +} + +bool isShadowCascadeProjectedOnPixel(vec4 cascadeTexCoords) { + bvec2 greaterThanZero = greaterThanEqual(cascadeTexCoords.xy, vec2(0)); + bvec2 lessThanOne = lessThanEqual(cascadeTexCoords.xy, vec2(1)); + return all(greaterThanZero) && all(lessThanOne); +} + +int getFirstShadowCascadeOnPixel(int startCascadeIndex, vec4 worldPosition, out vec4 cascadeShadowCoords) { + int cascadeIndex; + startCascadeIndex = min(startCascadeIndex, getShadowCascadeCount()-1); + for (cascadeIndex=startCascadeIndex ; cascadeIndex diff --git a/libraries/render-utils/src/Shadows_shared.slh b/libraries/render-utils/src/Shadows_shared.slh new file mode 100644 index 0000000000..abb226421c --- /dev/null +++ b/libraries/render-utils/src/Shadows_shared.slh @@ -0,0 +1,34 @@ +// glsl / C++ compatible source as interface for Shadows +#ifdef __cplusplus +# define MAT4 glm::mat4 +# define VEC3 glm::vec3 +#else +# define MAT4 mat4 +# define VEC3 vec3 +#endif + +#define SHADOW_CASCADE_MAX_COUNT 4 + +struct ShadowTransform { + MAT4 reprojection; + + float bias; + float _padding1; + float _padding2; + float _padding3; +}; + +struct ShadowParameters { + ShadowTransform cascades[SHADOW_CASCADE_MAX_COUNT]; + VEC3 lightDirInViewSpace; + int cascadeCount; + float invMapSize; + float invCascadeBlendWidth; + float maxDistance; + float invFalloffDistance; +}; + +// <@if 1@> +// Trigger Scribe include +// <@endif@> +// diff --git a/libraries/render-utils/src/debug_deferred_buffer.slf b/libraries/render-utils/src/debug_deferred_buffer.slf index e9750f0054..426de623a1 100644 --- a/libraries/render-utils/src/debug_deferred_buffer.slf +++ b/libraries/render-utils/src/debug_deferred_buffer.slf @@ -16,7 +16,6 @@ <@include gpu/Color.slh@> <$declareColorWheel()$> - uniform sampler2D linearDepthMap; uniform sampler2D halfLinearDepthMap; uniform sampler2D halfNormalMap; @@ -24,6 +23,8 @@ uniform sampler2D occlusionMap; uniform sampler2D occlusionBlurredMap; uniform sampler2D scatteringMap; +<@include ShadowCore.slh@> + <$declareDeferredCurvature()$> float curvatureAO(float k) { diff --git a/libraries/render-utils/src/directional_ambient_light_shadow.slf b/libraries/render-utils/src/directional_ambient_light_shadow.slf index d3778c9228..f7ea8c5966 100644 --- a/libraries/render-utils/src/directional_ambient_light_shadow.slf +++ b/libraries/render-utils/src/directional_ambient_light_shadow.slf @@ -26,8 +26,9 @@ void main(void) { DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); - vec4 worldPos = getViewInverse() * vec4(frag.position.xyz, 1.0); - float shadowAttenuation = evalShadowAttenuation(worldPos); + vec4 viewPos = vec4(frag.position.xyz, 1.0); + vec4 worldPos = getViewInverse() * viewPos; + float shadowAttenuation = evalShadowAttenuation(worldPos, -viewPos.z, frag.normal); if (frag.mode == FRAG_MODE_UNLIT) { discard; diff --git a/libraries/render-utils/src/directional_skybox_light_shadow.slf b/libraries/render-utils/src/directional_skybox_light_shadow.slf index 3ca0f71df5..37c9ae7fba 100644 --- a/libraries/render-utils/src/directional_skybox_light_shadow.slf +++ b/libraries/render-utils/src/directional_skybox_light_shadow.slf @@ -26,8 +26,9 @@ void main(void) { DeferredFrameTransform deferredTransform = getDeferredFrameTransform(); DeferredFragment frag = unpackDeferredFragment(deferredTransform, _texCoord0); - vec4 worldPos = getViewInverse() * vec4(frag.position.xyz, 1.0); - float shadowAttenuation = evalShadowAttenuation(worldPos); + vec4 viewPos = vec4(frag.position.xyz, 1.0); + vec4 worldPos = getViewInverse() * viewPos; + float shadowAttenuation = evalShadowAttenuation(worldPos, -viewPos.z, frag.normal); // Light mapped or not ? if (frag.mode == FRAG_MODE_UNLIT) { diff --git a/libraries/render-utils/src/forward_model_translucent.slf b/libraries/render-utils/src/forward_model_translucent.slf index 52e8ce50e7..906393db1f 100644 --- a/libraries/render-utils/src/forward_model_translucent.slf +++ b/libraries/render-utils/src/forward_model_translucent.slf @@ -66,7 +66,7 @@ void main(void) { TransformCamera cam = getTransformCamera(); - _fragColor = vec4(evalGlobalLightingAlphaBlended( + _fragColor = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, occlusionTex, diff --git a/libraries/render-utils/src/model_translucent.slf b/libraries/render-utils/src/model_translucent.slf index 6cf99a68ef..38f162fdc3 100644 --- a/libraries/render-utils/src/model_translucent.slf +++ b/libraries/render-utils/src/model_translucent.slf @@ -66,7 +66,7 @@ void main(void) { TransformCamera cam = getTransformCamera(); - _fragColor = vec4(evalGlobalLightingAlphaBlended( + _fragColor = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, occlusionTex, diff --git a/libraries/render-utils/src/model_translucent_fade.slf b/libraries/render-utils/src/model_translucent_fade.slf index c46b396ebc..9d5477304c 100644 --- a/libraries/render-utils/src/model_translucent_fade.slf +++ b/libraries/render-utils/src/model_translucent_fade.slf @@ -76,7 +76,7 @@ void main(void) { TransformCamera cam = getTransformCamera(); - _fragColor = vec4(evalGlobalLightingAlphaBlended( + _fragColor = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, occlusionTex, diff --git a/libraries/render-utils/src/overlay3D_model_translucent.slf b/libraries/render-utils/src/overlay3D_model_translucent.slf index 748eea329c..b26e70f465 100644 --- a/libraries/render-utils/src/overlay3D_model_translucent.slf +++ b/libraries/render-utils/src/overlay3D_model_translucent.slf @@ -65,7 +65,7 @@ void main(void) { vec3 fragNormal; <$transformEyeToWorldDir(cam, _normal, fragNormal)$> - vec4 color = vec4(evalGlobalLightingAlphaBlended( + vec4 color = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, occlusionTex, diff --git a/libraries/render-utils/src/simple_transparent_textured.slf b/libraries/render-utils/src/simple_transparent_textured.slf index b9eb921e9d..b16b19c8b4 100644 --- a/libraries/render-utils/src/simple_transparent_textured.slf +++ b/libraries/render-utils/src/simple_transparent_textured.slf @@ -45,7 +45,7 @@ void main(void) { TransformCamera cam = getTransformCamera(); - _fragColor0 = vec4(evalGlobalLightingAlphaBlended( + _fragColor0 = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, 1.0, diff --git a/libraries/render-utils/src/simple_transparent_textured_fade.slf b/libraries/render-utils/src/simple_transparent_textured_fade.slf index 20c7907bbe..ad260210a7 100644 --- a/libraries/render-utils/src/simple_transparent_textured_fade.slf +++ b/libraries/render-utils/src/simple_transparent_textured_fade.slf @@ -57,7 +57,7 @@ void main(void) { TransformCamera cam = getTransformCamera(); - _fragColor0 = vec4(evalGlobalLightingAlphaBlended( + _fragColor0 = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, 1.0, diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp index 4fc53d99f9..70331cdb47 100644 --- a/libraries/render/src/render/CullTask.cpp +++ b/libraries/render/src/render/CullTask.cpp @@ -82,28 +82,30 @@ void FetchSpatialTree::configure(const Config& config) { _lodAngle = config.lodAngle; } -void FetchSpatialTree::run(const RenderContextPointer& renderContext, ItemSpatialTree::ItemSelection& outSelection) { - assert(renderContext->args); - assert(renderContext->args->hasViewFrustum()); - RenderArgs* args = renderContext->args; - auto& scene = renderContext->_scene; - +void FetchSpatialTree::run(const RenderContextPointer& renderContext, const ItemFilter& filter, ItemSpatialTree::ItemSelection& outSelection) { // start fresh outSelection.clear(); - // Eventually use a frozen frustum - auto queryFrustum = args->getViewFrustum(); - if (_freezeFrustum) { - if (_justFrozeFrustum) { - _justFrozeFrustum = false; - _frozenFrutstum = args->getViewFrustum(); - } - queryFrustum = _frozenFrutstum; - } + if (!filter.selectsNothing()) { + assert(renderContext->args); + assert(renderContext->args->hasViewFrustum()); + RenderArgs* args = renderContext->args; + auto& scene = renderContext->_scene; - // Octree selection! - float angle = glm::degrees(getAccuracyAngle(args->_sizeScale, args->_boundaryLevelAdjust)); - scene->getSpatialTree().selectCellItems(outSelection, _filter, queryFrustum, angle); + // Eventually use a frozen frustum + auto queryFrustum = args->getViewFrustum(); + if (_freezeFrustum) { + if (_justFrozeFrustum) { + _justFrozeFrustum = false; + _frozenFrustum = args->getViewFrustum(); + } + queryFrustum = _frozenFrustum; + } + + // Octree selection! + float angle = glm::degrees(getAccuracyAngle(args->_sizeScale, args->_boundaryLevelAdjust)); + scene->getSpatialTree().selectCellItems(outSelection, filter, queryFrustum, angle); + } } void CullSpatialSelection::configure(const Config& config) { @@ -113,11 +115,12 @@ void CullSpatialSelection::configure(const Config& config) { } void CullSpatialSelection::run(const RenderContextPointer& renderContext, - const ItemSpatialTree::ItemSelection& inSelection, ItemBounds& outItems) { + const Inputs& inputs, ItemBounds& outItems) { assert(renderContext->args); assert(renderContext->args->hasViewFrustum()); RenderArgs* args = renderContext->args; auto& scene = renderContext->_scene; + auto& inSelection = inputs.get0(); auto& details = args->_details.edit(_detailType); details._considered += (int)inSelection.numItems(); @@ -126,9 +129,9 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext, if (_freezeFrustum) { if (_justFrozeFrustum) { _justFrozeFrustum = false; - _frozenFrutstum = args->getViewFrustum(); + _frozenFrustum = args->getViewFrustum(); } - args->pushViewFrustum(_frozenFrutstum); // replace the true view frustum by the frozen one + args->pushViewFrustum(_frozenFrustum); // replace the true view frustum by the frozen one } // Culling Frustum / solidAngle test helper class @@ -181,122 +184,124 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext, outItems.clear(); outItems.reserve(inSelection.numItems()); - // Now get the bound, and - // filter individually against the _filter - // visibility cull if partially selected ( octree cell contianing it was partial) - // distance cull if was a subcell item ( octree cell is way bigger than the item bound itself, so now need to test per item) + const auto filter = inputs.get1(); + if (!filter.selectsNothing()) { + // Now get the bound, and + // filter individually against the _filter + // visibility cull if partially selected ( octree cell contianing it was partial) + // distance cull if was a subcell item ( octree cell is way bigger than the item bound itself, so now need to test per item) - if (_skipCulling) { - // inside & fit items: filter only, culling is disabled - { - PerformanceTimer perfTimer("insideFitItems"); - for (auto id : inSelection.insideItems) { - auto& item = scene->getItem(id); - if (_filter.test(item.getKey())) { - ItemBound itemBound(id, item.getBound()); - outItems.emplace_back(itemBound); - } - } - } - - // inside & subcell items: filter only, culling is disabled - { - PerformanceTimer perfTimer("insideSmallItems"); - for (auto id : inSelection.insideSubcellItems) { - auto& item = scene->getItem(id); - if (_filter.test(item.getKey())) { - ItemBound itemBound(id, item.getBound()); - outItems.emplace_back(itemBound); - } - } - } - - // partial & fit items: filter only, culling is disabled - { - PerformanceTimer perfTimer("partialFitItems"); - for (auto id : inSelection.partialItems) { - auto& item = scene->getItem(id); - if (_filter.test(item.getKey())) { - ItemBound itemBound(id, item.getBound()); - outItems.emplace_back(itemBound); - } - } - } - - // partial & subcell items: filter only, culling is disabled - { - PerformanceTimer perfTimer("partialSmallItems"); - for (auto id : inSelection.partialSubcellItems) { - auto& item = scene->getItem(id); - if (_filter.test(item.getKey())) { - ItemBound itemBound(id, item.getBound()); - outItems.emplace_back(itemBound); - } - } - } - - } else { - - // inside & fit items: easy, just filter - { - PerformanceTimer perfTimer("insideFitItems"); - for (auto id : inSelection.insideItems) { - auto& item = scene->getItem(id); - if (_filter.test(item.getKey())) { - ItemBound itemBound(id, item.getBound()); - outItems.emplace_back(itemBound); - } - } - } - - // inside & subcell items: filter & distance cull - { - PerformanceTimer perfTimer("insideSmallItems"); - for (auto id : inSelection.insideSubcellItems) { - auto& item = scene->getItem(id); - if (_filter.test(item.getKey())) { - ItemBound itemBound(id, item.getBound()); - if (test.solidAngleTest(itemBound.bound)) { + if (_skipCulling) { + // inside & fit items: filter only, culling is disabled + { + PerformanceTimer perfTimer("insideFitItems"); + for (auto id : inSelection.insideItems) { + auto& item = scene->getItem(id); + if (filter.test(item.getKey())) { + ItemBound itemBound(id, item.getBound()); outItems.emplace_back(itemBound); } } } - } - // partial & fit items: filter & frustum cull - { - PerformanceTimer perfTimer("partialFitItems"); - for (auto id : inSelection.partialItems) { - auto& item = scene->getItem(id); - if (_filter.test(item.getKey())) { - ItemBound itemBound(id, item.getBound()); - if (test.frustumTest(itemBound.bound)) { + // inside & subcell items: filter only, culling is disabled + { + PerformanceTimer perfTimer("insideSmallItems"); + for (auto id : inSelection.insideSubcellItems) { + auto& item = scene->getItem(id); + if (filter.test(item.getKey())) { + ItemBound itemBound(id, item.getBound()); outItems.emplace_back(itemBound); } } } - } - // partial & subcell items:: filter & frutum cull & solidangle cull - { - PerformanceTimer perfTimer("partialSmallItems"); - for (auto id : inSelection.partialSubcellItems) { - auto& item = scene->getItem(id); - if (_filter.test(item.getKey())) { - ItemBound itemBound(id, item.getBound()); - if (test.frustumTest(itemBound.bound)) { + // partial & fit items: filter only, culling is disabled + { + PerformanceTimer perfTimer("partialFitItems"); + for (auto id : inSelection.partialItems) { + auto& item = scene->getItem(id); + if (filter.test(item.getKey())) { + ItemBound itemBound(id, item.getBound()); + outItems.emplace_back(itemBound); + } + } + } + + // partial & subcell items: filter only, culling is disabled + { + PerformanceTimer perfTimer("partialSmallItems"); + for (auto id : inSelection.partialSubcellItems) { + auto& item = scene->getItem(id); + if (filter.test(item.getKey())) { + ItemBound itemBound(id, item.getBound()); + outItems.emplace_back(itemBound); + } + } + } + + } else { + + // inside & fit items: easy, just filter + { + PerformanceTimer perfTimer("insideFitItems"); + for (auto id : inSelection.insideItems) { + auto& item = scene->getItem(id); + if (filter.test(item.getKey())) { + ItemBound itemBound(id, item.getBound()); + outItems.emplace_back(itemBound); + } + } + } + + // inside & subcell items: filter & distance cull + { + PerformanceTimer perfTimer("insideSmallItems"); + for (auto id : inSelection.insideSubcellItems) { + auto& item = scene->getItem(id); + if (filter.test(item.getKey())) { + ItemBound itemBound(id, item.getBound()); if (test.solidAngleTest(itemBound.bound)) { outItems.emplace_back(itemBound); } } } } + + // partial & fit items: filter & frustum cull + { + PerformanceTimer perfTimer("partialFitItems"); + for (auto id : inSelection.partialItems) { + auto& item = scene->getItem(id); + if (filter.test(item.getKey())) { + ItemBound itemBound(id, item.getBound()); + if (test.frustumTest(itemBound.bound)) { + outItems.emplace_back(itemBound); + } + } + } + } + + // partial & subcell items:: filter & frutum cull & solidangle cull + { + PerformanceTimer perfTimer("partialSmallItems"); + for (auto id : inSelection.partialSubcellItems) { + auto& item = scene->getItem(id); + if (filter.test(item.getKey())) { + ItemBound itemBound(id, item.getBound()); + if (test.frustumTest(itemBound.bound)) { + if (test.solidAngleTest(itemBound.bound)) { + outItems.emplace_back(itemBound); + } + } + } + } + } } } details._rendered += (int)outItems.size(); - // Restore frustum if using the frozen one: if (_freezeFrustum) { args->popViewFrustum(); diff --git a/libraries/render/src/render/CullTask.h b/libraries/render/src/render/CullTask.h index fae2a342a1..486c4f4cdf 100644 --- a/libraries/render/src/render/CullTask.h +++ b/libraries/render/src/render/CullTask.h @@ -51,19 +51,16 @@ namespace render { class FetchSpatialTree { bool _freezeFrustum{ false }; // initialized by Config bool _justFrozeFrustum{ false }; - ViewFrustum _frozenFrutstum; + ViewFrustum _frozenFrustum; float _lodAngle; public: using Config = FetchSpatialTreeConfig; - using JobModel = Job::ModelO; + using JobModel = Job::ModelIO; FetchSpatialTree() {} - FetchSpatialTree(const ItemFilter& filter) : _filter(filter) {} - - ItemFilter _filter{ ItemFilter::Builder::opaqueShape().withoutLayered() }; void configure(const Config& config); - void run(const RenderContextPointer& renderContext, ItemSpatialTree::ItemSelection& outSelection); + void run(const RenderContextPointer& renderContext, const ItemFilter& filter, ItemSpatialTree::ItemSelection& outSelection); }; class CullSpatialSelectionConfig : public Job::Config { @@ -88,25 +85,24 @@ namespace render { bool _freezeFrustum{ false }; // initialized by Config bool _justFrozeFrustum{ false }; bool _skipCulling{ false }; - ViewFrustum _frozenFrutstum; + ViewFrustum _frozenFrustum; public: using Config = CullSpatialSelectionConfig; - using JobModel = Job::ModelIO; + using Inputs = render::VaryingSet2; + using JobModel = Job::ModelIO; - CullSpatialSelection(CullFunctor cullFunctor, RenderDetails::Type type, const ItemFilter& filter) : + CullSpatialSelection(CullFunctor cullFunctor, RenderDetails::Type type) : _cullFunctor{ cullFunctor }, - _detailType(type), - _filter(filter) {} + _detailType(type) {} CullSpatialSelection(CullFunctor cullFunctor) : _cullFunctor{ cullFunctor } {} CullFunctor _cullFunctor; RenderDetails::Type _detailType{ RenderDetails::OTHER }; - ItemFilter _filter{ ItemFilter::Builder::opaqueShape().withoutLayered() }; void configure(const Config& config); - void run(const RenderContextPointer& renderContext, const ItemSpatialTree::ItemSelection& inSelection, ItemBounds& outItems); + void run(const RenderContextPointer& renderContext, const Inputs& inputs, ItemBounds& outItems); }; } diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 0f4137e38d..629cc55ccb 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -215,79 +215,158 @@ void DrawBounds::run(const RenderContextPointer& renderContext, }); } -gpu::PipelinePointer DrawFrustum::_pipeline; +DrawQuadVolume::DrawQuadVolume(const glm::vec3& color) : + _color{ color } { + _meshVertices = gpu::BufferView(std::make_shared(sizeof(glm::vec3) * 8, nullptr), gpu::Element::VEC3F_XYZ); + _meshStream.addBuffer(_meshVertices._buffer, _meshVertices._offset, _meshVertices._stride); +} + +void DrawQuadVolume::configure(const Config& configuration) { + _isUpdateEnabled = !configuration.isFrozen; +} + +void DrawQuadVolume::run(const render::RenderContextPointer& renderContext, const glm::vec3 vertices[8], + const gpu::BufferView& indices, int indexCount) { + assert(renderContext->args); + assert(renderContext->args->_context); + if (_isUpdateEnabled) { + auto& streamVertices = _meshVertices.edit >(); + std::copy(vertices, vertices + 8, streamVertices.begin()); + } + + RenderArgs* args = renderContext->args; + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + args->_batch = &batch; + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + + glm::mat4 projMat; + Transform viewMat; + args->getViewFrustum().evalProjectionMatrix(projMat); + args->getViewFrustum().evalViewTransform(viewMat); + + batch.setProjectionTransform(projMat); + batch.setViewTransform(viewMat); + batch.setPipeline(getPipeline()); + batch.setIndexBuffer(indices); + + batch._glUniform4f(0, _color.x, _color.y, _color.z, 1.0f); + batch.setInputStream(0, _meshStream); + batch.drawIndexed(gpu::LINES, indexCount, 0U); + + args->_batch = nullptr; + }); +} + +gpu::PipelinePointer DrawQuadVolume::getPipeline() { + static gpu::PipelinePointer pipeline; + + if (!pipeline) { + auto vs = gpu::StandardShaderLib::getDrawTransformVertexPositionVS(); + auto ps = gpu::StandardShaderLib::getDrawColorPS(); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding("color", 0)); + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(true, false)); + pipeline = gpu::Pipeline::create(program, state); + } + return pipeline; +} + +gpu::BufferView DrawAABox::_cubeMeshIndices; + +DrawAABox::DrawAABox(const glm::vec3& color) : + DrawQuadVolume{ color } { +} + +void DrawAABox::run(const render::RenderContextPointer& renderContext, const Inputs& box) { + if (!box.isNull()) { + static const uint8_t indexData[] = { + 0, 1, + 1, 2, + 2, 3, + 3, 0, + 4, 5, + 5, 6, + 6, 7, + 7, 4, + 0, 4, + 1, 5, + 3, 7, + 2, 6 + }; + + if (!_cubeMeshIndices._buffer) { + auto indices = std::make_shared(sizeof(indexData), indexData); + _cubeMeshIndices = gpu::BufferView(indices, gpu::Element(gpu::SCALAR, gpu::UINT8, gpu::INDEX)); + } + + glm::vec3 vertices[8]; + + getVertices(box, vertices); + + DrawQuadVolume::run(renderContext, vertices, _cubeMeshIndices, sizeof(indexData) / sizeof(indexData[0])); + } +} + +void DrawAABox::getVertices(const AABox& box, glm::vec3 vertices[8]) { + vertices[0] = box.getVertex(TOP_LEFT_NEAR); + vertices[1] = box.getVertex(TOP_RIGHT_NEAR); + vertices[2] = box.getVertex(BOTTOM_RIGHT_NEAR); + vertices[3] = box.getVertex(BOTTOM_LEFT_NEAR); + vertices[4] = box.getVertex(TOP_LEFT_FAR); + vertices[5] = box.getVertex(TOP_RIGHT_FAR); + vertices[6] = box.getVertex(BOTTOM_RIGHT_FAR); + vertices[7] = box.getVertex(BOTTOM_LEFT_FAR); +} + gpu::BufferView DrawFrustum::_frustumMeshIndices; DrawFrustum::DrawFrustum(const glm::vec3& color) : - _color{ color } { - _frustumMeshVertices = gpu::BufferView(std::make_shared(sizeof(glm::vec3) * 8, nullptr), gpu::Element::VEC3F_XYZ); - _frustumMeshStream.addBuffer(_frustumMeshVertices._buffer, _frustumMeshVertices._offset, _frustumMeshVertices._stride); -} - -void DrawFrustum::configure(const Config& configuration) { - _updateFrustum = !configuration.isFrozen; + DrawQuadVolume{ color } { } void DrawFrustum::run(const render::RenderContextPointer& renderContext, const Input& input) { - assert(renderContext->args); - assert(renderContext->args->_context); - - RenderArgs* args = renderContext->args; if (input) { const auto& frustum = *input; - static uint8_t indexData[] = { 0, 1, 2, 3, 0, 4, 5, 6, 7, 4, 5, 1, 2, 6, 7, 3 }; + static const uint8_t indexData[] = { + 0, 1, + 1, 2, + 2, 3, + 3, 0, + 0, 2, + 3, 1, + 4, 5, + 5, 6, + 6, 7, + 7, 4, + 4, 6, + 7, 5, + 0, 4, + 1, 5, + 3, 7, + 2, 6 + }; if (!_frustumMeshIndices._buffer) { auto indices = std::make_shared(sizeof(indexData), indexData); _frustumMeshIndices = gpu::BufferView(indices, gpu::Element(gpu::SCALAR, gpu::UINT8, gpu::INDEX)); } - if (!_pipeline) { - auto vs = gpu::StandardShaderLib::getDrawTransformVertexPositionVS(); - auto ps = gpu::StandardShaderLib::getDrawColorPS(); - gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + glm::vec3 vertices[8]; - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding("color", 0)); - gpu::Shader::makeProgram(*program, slotBindings); + getVertices(frustum, vertices); - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setDepthTest(gpu::State::DepthTest(true, false)); - _pipeline = gpu::Pipeline::create(program, state); - } - - if (_updateFrustum) { - updateFrustum(frustum); - } - - // Render the frustums in wireframe - gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { - args->_batch = &batch; - batch.setViewportTransform(args->_viewport); - batch.setStateScissorRect(args->_viewport); - - glm::mat4 projMat; - Transform viewMat; - args->getViewFrustum().evalProjectionMatrix(projMat); - args->getViewFrustum().evalViewTransform(viewMat); - - batch.setProjectionTransform(projMat); - batch.setViewTransform(viewMat); - batch.setPipeline(_pipeline); - batch.setIndexBuffer(_frustumMeshIndices); - - batch._glUniform4f(0, _color.x, _color.y, _color.z, 1.0f); - batch.setInputStream(0, _frustumMeshStream); - batch.drawIndexed(gpu::LINE_STRIP, sizeof(indexData) / sizeof(indexData[0]), 0U); - - args->_batch = nullptr; - }); + DrawQuadVolume::run(renderContext, vertices, _frustumMeshIndices, sizeof(indexData) / sizeof(indexData[0])); } } -void DrawFrustum::updateFrustum(const ViewFrustum& frustum) { - auto& vertices = _frustumMeshVertices.edit >(); +void DrawFrustum::getVertices(const ViewFrustum& frustum, glm::vec3 vertices[8]) { vertices[0] = frustum.getNearTopLeft(); vertices[1] = frustum.getNearTopRight(); vertices[2] = frustum.getNearBottomRight(); diff --git a/libraries/render/src/render/DrawTask.h b/libraries/render/src/render/DrawTask.h index 5d98c37c21..6f98e3bef1 100755 --- a/libraries/render/src/render/DrawTask.h +++ b/libraries/render/src/render/DrawTask.h @@ -70,12 +70,12 @@ private: int _colorLocation { -1 }; }; -class DrawFrustumConfig : public render::JobConfig { +class DrawQuadVolumeConfig : public render::JobConfig { Q_OBJECT Q_PROPERTY(bool isFrozen MEMBER isFrozen NOTIFY dirty) public: - DrawFrustumConfig(bool enabled = false) : JobConfig(enabled) {} + DrawQuadVolumeConfig(bool enabled = false) : JobConfig(enabled) {} bool isFrozen{ false }; signals: @@ -83,30 +83,58 @@ signals: }; -class DrawFrustum { +class DrawQuadVolume { +public: + + using Config = DrawQuadVolumeConfig; + + void configure(const Config& configuration); + +protected: + DrawQuadVolume(const glm::vec3& color); + + void run(const render::RenderContextPointer& renderContext, const glm::vec3 vertices[8], + const gpu::BufferView& indices, int indexCount); + + gpu::BufferView _meshVertices; + gpu::BufferStream _meshStream; + glm::vec3 _color; + bool _isUpdateEnabled{ true }; + + static gpu::PipelinePointer getPipeline(); +}; + +class DrawAABox : public DrawQuadVolume { +public: + using Inputs = AABox; + using JobModel = render::Job::ModelI; + + DrawAABox(const glm::vec3& color = glm::vec3(1.0f, 1.0f, 1.0f)); + + void run(const render::RenderContextPointer& renderContext, const Inputs& box); + +protected: + + static gpu::BufferView _cubeMeshIndices; + + static void getVertices(const AABox& box, glm::vec3 vertices[8]); +}; + +class DrawFrustum : public DrawQuadVolume { public: - using Config = DrawFrustumConfig; using Input = ViewFrustumPointer; using JobModel = render::Job::ModelI; DrawFrustum(const glm::vec3& color = glm::vec3(1.0f, 1.0f, 1.0f)); - void configure(const Config& configuration); void run(const render::RenderContextPointer& renderContext, const Input& input); private: - static gpu::PipelinePointer _pipeline; static gpu::BufferView _frustumMeshIndices; - bool _updateFrustum{ true }; - gpu::BufferView _frustumMeshVertices; - gpu::BufferStream _frustumMeshStream; - glm::vec3 _color; - - void updateFrustum(const ViewFrustum& frustum); + static void getVertices(const ViewFrustum& frustum, glm::vec3 vertices[8]); }; - } #endif // hifi_render_DrawTask_h diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index 2b02db81f9..77f5910b9e 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -182,6 +182,8 @@ public: Builder& withoutLayered() { _value.reset(ItemKey::LAYERED); _mask.set(ItemKey::LAYERED); return (*this); } Builder& withLayered() { _value.set(ItemKey::LAYERED); _mask.set(ItemKey::LAYERED); return (*this); } + Builder& withNothing() { _value.reset(); _mask.reset(); return (*this); } + // Convenient standard keys that we will keep on using all over the place static Builder visibleWorldItems() { return Builder().withVisible().withWorldSpace(); } static Builder opaqueShape() { return Builder().withTypeShape().withOpaque().withWorldSpace(); } @@ -191,12 +193,14 @@ public: static Builder background() { return Builder().withViewSpace().withLayered(); } static Builder opaqueShapeLayered() { return Builder().withTypeShape().withOpaque().withWorldSpace().withLayered(); } static Builder transparentShapeLayered() { return Builder().withTypeShape().withTransparent().withWorldSpace().withLayered(); } + static Builder nothing() { return Builder().withNothing(); } }; ItemFilter(const Builder& builder) : ItemFilter(builder._value, builder._mask) {} // Item Filter operator testing if a key pass the filter bool test(const ItemKey& key) const { return (key._flags & _mask) == (_value & _mask); } + bool selectsNothing() const { return !_mask.any(); } class Less { public: diff --git a/libraries/render/src/render/RenderFetchCullSortTask.cpp b/libraries/render/src/render/RenderFetchCullSortTask.cpp index b9f65f48a0..d7294fa2bd 100644 --- a/libraries/render/src/render/RenderFetchCullSortTask.cpp +++ b/libraries/render/src/render/RenderFetchCullSortTask.cpp @@ -22,9 +22,11 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin // CPU jobs: // Fetch and cull the items from the scene - auto spatialFilter = ItemFilter::Builder::visibleWorldItems().withoutLayered(); + const ItemFilter filter = ItemFilter::Builder::visibleWorldItems().withoutLayered(); + const auto spatialFilter = render::Varying(filter); const auto spatialSelection = task.addJob("FetchSceneSelection", spatialFilter); - const auto culledSpatialSelection = task.addJob("CullSceneSelection", spatialSelection, cullFunctor, RenderDetails::ITEM, spatialFilter); + const auto cullInputs = CullSpatialSelection::Inputs(spatialSelection, spatialFilter).asVarying(); + const auto culledSpatialSelection = task.addJob("CullSceneSelection", cullInputs, cullFunctor, RenderDetails::ITEM); // Overlays are not culled const auto nonspatialSelection = task.addJob("FetchOverlaySelection"); diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index 762b7712d7..4254280fa1 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -87,6 +87,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), Slot::MAP::LIGHT_AMBIENT)); slotBindings.insert(gpu::Shader::Binding(std::string("fadeMaskMap"), Slot::MAP::FADE_MASK)); slotBindings.insert(gpu::Shader::Binding(std::string("fadeParametersBuffer"), Slot::BUFFER::FADE_PARAMETERS)); + slotBindings.insert(gpu::Shader::Binding(std::string("hazeParametersBuffer"), Slot::BUFFER::HAZE_MODEL)); gpu::Shader::makeProgram(*program, slotBindings); @@ -107,6 +108,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p locations->lightAmbientMapUnit = program->getTextures().findLocation("skyboxMap"); locations->fadeMaskTextureUnit = program->getTextures().findLocation("fadeMaskMap"); locations->fadeParameterBufferUnit = program->getUniformBuffers().findLocation("fadeParametersBuffer"); + locations->hazeParameterBufferUnit = program->getUniformBuffers().findLocation("hazeParametersBuffer"); ShapeKey key{filter._flags}; auto gpuPipeline = gpu::Pipeline::create(program, state); diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index f0749504eb..101c060306 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -238,6 +238,7 @@ public: LIGHT, LIGHT_AMBIENT_BUFFER, FADE_PARAMETERS, + HAZE_MODEL }; enum MAP { @@ -270,6 +271,7 @@ public: int lightAmbientMapUnit; int fadeMaskTextureUnit; int fadeParameterBufferUnit; + int hazeParameterBufferUnit; }; using LocationsPointer = std::shared_ptr; diff --git a/libraries/render/src/render/SortTask.cpp b/libraries/render/src/render/SortTask.cpp index 63673a71a5..5b53b5d403 100644 --- a/libraries/render/src/render/SortTask.cpp +++ b/libraries/render/src/render/SortTask.cpp @@ -75,7 +75,6 @@ void render::depthSortItems(const RenderContextPointer& renderContext, bool fron std::sort(itemBoundSorts.begin(), itemBoundSorts.end(), backToFrontSort); } - // Finally once sorted result to a list of itemID // Finally once sorted result to a list of itemID and keep uniques render::ItemID previousID = Item::INVALID_ITEM_ID; if (!bounds) { diff --git a/libraries/script-engine/src/MenuItemProperties.cpp b/libraries/script-engine/src/MenuItemProperties.cpp index de76dbc3ca..40254eeccb 100644 --- a/libraries/script-engine/src/MenuItemProperties.cpp +++ b/libraries/script-engine/src/MenuItemProperties.cpp @@ -51,22 +51,24 @@ QScriptValue menuItemPropertiesToScriptValue(QScriptEngine* engine, const MenuIt } /**jsdoc - * `MenuItemProperties` is a list of properties that can be passed to Menu.addMenuItem - * to create a new menu item. + * A set of properties that can be passed to {@link Menu.addMenuItem} to create a new menu item. * - * If none of position, beforeItem, afterItem, or grouping are specified, the - * menu item will be placed in the last position. + * If none of position, beforeItem, afterItem, or grouping are specified, + * the menu item will be placed at the end of the menu. * - * @typedef {Object} Menu.MenuItemProperties - * @property {string} menuName Name of the top-level menu - * @property {string} menuItemName Name of the menu item - * @property {bool} isCheckable Whether the menu item is checkable or not - * @property {bool} isChecked Where the menu item is checked or not - * @property {string} shortcutKey An optional shortcut key to trigger the menu item. - * @property {int} position The position to place the new menu item. `0` is the first menu item. - * @property {string} beforeItem The name of the menu item to place this menu item before. - * @property {string} afterItem The name of the menu item to place this menu item after. - * @property {string} grouping The name of grouping to add this menu item to. + * @typedef {object} Menu.MenuItemProperties + * @property {string} menuName Name of the menu. Nested menus can be described using the ">" symbol. + * @property {string} menuItemName Name of the menu item. + * @property {boolean} [isCheckable=false] Whether or not the menu item is checkable. + * @property {boolean} [isChecked=false] Whether or not the menu item is checked. + * @property {boolean} [isSeparator=false] Whether or not the menu item is a separator. + * @property {string} [shortcutKey] A shortcut key that triggers the menu item. + * @property {KeyEvent} [shortcutKeyEvent] A {@link KeyEvent} that specifies a key that triggers the menu item. + * @property {number} [position] The position to place the new menu item. An integer number with 0 being the first + * menu item. + * @property {string} [beforeItem] The name of the menu item to place this menu item before. + * @property {string} [afterItem] The name of the menu item to place this menu item after. + * @property {string} [grouping] The name of grouping to add this menu item to. */ void menuItemPropertiesFromScriptValue(const QScriptValue& object, MenuItemProperties& properties) { properties.menuName = object.property("menuName").toVariant().toString(); diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index d3fb2ed5c2..23e8e3b896 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -21,13 +21,24 @@ #include /**jsdoc - * A Quaternion - * - * @typedef Quat - * @property {float} x imaginary component i. - * @property {float} y imaginary component j. - * @property {float} z imaginary component k. - * @property {float} w real component. + * A quaternion value. See also the {@link Quat(0)|Quat} object. + * @typedef {object} Quat + * @property {number} x - Imaginary component i. + * @property {number} y - Imaginary component j. + * @property {number} z - Imaginary component k. + * @property {number} w - Real component. + */ + +/**jsdoc + * The Quat API provides facilities for generating and manipulating quaternions. + * Quaternions should be used in preference to Euler angles wherever possible because quaternions don't suffer from the problem + * of gimbal lock. + * @namespace Quat + * @variation 0 + * @property IDENTITY {Quat} The identity rotation, i.e., no rotation. + * @example Print the IDENTITY value. + * print(JSON.stringify(Quat.IDENTITY)); // { x: 0, y: 0, z: 0, w: 1 } + * print(JSON.stringify(Quat.safeEulerAngles(Quat.IDENTITY))); // { x: 0, y: 0, z: 0 } */ /// Scriptable interface a Quaternion helper class object. Used exclusively in the JavaScript API @@ -36,33 +47,408 @@ class Quat : public QObject, protected QScriptable { Q_PROPERTY(glm::quat IDENTITY READ IDENTITY CONSTANT) public slots: + + /**jsdoc + * Multiply two quaternions. + * @function Quat(0).multiply + * @param {Quat} q1 - The first quaternion. + * @param {Quat} q2 - The second quaternion. + * @returns {Quat} q1 multiplied with q2. + * @example Calculate the orientation of your avatar's right hand in world coordinates. + * var handController = Controller.Standard.RightHand; + * var handPose = Controller.getPoseValue(handController); + * if (handPose.valid) { + * var handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); + * } + */ glm::quat multiply(const glm::quat& q1, const glm::quat& q2); + + /**jsdoc + * Normalizes a quaternion. + * @function Quat(0).normalize + * @param {Quat} q - The quaternion to normalize. + * @returns {Quat} q normalized to have unit length. + * @example Normalize a repeated delta rotation so that maths rounding errors don't accumulate. + * var deltaRotation = Quat.fromPitchYawRollDegrees(0, 0.1, 0); + * var currentRotation = Quat.ZERO; + * while (Quat.safeEulerAngles(currentRotation).y < 180) { + * currentRotation = Quat.multiply(deltaRotation, currentRotation); + * currentRotation = Quat.normalize(currentRotation); + * // Use currentRotatation for something. + * } + */ glm::quat normalize(const glm::quat& q); + + /**jsdoc + * Calculate the conjugate of a quaternion. For a unit quaternion, its conjugate is the same as its + * {@link Quat(0).inverse|Quat.inverse}. + * @function Quat(0).conjugate + * @param {Quat} q - The quaternion to conjugate. + * @returns {Quat} The conjugate of q. + * @example A unit quaternion multiplied by its conjugate is a zero rotation. + * var quaternion = Quat.fromPitchYawRollDegrees(10, 20, 30); + * Quat.print("quaternion", quaternion, true); // dvec3(10.000000, 20.000004, 30.000004) + * var conjugate = Quat.conjugate(quaternion); + * Quat.print("conjugate", conjugate, true); // dvec3(1.116056, -22.242186, -28.451778) + * var identity = Quat.multiply(conjugate, quaternion); + * Quat.print("identity", identity, true); // dvec3(0.000000, 0.000000, 0.000000) + */ glm::quat conjugate(const glm::quat& q); + + /**jsdoc + * Calculate a camera orientation given eye position, point of interest, and "up" direction. The camera's negative z-axis is + * the forward direction. The result has zero roll about its forward direction with respect to the given "up" direction. + * @function Quat(0).lookAt + * @param {Vec3} eye - The eye position. + * @param {Vec3} target - The point to look at. + * @param {Vec3} up - The "up" direction. + * @returns {Quat} A quaternion that orients the negative z-axis to point along the eye-to-target vector and the x-axis to + * be the cross product of the eye-to-target and up vectors. + * @example Rotate your view in independent mode to look at the world origin upside down. + * Camera.mode = "independent"; + * Camera.orientation = Quat.lookAt(Camera.position, Vec3.ZERO, Vec3.UNIT_NEG_Y); + */ glm::quat lookAt(const glm::vec3& eye, const glm::vec3& center, const glm::vec3& up); + + /**jsdoc + * Calculate a camera orientation given eye position and point of interest. The camera's negative z-axis is the forward + * direction. The result has zero roll about its forward direction. + * @function Quat(0).lookAtSimple + * @param {Vec3} eye - The eye position. + * @param {Vec3} target - The point to look at. + * @returns {Quat} A quaternion that orients the negative z-axis to point along the eye-to-target vector and the x-axis to be + * the cross product of the eye-to-target and an "up" vector. The "up" vector is the y-axis unless the eye-to-target + * vector is nearly aligned with it (i.e., looking near vertically up or down), in which case the x-axis is used as the + * "up" vector. + * @example Rotate your view in independent mode to look at the world origin. + * Camera.mode = "independent"; + * Camera.orientation = Quat.lookAtSimple(Camera.position, Vec3.ZERO); + */ glm::quat lookAtSimple(const glm::vec3& eye, const glm::vec3& center); + + /**jsdoc + * Calculate the shortest rotation from a first vector onto a second. + * @function Quat(0).rotationBetween + * @param {Vec3} v1 - The first vector. + * @param {Vec3} v2 - The second vector. + * @returns {Quat} The rotation from v1 onto v2. + * @example Apply a change in velocity to an entity and rotate it to face the direction it's travelling. + * var newVelocity = Vec3.sum(entityVelocity, deltaVelocity); + * var properties = { velocity: newVelocity }; + * if (Vec3.length(newVelocity) > 0.001) { + * properties.rotation = Quat.rotationBetween(entityVelocity, newVelocity); + * } + * Entities.editEntity(entityID, properties); + * entityVelocity = newVelocity; + */ glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2); - glm::quat fromVec3Degrees(const glm::vec3& vec3); // degrees - glm::quat fromVec3Radians(const glm::vec3& vec3); // radians - glm::quat fromPitchYawRollDegrees(float pitch, float yaw, float roll); // degrees - glm::quat fromPitchYawRollRadians(float pitch, float yaw, float roll); // radians + + /**jsdoc + * Generate a quaternion from a {@link Vec3} of Euler angles in degrees. + * @function Quat(0).fromVec3Degrees + * @param {Vec3} vector - A vector of three Euler angles in degrees, the angles being the rotations about the x, y, and z + * axes. + * @returns {Quat} A quaternion created from the Euler angles in vector. + * @example Zero out pitch and roll from an orientation. + * var eulerAngles = Quat.safeEulerAngles(orientation); + * eulerAngles.x = 0; + * eulerAngles.z = 0; + * var newOrientation = Quat.fromVec3Degrees(eulerAngles); + */ + glm::quat fromVec3Degrees(const glm::vec3& vec3); + + /**jsdoc + * Generate a quaternion from a {@link Vec3} of Euler angles in radians. + * @function Quat(0).fromVec3Radians + * @param {Vec3} vector - A vector of three Euler angles in radians, the angles being the rotations about the x, y, and z + * axes. + * @returns {Quat} A quaternion created using the Euler angles in vector. + * @example Create a rotation of 180 degrees about the y axis. + * var rotation = Quat.fromVec3Radians({ x: 0, y: Math.PI, z: 0 }); + */ + glm::quat fromVec3Radians(const glm::vec3& vec3); + + /**jsdoc + * Generate a quaternion from pitch, yaw, and roll values in degrees. + * @function Quat(0).fromPitchYawRollDegrees + * @param {number} pitch - The pitch angle in degrees. + * @param {number} yaw - The yaw angle in degrees. + * @param {number} roll - The roll angle in degrees. + * @returns {Quat} A quaternion created using the pitch, yaw, and roll Euler angles. + * @example Create a rotation of 180 degrees about the y axis. + * var rotation = Quat.fromPitchYawRollDgrees(0, 180, 0 ); + */ + glm::quat fromPitchYawRollDegrees(float pitch, float yaw, float roll); + + /**jsdoc + * Generate a quaternion from pitch, yaw, and roll values in radians. + * @function Quat(0).fromPitchYawRollRadians + * @param {number} pitch - The pitch angle in radians. + * @param {number} yaw - The yaw angle in radians. + * @param {number} roll - The roll angle in radians. + * @returns {Quat} A quaternion created from the pitch, yaw, and roll Euler angles. + * @example Create a rotation of 180 degrees about the y axis. + * var rotation = Quat.fromPitchYawRollRadians(0, Math.PI, 0); + */ + glm::quat fromPitchYawRollRadians(float pitch, float yaw, float roll); + + /**jsdoc + * Calculate the inverse of a quaternion. For a unit quaternion, its inverse is the same as its + * {@link Quat(0).conjugate|Quat.conjugate}. + * @function Quat(0).inverse + * @param {Quat} q - The quaternion. + * @returns {Quat} The inverse of q. + * @example A quaternion multiplied by its inverse is a zero rotation. + * var quaternion = Quat.fromPitchYawRollDegrees(10, 20, 30); + * Quat.print("quaternion", quaternion, true); // dvec3(10.000000, 20.000004, 30.000004) + * var inverse = Quat.invserse(quaternion); + * Quat.print("inverse", inverse, true); // dvec3(1.116056, -22.242186, -28.451778) + * var identity = Quat.multiply(inverse, quaternion); + * Quat.print("identity", identity, true); // dvec3(0.000000, 0.000000, 0.000000) + */ glm::quat inverse(const glm::quat& q); - // redundant, calls getForward which better describes the returned vector as a direction + + /**jsdoc + * Get the "front" direction that the camera would face if its orientation was set to the quaternion value. + * This is a synonym for {@link Quat(0).getForward|Quat.getForward}. + * The High Fidelity camera has axes x = right, y = up, -z = forward. + * @function Quat(0).getFront + * @param {Quat} orientation - A quaternion representing an orientation. + * @returns {Vec3} The negative z-axis rotated by orientation. + */ glm::vec3 getFront(const glm::quat& orientation) { return getForward(orientation); } + + /**jsdoc + * Get the "forward" direction that the camera would face if its orientation was set to the quaternion value. + * This is a synonym for {@link Quat(0).getFront|Quat.getFront}. + * The High Fidelity camera has axes x = right, y = up, -z = forward. + * @function Quat(0).getForward + * @param {Quat} orientation - A quaternion representing an orientation. + * @returns {Vec3} The negative z-axis rotated by orientation. + * @example Demonstrate that the "forward" vector is for the negative z-axis. + * var forward = Quat.getForward(Quat.IDENTITY); + * print(JSON.stringify(forward)); // {"x":0,"y":0,"z":-1} + */ glm::vec3 getForward(const glm::quat& orientation); + + /**jsdoc + * Get the "right" direction that the camera would have if its orientation was set to the quaternion value. + * The High Fidelity camera has axes x = right, y = up, -z = forward. + * @function Quat(0).getRight + * @param {Quat} orientation - A quaternion representing an orientation. + * @returns {Vec3} The x-axis rotated by orientation. + */ glm::vec3 getRight(const glm::quat& orientation); + + /**jsdoc + * Get the "up" direction that the camera would have if its orientation was set to the quaternion value. + * The High Fidelity camera has axes x = right, y = up, -z = forward. + * @function Quat(0).getUp + * @param {Quat} orientation - A quaternion representing an orientation. + * @returns {Vec3} The y-axis rotated by orientation. + */ glm::vec3 getUp(const glm::quat& orientation); - glm::vec3 safeEulerAngles(const glm::quat& orientation); // degrees - glm::quat angleAxis(float angle, const glm::vec3& v); // degrees + + /**jsdoc + * Calculate the Euler angles for the quaternion, in degrees. (The "safe" in the name signifies that the angle results will + * not be garbage even when the rotation is particularly difficult to decompose with pitches around +/-90 degrees.) + * @function Quat(0).safeEulerAngles + * @param {Quat} orientation - A quaternion representing an orientation. + * @returns {Vec3} A {@link Vec3} of Euler angles for the orientation, in degrees, the angles being the + * rotations about the x, y, and z axes. + * @example Report the camera yaw. + * var eulerAngles = Quat.safeEulerAngles(Camera.orientation); + * print("Camera yaw: " + eulerAngles.y); + */ + glm::vec3 safeEulerAngles(const glm::quat& orientation); + + /**jsdoc + * Generate a quaternion given an angle to rotate through and an axis to rotate about. + * @function Quat(0).angleAxis + * @param {number} angle - The angle to rotate through, in degrees. + * @param {Vec3} axis - The unit axis to rotate about. + * @returns {Quat} A quaternion that is a rotation through angle degrees about the axis. + * WARNING: This value is in degrees whereas the value returned by {@link Quat(0).angle|Quat.angle} is + * in radians. + * @example Calculate a rotation of 90 degrees about the direction your camera is looking. + * var rotation = Quat.angleAxis(90, Quat.getForward(Camera.orientation)); + */ + glm::quat angleAxis(float angle, const glm::vec3& v); + + /**jsdoc + * Get the rotation axis for a quaternion. + * @function Quat(0).axis + * @param {Quat} q - The quaternion. + * @returns {Vec3} The normalized rotation axis for q. + * @example Get the rotation axis of a quaternion. + * var forward = Quat.getForward(Camera.orientation); + * var rotation = Quat.angleAxis(90, forward); + * var axis = Quat.axis(rotation); + * print("Forward: " + JSON.stringify(forward)); + * print("Axis: " + JSON.stringify(axis)); // Same value as forward. + */ glm::vec3 axis(const glm::quat& orientation); + + /**jsdoc + * Get the rotation angle for a quaternion. + * @function Quat(0).angle + * @param {Quat} q - The quaternion. + * @returns {number} The rotation angle for q, in radians. WARNING: This value is in radians + * whereas the value used by {@link Quat(0).angleAxis|Quat.angleAxis} is in degrees. + * @example Get the rotation angle of a quaternion. + * var forward = Quat.getForward(Camera.orientation); + * var rotation = Quat.angleAxis(90, forward); + * var angle = Quat.angle(rotation); + * print("Angle: " + angle * 180 / Math.PI); // 90 degrees. + */ float angle(const glm::quat& orientation); + + // spherical linear interpolation + // alpha: 0.0 to 1.0? + /**jsdoc + * Compute a spherical linear interpolation between two rotations, safely handling two rotations that are very similar. + * See also, {@link Quat(0).slerp|Quat.slerp}. + * @function Quat(0).mix + * @param {Quat} q1 - The beginning rotation. + * @param {Quat} q2 - The ending rotation. + * @param {number} alpha - The mixture coefficient between 0.0 and 1.0. Specifies the proportion + * of q2's value to return in favor of q1's value. A value of 0.0 returns + * q1's value; 1.0 returns q2s's value. + * @returns {Quat} A spherical linear interpolation between rotations q1 and q2. + * @example Animate between one rotation and another. + * var dt = amountOfTimeThatHasPassed; + * var mixFactor = amountOfTimeThatHasPassed / TIME_TO_COMPLETE; + * if (mixFactor > 1) { + * mixFactor = 1; + * } + * var newRotation = Quat.mix(startRotation, endRotation, mixFactor); + */ glm::quat mix(const glm::quat& q1, const glm::quat& q2, float alpha); + + /**jsdoc + * Compute a spherical linear interpolation between two rotations, for rotations that are not very similar. + * See also, {@link Quat(0).mix|Quat.mix}. + * @function Quat(0).slerp + * @param {Quat} q1 - The beginning rotation. + * @param {Quat} q2 - The ending rotation. + * @param {number} alpha - The mixture coefficient between 0.0 and 1.0. Specifies the proportion + * of q2's value to return in favor of q1's value. A value of 0.0 returns + * q1's value; 1.0 returns q2s's value. + * @returns {Quat} A spherical linear interpolation between rotations q1 and q2. + */ glm::quat slerp(const glm::quat& q1, const glm::quat& q2, float alpha); + + /**jsdoc + * Compute a spherical quadrangle interpolation between two rotations along a path oriented toward two other rotations. + * Equivalent to: Quat.slerp(Quat.slerp(q1, q2, alpha), Quat.slerp(s1, s2, alpha), 2 * alpha * (1.0 - alpha)). + * @function Quat(0).squad + * @param {Quat} q1 - Initial rotation. + * @param {Quat} q2 - Final rotation. + * @param {Quat} s1 - First control point. + * @param {Quat} s2 - Second control point. + * @param {number} alpha - The mixture coefficient between 0.0 and 1.0. A value of + * 0.0 returns q1's value; 1.0 returns q2s's value. + * @returns {Quat} A spherical quadrangle interpolation between rotations q1 and q2 using control + * points s1 and s2. + */ glm::quat squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& s1, const glm::quat& s2, float h); + + /**jsdoc + * Calculate the dot product of two quaternions. The closer the quaternions are to each other the more non-zero the value is + * (either positive or negative). Identical unit rotations have a dot product of +/- 1. + * @function Quat(0).dot + * @param {Quat} q1 - The first quaternion. + * @param {Quat} q2 - The second quaternion. + * @returns {number} The dot product of q1 and q2. + * @example Testing unit quaternions for equality. + * var q1 = Quat.fromPitchYawRollDegrees(0, 0, 0); + * var q2 = Quat.fromPitchYawRollDegrees(0, 0, 0); + * print(Quat.equal(q1, q2)); // true + * var q3 = Quat.fromPitchYawRollDegrees(0, 0, 359.95); + * print(Quat.equal(q1, q3)); // false + * + * var dot = Quat.dot(q1, q3); + * print(dot); // -0.9999999403953552 + * var equal = Math.abs(1 - Math.abs(dot)) < 0.000001; + * print(equal); // true + */ float dot(const glm::quat& q1, const glm::quat& q2); + + /**jsdoc + * Print to the program log a text label followed by a quaternion's pitch, yaw, and roll Euler angles. + * @function Quat(0).print + * @param {string} label - The label to print. + * @param {Quat} q - The quaternion to print. + * @param {boolean} [asDegrees=false] - If true the angle values are printed in degrees, otherwise they are + * printed in radians. + * @example Two ways of printing a label plus a quaternion's Euler angles. + * var quaternion = Quat.fromPitchYawRollDegrees(0, 45, 0); + * + * // Quaternion: dvec3(0.000000, 45.000004, 0.000000) + * Quat.print("Quaternion:", quaternion, true); + * + * // Quaternion: {"x":0,"y":45.000003814697266,"z":0} + * print("Quaternion: " + JSON.stringify(Quat.safeEulerAngles(quaternion))); + */ void print(const QString& label, const glm::quat& q, bool asDegrees = false); + + /**jsdoc + * Test whether two quaternions are equal. Note: The quaternions must be exactly equal in order for + * true to be returned; it is often better to use {@link Quat(0).dot|Quat.dot} and test for closeness to +/-1. + * @function Quat(0).equal + * @param {Quat} q1 - The first quaternion. + * @param {Quat} q2 - The second quaternion. + * @returns {boolean} true if the quaternions are equal, otherwise false. + * @example Testing unit quaternions for equality. + * var q1 = Quat.fromPitchYawRollDegrees(0, 0, 0); + * var q2 = Quat.fromPitchYawRollDegrees(0, 0, 0); + * print(Quat.equal(q1, q2)); // true + * var q3 = Quat.fromPitchYawRollDegrees(0, 0, 359.95); + * print(Quat.equal(q1, q3)); // false + * + * var dot = Quat.dot(q1, q3); + * print(dot); // -0.9999999403953552 + * var equal = Math.abs(1 - Math.abs(dot)) < 0.000001; + * print(equal); // true + */ bool equal(const glm::quat& q1, const glm::quat& q2); + + /**jsdoc + * Cancels out the roll and pitch component of a quaternion so that its completely horizontal with a yaw pointing in the + * given quaternion's direction. + * @function Quat(0).cancelOutRollAndPitch + * @param {Quat} orientation - A quaternion representing an orientation. + * @returns {Quat} orientation with its roll and pitch canceled out. + * @example Two ways of calculating a camera orientation in the x-z plane with a yaw pointing in the direction of + * a given quaternion. + * var quaternion = Quat.fromPitchYawRollDegrees(10, 20, 30); + * + * var noRollOrPitch = Quat.cancelOutRollAndPitch(quaternion); + * Quat.print("", noRollOrPitch, true); // dvec3(0.000000, 22.245995, 0.000000) + * + * var front = Quat.getFront(quaternion); + * var lookAt = Quat.lookAtSimple(Vec3.ZERO, { x: front.x, y: 0, z: front.z }); + * Quat.print("", lookAt, true); // dvec3(0.000000, 22.245996, 0.000000) + * + */ glm::quat cancelOutRollAndPitch(const glm::quat& q); + + /**jsdoc + * Cancels out the roll component of a quaternion so that its horizontal axis is level. + * @function Quat(0).cancelOutRoll + * @param {Quat} orientation - A quaternion representing an orientation. + * @returns {Quat} orientation with its roll canceled out. + * @example Two ways of calculating a camera orientation that points in the direction of a given quaternion but + * keeps the camera's horizontal axis level. + * var quaternion = Quat.fromPitchYawRollDegrees(10, 20, 30); + * + * var noRoll = Quat.cancelOutRoll(quaternion); + * Quat.print("", noRoll, true); // dvec3(-1.033004, 22.245996, -0.000000) + * + * var front = Quat.getFront(quaternion); + * var lookAt = Quat.lookAtSimple(Vec3.ZERO, front); + * Quat.print("", lookAt, true); // dvec3(-1.033004, 22.245996, -0.000000) + */ glm::quat cancelOutRoll(const glm::quat& q); private: diff --git a/libraries/script-engine/src/ScriptUUID.h b/libraries/script-engine/src/ScriptUUID.h index 3f504c35ba..303a871d1d 100644 --- a/libraries/script-engine/src/ScriptUUID.h +++ b/libraries/script-engine/src/ScriptUUID.h @@ -17,17 +17,100 @@ #include #include +/**jsdoc + * A UUID (Universally Unique IDentifier) is used to uniquely identify entities, overlays, avatars, and the like. It is + * represented in JavaScript as a string in the format, {nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn}, where the "n"s are + * hexadecimal digits. + * + * @namespace Uuid + * @property NULL {Uuid} The null UUID, {00000000-0000-0000-0000-000000000000}. + */ + /// Scriptable interface for a UUID helper class object. Used exclusively in the JavaScript API class ScriptUUID : public QObject, protected QScriptable { Q_OBJECT Q_PROPERTY(QString NULL READ NULL_UUID CONSTANT) // String for use in scripts. public slots: + /**jsdoc + * Generates a UUID from a string representation of the UUID. + * @function Uuid.fromString + * @param {string} string - A string representation of a UUID. The curly braces are optional. + * @returns {Uuid} A UUID if the given string is valid, null otherwise. + * @example Valid and invalid parameters. + * var uuid = Uuid.fromString("{527c27ea-6d7b-4b47-9ae2-b3051d50d2cd}"); + * print(uuid); // {527c27ea-6d7b-4b47-9ae2-b3051d50d2cd} + * + * uuid = Uuid.fromString("527c27ea-6d7b-4b47-9ae2-b3051d50d2cd"); + * print(uuid); // {527c27ea-6d7b-4b47-9ae2-b3051d50d2cd} + * + * uuid = Uuid.fromString("527c27ea"); + * print(uuid); // null + */ QUuid fromString(const QString& string); + + /**jsdoc + * Generates a string representation of a UUID. However, because UUIDs are represented in JavaScript as strings, this is in + * effect a no-op. + * @function Uuid.toString + * @param {Uuid} id - The UUID to generate a string from. + * @returns {string} - A string representation of the UUID. + */ QString toString(const QUuid& id); + + /**jsdoc + * Generate a new UUID. + * @function Uuid.generate + * @returns {Uuid} A new UUID. + * @example Generate a new UUID and reports its JavaScript type. + * var uuid = Uuid.generate(); + * print(uuid); // {nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn} + * print(typeof uuid); // string + */ QUuid generate(); + + /**jsdoc + * Test whether two given UUIDs are equal. + * @function Uuid.isEqual + * @param {Uuid} idA - The first UUID to compare. + * @param {Uuid} idB - The second UUID to compare. + * @returns {boolean} true if the two UUIDs are equal, otherwise false. + * @example Demonstrate true and false cases. + * var uuidA = Uuid.generate(); + * var uuidB = Uuid.generate(); + * print(Uuid.isEqual(uuidA, uuidB)); // false + * uuidB = uuidA; + * print(Uuid.isEqual(uuidA, uuidB)); // true + */ bool isEqual(const QUuid& idA, const QUuid& idB); + + /**jsdoc + * Test whether a given UUID is null. + * @function Uuid.isNull + * @param {Uuid} id - The UUID to test. + * @returns {boolean} true if the UUID equals Uuid.NULL or is null, otherwise false. + * @example Demonstrate true and false cases. + * var uuid; // undefined + * print(Uuid.isNull(uuid)); // false + * uuid = Uuid.generate(); + * print(Uuid.isNull(uuid)); // false + * uuid = Uuid.NULL; + * print(Uuid.isNull(uuid)); // true + * uuid = null; + * print(Uuid.isNull(uuid)); // true + */ bool isNull(const QUuid& id); + + /**jsdoc + * Print to the program log a text label followed by the UUID value. + * @function Uuid.print + * @param {string} label - The label to print. + * @param {Uuid} id - The UUID to print. + * @example Two ways of printing a label plus UUID. + * var uuid = Uuid.generate(); + * Uuid.print("Generated UUID:", uuid); // Generated UUID: {nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn} + * print("Generated UUID: " + uuid); // Generated UUID: {nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn} + */ void print(const QString& label, const QUuid& id); private: diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index c7179a80c0..1e1cd917a3 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -24,7 +24,7 @@ /**jsdoc * A 2-dimensional vector. * - * @typedef Vec2 + * @typedef {object} Vec2 * @property {float} x X-coordinate of the vector. * @property {float} y Y-coordinate of the vector. */ @@ -32,7 +32,7 @@ /**jsdoc * A 3-dimensional vector. * - * @typedef Vec3 + * @typedef {object} Vec3 * @property {float} x X-coordinate of the vector. * @property {float} y Y-coordinate of the vector. * @property {float} z Z-coordinate of the vector. @@ -41,7 +41,7 @@ /**jsdoc * A 4-dimensional vector. * - * @typedef Vec4 + * @typedef {object} Vec4 * @property {float} x X-coordinate of the vector. * @property {float} y Y-coordinate of the vector. * @property {float} z Z-coordinate of the vector. diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 1d3c8fda32..62384f9d97 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -22,7 +22,7 @@ #include "ScriptEngine.h" #include "XMLHttpRequestClass.h" -const QString METAVERSE_API_URL = NetworkingConstants::METAVERSE_SERVER_URL.toString() + "/api/"; +const QString METAVERSE_API_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/api/"; Q_DECLARE_METATYPE(QByteArray*) diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index e502d44a08..956c61deaf 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -338,8 +338,6 @@ int clipTriangleWithPlane(const Triangle& triangle, const Plane& plane, Triangle int clippedTriangleCount = 0; int i; - assert(clippedTriangleCount > 0); - for (i = 0; i < 3; i++) { pointDistanceToPlane[i] = plane.distance(triangleVertices[i]); arePointsClipped.set(i, pointDistanceToPlane[i] < 0.0f); @@ -424,7 +422,7 @@ int clipTriangleWithPlanes(const Triangle& triangle, const Plane* planes, int pl *clippedTriangles = triangle; - while (planes < planesEnd) { + while (planes < planesEnd && triangleCount) { int clippedSubTriangleCount; trianglesToTest.clear(); diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 6f2e7d8b19..b33b330fc0 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -30,9 +30,30 @@ QString TEMP_DIR_FORMAT { "%1-%2-%3" }; const QString& PathUtils::resourcesPath() { #ifdef Q_OS_MAC - static QString staticResourcePath = QCoreApplication::applicationDirPath() + "/../Resources/"; + static const QString staticResourcePath = QCoreApplication::applicationDirPath() + "/../Resources/"; #else - static QString staticResourcePath = QCoreApplication::applicationDirPath() + "/resources/"; + static const QString staticResourcePath = QCoreApplication::applicationDirPath() + "/resources/"; +#endif + return staticResourcePath; +} + +#ifdef DEV_BUILD +const QString& PathUtils::projectRootPath() { + static QString sourceFolder; + static std::once_flag once; + std::call_once(once, [&] { + QDir thisDir = QFileInfo(__FILE__).absoluteDir(); + sourceFolder = QDir::cleanPath(thisDir.absoluteFilePath("../../../")); + }); + return sourceFolder; +} +#endif + +const QString& PathUtils::qmlBasePath() { +#ifdef DEV_BUILD + static const QString staticResourcePath = QUrl::fromLocalFile(projectRootPath() + "/interface/resources/qml/").toString(); +#else + static const QString staticResourcePath = "qrc:///qml/"; #endif return staticResourcePath; diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index f3ba5910c4..2b4fe35d97 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -19,9 +19,13 @@ #include "DependencyManager.h" /**jsdoc + * The Paths API provides absolute paths to the scripts and resources directories. + * * @namespace Paths + * @deprecated The Paths API is deprecated. Use {@link Script.resolvePath} and {@link Script.resourcesPath} instead. * @readonly - * @property {string} resources The path to the resources directory. + * @property {string} defaultScripts - The path to the scripts directory. Read-only. + * @property {string} resources - The path to the resources directory. Read-only. */ class PathUtils : public QObject, public Dependency { Q_OBJECT @@ -30,6 +34,10 @@ class PathUtils : public QObject, public Dependency { Q_PROPERTY(QUrl defaultScripts READ defaultScriptsLocation CONSTANT) public: static const QString& resourcesPath(); + static const QString& qmlBasePath(); +#ifdef DEV_BUILD + static const QString& projectRootPath(); +#endif static QString getAppDataPath(); static QString getAppLocalDataPath(); diff --git a/libraries/shared/src/PointerEvent.cpp b/libraries/shared/src/PointerEvent.cpp index f0f868655c..c07de18032 100644 --- a/libraries/shared/src/PointerEvent.cpp +++ b/libraries/shared/src/PointerEvent.cpp @@ -65,6 +65,64 @@ void PointerEvent::setButton(Button button) { _buttons |= button; } +/**jsdoc + * A PointerEvent defines a 2D or 3D mouse or similar pointer event. + * @typedef {object} PointerEvent + * @property {string} type - The type of event: "Press", "DoublePress", "Release", or + * "Move". + * @property {number} id - Integer number used to identify the pointer: 0 = hardware mouse, 1 = left + * controller, 2 = right controller. + * @property {Vec2} pos2D - The 2D position of the event on the intersected overlay or entity XY plane, where applicable. + * @property {Vec3} pos3D - The 3D position of the event on the intersected overlay or entity, where applicable. + * @property {Vec3} normal - The surface normal at the intersection point. + * @property {Vec3} direction - The direction of the intersection ray. + * @property {string} button - The name of the button pressed: None, Primary, Secondary, + * or Tertiary. + * @property {boolean} isPrimaryButton - true if the button pressed was the primary button, otherwise + * undefined; + * @property {boolean} isLeftButton - true if the button pressed was the primary button, otherwise + * undefined; + * @property {boolean} isSecondaryButton - true if the button pressed was the secondary button, otherwise + * undefined; + * @property {boolean} isRightButton - true if the button pressed was the secondary button, otherwise + * undefined; + * @property {boolean} isTertiaryButton - true if the button pressed was the tertiary button, otherwise + * undefined; + * @property {boolean} isMiddleButton - true if the button pressed was the tertiary button, otherwise + * undefined; + * @property {boolean} isPrimaryHeld - true if the primary button is currently being pressed, otherwise + * false + * @property {boolean} isSecondaryHeld - true if the secondary button is currently being pressed, otherwise + * false + * @property {boolean} isTertiaryHeld - true if the tertiary button is currently being pressed, otherwise + * false + * @property {KeyboardModifiers} keyboardModifiers - Integer value with bits set according to which keyboard modifier keys were + * pressed when the event was generated. + */ +/**jsdoc + *

A KeyboardModifiers value is used to specify which modifier keys on the keyboard are pressed. The value is the sum + * (bitwise OR) of the relevant combination of values from the following table:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
KeyHexadecimal valueDecimal valueDescription
Shift0x0200000033554432A Shift key on the keyboard is pressed.
Control0x0400000067108864A Control key on the keyboard is pressed.
Alt0x08000000134217728An Alt key on the keyboard is pressed.
Meta0x10000000268435456A Meta or Windows key on the keyboard is pressed.
Keypad0x20000000536870912A keypad button is pressed.
Group0x400000001073741824X11 operating system only: An AltGr / Mode_switch key on the keyboard is pressed.
+ * @typedef {number} KeyboardModifiers + */ QScriptValue PointerEvent::toScriptValue(QScriptEngine* engine, const PointerEvent& event) { QScriptValue obj = engine->newObject(); diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index b4d6ebaa7a..7b455beae5 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -561,6 +561,14 @@ QScriptValue xColorToScriptValue(QScriptEngine *engine, const xColor& color) { return obj; } +/**jsdoc + * Defines a rectangular portion of an image or screen. + * @typedef {object} Rect + * @property {number} x - Integer left, x-coordinate value. + * @property {number} y - Integer top, y-coordinate value. + * @property {number} width - Integer width of the rectangle. + * @property {number} height - Integer height of the rectangle. + */ QVariant qRectToVariant(const QRect& rect) { QVariantMap obj; obj["x"] = rect.x(); @@ -613,7 +621,13 @@ void xColorFromScriptValue(const QScriptValue &object, xColor& color) { } } - +/**jsdoc + * An RGB color value. + * @typedef {object} Color + * @property {number} red - Red component value. Integer in the range 0 - 255. + * @property {number} green - Green component value. Integer in the range 0 - 255. + * @property {number} blue - Blue component value. Integer in the range 0 - 255. + */ QVariant xColorToVariant(const xColor& color) { QVariantMap obj; obj["red"] = color.red; @@ -790,6 +804,12 @@ void quuidFromScriptValue(const QScriptValue& object, QUuid& uuid) { uuid = fromString; } +/**jsdoc + * A 2D size value. + * @typedef {object} Size + * @property {number} height - The height value. + * @property {number} width - The width value. + */ QScriptValue qSizeFToScriptValue(QScriptEngine* engine, const QSizeF& qSizeF) { QScriptValue obj = engine->newObject(); obj.setProperty("width", qSizeF.width()); diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 673ff0cd9c..be492bcdee 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -134,15 +134,16 @@ public: virtual QVariantMap toVariantMap() const = 0; }; +/**jsdoc + * A PickRay defines a vector with a starting point. It is used, for example, when finding entities or overlays that lie under a + * mouse click or intersect a laser beam. + * + * @typedef {object} PickRay + * @property {Vec3} origin - The starting position of the PickRay. + * @property {Quat} direction - The direction that the PickRay travels. + */ class PickRay : public MathPick { public: - /**jsdoc - * The mathematical definition of a ray. - * - * @typedef {Object} PickRay - * @property {Vec3} origin The origin of the ray. - * @property {Vec3} direction The direction of the ray. - */ PickRay() : origin(NAN), direction(NAN) { } PickRay(const QVariantMap& pickVariant) : origin(vec3FromVariant(pickVariant["origin"])), direction(vec3FromVariant(pickVariant["direction"])) {} PickRay(const glm::vec3& origin, const glm::vec3 direction) : origin(origin), direction(direction) {} @@ -166,17 +167,17 @@ Q_DECLARE_METATYPE(PickRay) QScriptValue pickRayToScriptValue(QScriptEngine* engine, const PickRay& pickRay); void pickRayFromScriptValue(const QScriptValue& object, PickRay& pickRay); +/**jsdoc + * A StylusTip defines the tip of a stylus. + * + * @typedef {object} StylusTip + * @property {number} side - The hand the tip is attached to: 0 for left, 1 for right. + * @property {Vec3} position - The position of the stylus tip. + * @property {Quat} orientation - The orientation of the stylus tip. + * @property {Vec3} velocity - The velocity of the stylus tip. + */ class StylusTip : public MathPick { public: - /**jsdoc - * The mathematical definition of a stylus tip. - * - * @typedef {Object} StylusTip - * @property {number} side The hand the tip is attached to. 0 == left, 1 == right. - * @property {Vec3} position The position of the tip. - * @property {Quat} orientation The orientation of the tip. - * @property {Vec3} velocity The velocity of the tip. - */ StylusTip() : position(NAN), velocity(NAN) {} StylusTip(const QVariantMap& pickVariant) : side(bilateral::Side(pickVariant["side"].toInt())), position(vec3FromVariant(pickVariant["position"])), orientation(quatFromVariant(pickVariant["orientation"])), velocity(vec3FromVariant(pickVariant["velocity"])) {} @@ -208,9 +209,9 @@ namespace std { template inline void hash_combine(std::size_t& seed, const T& v, Rest... rest) { - std::hash hasher; - seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); - hash_combine(seed, rest...); + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); + hash_combine(seed, rest...); } template <> diff --git a/libraries/shared/src/Trace.cpp b/libraries/shared/src/Trace.cpp index d7feb65ff3..3f6a2dd643 100644 --- a/libraries/shared/src/Trace.cpp +++ b/libraries/shared/src/Trace.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include @@ -31,6 +30,8 @@ #include "Gzip.h" #include "PortableHighResolutionClock.h" +#include "SharedLogging.h" +#include "shared/FileUtils.h" #include "shared/GlobalAppProperties.h" using namespace tracing; @@ -104,30 +105,13 @@ void TraceEvent::writeJson(QTextStream& out) const { #endif } -void Tracer::serialize(const QString& originalPath) { - - QString path = originalPath; - - // Filter for specific tokens potentially present in the path: - auto now = QDateTime::currentDateTime(); - - path = path.replace("{DATE}", now.date().toString("yyyyMMdd")); - path = path.replace("{TIME}", now.time().toString("HHmm")); - - // If the filename is relative, turn it into an absolute path relative to the document directory. - QFileInfo originalFileInfo(path); - if (originalFileInfo.isRelative()) { - QString docsLocation = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); - path = docsLocation + "/" + path; - QFileInfo info(path); - if (!info.absoluteDir().exists()) { - QString originalRelativePath = originalFileInfo.path(); - QDir(docsLocation).mkpath(originalRelativePath); - } +void Tracer::serialize(const QString& filename) { + QString fullPath = FileUtils::replaceDateTimeTokens(filename); + fullPath = FileUtils::computeDocumentPath(fullPath); + if (!FileUtils::canCreateFile(fullPath)) { + return; } - - std::list currentEvents; { std::lock_guard guard(_eventsMutex); @@ -137,11 +121,6 @@ void Tracer::serialize(const QString& originalPath) { } } - // If the file exists and we can't remove it, fail early - if (QFileInfo(path).exists() && !QFile::remove(path)) { - return; - } - // If we can't open a temp file for writing, fail early QByteArray data; { @@ -159,15 +138,16 @@ void Tracer::serialize(const QString& originalPath) { out << "\n]"; } - if (path.endsWith(".gz")) { + if (fullPath.endsWith(".gz")) { QByteArray compressed; gzip(data, compressed); data = compressed; - } - + } + { - QFile file(path); + QFile file(fullPath); if (!file.open(QIODevice::WriteOnly)) { + qDebug(shared) << "failed to open file '" << fullPath << "'"; return; } file.write(data); @@ -191,7 +171,6 @@ void Tracer::serialize(const QString& originalPath) { } } } }; - data = document.toJson(QJsonDocument::Compact); } #endif diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index dcbfd83ec7..5b016d4e91 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -71,6 +71,8 @@ void ViewFrustum::setProjection(const glm::mat4& projection) { glm::vec4 top = inverseProjection * vec4(0.0f, 1.0f, -1.0f, 1.0f); top /= top.w; _fieldOfView = abs(glm::degrees(2.0f * abs(glm::angle(vec3(0.0f, 0.0f, -1.0f), glm::normalize(vec3(top)))))); + _height = _corners[TOP_RIGHT_NEAR].y - _corners[BOTTOM_RIGHT_NEAR].y; + _width = _corners[TOP_RIGHT_NEAR].x - _corners[TOP_LEFT_NEAR].x; } // ViewFrustum::calculate() @@ -691,7 +693,7 @@ void ViewFrustum::getFurthestPointFromCamera(const AACube& box, glm::vec3& furth } } -const ViewFrustum::Corners ViewFrustum::getCorners(const float& depth) const { +const ViewFrustum::Corners ViewFrustum::getCorners(const float depth) const { glm::vec3 normal = glm::normalize(_direction); auto getCorner = [&](enum::BoxVertex nearCorner, enum::BoxVertex farCorner) { @@ -750,3 +752,98 @@ void ViewFrustum::invalidate() { } _centerSphereRadius = -1.0e6f; // -10^6 should be negative enough } + +void ViewFrustum::getSidePlanes(::Plane planes[4]) const { + planes[0] = _planes[TOP_PLANE]; + planes[1] = _planes[BOTTOM_PLANE]; + planes[2] = _planes[LEFT_PLANE]; + planes[3] = _planes[RIGHT_PLANE]; +} + +void ViewFrustum::getTransformedSidePlanes(const Transform& transform, ::Plane planes[4]) const { + glm::mat4 normalTransform; + transform.getInverseTransposeMatrix(normalTransform); + getSidePlanes(planes); + for (auto i = 0; i < 4; i++) { + // We assume the transform doesn't have a non uniform scale component to apply the + // transform to the normal without using the correct transpose of inverse. + auto transformedNormal = normalTransform * Transform::Vec4(planes[i].getNormal(), 0.0f); + auto planePoint = transform.transform(planes[i].getPoint()); + glm::vec3 planeNormal(transformedNormal.x, transformedNormal.y, transformedNormal.z); + planes[i].setNormalAndPoint(planeNormal, planePoint); + } +} + +void ViewFrustum::getUniformlyTransformedSidePlanes(const Transform& transform, ::Plane planes[4]) const { + getSidePlanes(planes); + for (auto i = 0; i < 4; i++) { + // We assume the transform doesn't have a non uniform scale component to apply the + // transform to the normal without using the correct transpose of inverse. + auto planeNormal = transform.transformDirection(planes[i].getNormal()); + auto planePoint = transform.transform(planes[i].getPoint()); + planes[i].setNormalAndPoint(planeNormal, planePoint); + } +} + +void ViewFrustum::tesselateSides(Triangle triangles[8]) const { + tesselateSides(_cornersWorld, triangles); +} + +void ViewFrustum::tesselateSides(const Transform& transform, Triangle triangles[8]) const { + glm::vec3 points[8]; + + for (auto i = 0; i < 8; i++) { + points[i] = transform.transform(_cornersWorld[i]); + } + + tesselateSides(points, triangles); +} + +void ViewFrustum::tesselateSidesAndFar(const Transform& transform, Triangle triangles[10], float farDistance) const { + glm::vec3 points[8]; + + // First 4 points are at near + for (auto i = 0; i < 4; i++) { + points[i] = transform.transform(_cornersWorld[i]); + } + auto farCorners = getCorners(farDistance); + + points[BOTTOM_LEFT_FAR] = transform.transform(farCorners.bottomLeft); + points[BOTTOM_RIGHT_FAR] = transform.transform(farCorners.bottomRight); + points[TOP_LEFT_FAR] = transform.transform(farCorners.topLeft); + points[TOP_RIGHT_FAR] = transform.transform(farCorners.topRight); + + tesselateSides(points, triangles); + // Add far side + triangles[8].v0 = points[BOTTOM_LEFT_FAR]; + triangles[8].v1 = points[BOTTOM_RIGHT_FAR]; + triangles[8].v2 = points[TOP_RIGHT_FAR]; + + triangles[9].v0 = points[BOTTOM_LEFT_FAR]; + triangles[9].v1 = points[TOP_LEFT_FAR]; + triangles[9].v2 = points[TOP_RIGHT_FAR]; +} + +void ViewFrustum::tesselateSides(const glm::vec3 points[8], Triangle triangles[8]) { + static_assert(BOTTOM_RIGHT_NEAR == (BOTTOM_LEFT_NEAR + 1), "Assuming a certain sequence in corners"); + static_assert(TOP_RIGHT_NEAR == (BOTTOM_RIGHT_NEAR + 1), "Assuming a certain sequence in corners"); + static_assert(TOP_LEFT_NEAR == (TOP_RIGHT_NEAR + 1), "Assuming a certain sequence in corners"); + static_assert(BOTTOM_RIGHT_FAR == (BOTTOM_LEFT_FAR + 1), "Assuming a certain sequence in corners"); + static_assert(TOP_RIGHT_FAR == (BOTTOM_RIGHT_FAR + 1), "Assuming a certain sequence in corners"); + static_assert(TOP_LEFT_FAR == (TOP_RIGHT_FAR + 1), "Assuming a certain sequence in corners"); + static const int triangleVertexIndices[8][3] = { + { BOTTOM_LEFT_NEAR, BOTTOM_LEFT_FAR, BOTTOM_RIGHT_FAR },{ BOTTOM_LEFT_NEAR, BOTTOM_RIGHT_NEAR, BOTTOM_RIGHT_FAR }, + { BOTTOM_RIGHT_NEAR, TOP_RIGHT_NEAR, TOP_RIGHT_FAR },{ BOTTOM_RIGHT_NEAR, BOTTOM_RIGHT_FAR, TOP_RIGHT_FAR }, + { TOP_RIGHT_NEAR, TOP_LEFT_NEAR, TOP_RIGHT_FAR },{ TOP_LEFT_NEAR, TOP_RIGHT_FAR, TOP_LEFT_FAR }, + { BOTTOM_LEFT_NEAR, TOP_LEFT_NEAR, TOP_LEFT_FAR },{ BOTTOM_LEFT_NEAR, BOTTOM_LEFT_FAR, TOP_LEFT_FAR } + }; + + for (auto i = 0; i < 8; i++) { + auto& triangle = triangles[i]; + auto vertexIndices = triangleVertexIndices[i]; + + triangle.v0 = points[vertexIndices[0]]; + triangle.v1 = points[vertexIndices[1]]; + triangle.v2 = points[vertexIndices[2]]; + } +} diff --git a/libraries/shared/src/ViewFrustum.h b/libraries/shared/src/ViewFrustum.h index 98f666d666..b55fe8b327 100644 --- a/libraries/shared/src/ViewFrustum.h +++ b/libraries/shared/src/ViewFrustum.h @@ -33,9 +33,6 @@ const float DEFAULT_ASPECT_RATIO = 16.0f/9.0f; const float DEFAULT_NEAR_CLIP = 0.08f; const float DEFAULT_FAR_CLIP = 16384.0f; -// the "ViewFrustum" has a "keyhole" shape: a regular frustum for stuff that is "visible" with -// a central sphere for stuff that is nearby (for physics simulation). - class ViewFrustum { public: // setters for camera attributes @@ -74,7 +71,7 @@ public: glm::vec3 bottomRight; // Get the corners depth units from frustum position, along frustum orientation }; - const Corners getCorners(const float& depth) const; + const Corners getCorners(const float depth) const; // getters for corners const glm::vec3& getFarTopLeft() const { return _cornersWorld[TOP_LEFT_FAR]; } @@ -90,6 +87,10 @@ public: void setCenterRadius(float radius) { _centerSphereRadius = radius; } float getCenterRadius() const { return _centerSphereRadius; } + void tesselateSides(Triangle triangles[8]) const; + void tesselateSides(const Transform& transform, Triangle triangles[8]) const; + void tesselateSidesAndFar(const Transform& transform, Triangle triangles[10], float farDistance) const; + void calculate(); typedef enum { OUTSIDE = 0, INTERSECT, INSIDE } intersection; @@ -134,6 +135,12 @@ public: enum PlaneIndex { TOP_PLANE = 0, BOTTOM_PLANE, LEFT_PLANE, RIGHT_PLANE, NEAR_PLANE, FAR_PLANE, NUM_PLANES }; const ::Plane* getPlanes() const { return _planes; } + void getSidePlanes(::Plane planes[4]) const; + // Transform can have a different scale value in X,Y,Z components + void getTransformedSidePlanes(const Transform& transform, ::Plane planes[4]) const; + // Transform is assumed to have the same scale value in all three X,Y,Z components, which + // allows for a faster computation. + void getUniformlyTransformedSidePlanes(const Transform& transform, ::Plane planes[4]) const; void invalidate(); // causes all reasonable intersection tests to fail @@ -175,6 +182,8 @@ private: template CubeProjectedPolygon computeProjectedPolygon(const TBOX& box) const; + static void tesselateSides(const glm::vec3 points[8], Triangle triangles[8]); + }; using ViewFrustumPointer = std::shared_ptr; diff --git a/libraries/shared/src/shared/Camera.cpp b/libraries/shared/src/shared/Camera.cpp index ab841c4717..787b7bfb1a 100644 --- a/libraries/shared/src/shared/Camera.cpp +++ b/libraries/shared/src/shared/Camera.cpp @@ -10,6 +10,52 @@ #include "Camera.h" +/**jsdoc + *

Camera modes affect the position of the camera and the controls for camera movement. The camera can be in one of the + * following modes:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
ModeStringDescription
First Person"first person"The camera is positioned such that you have the same view as your avatar. The camera moves and rotates with your + * avatar.
Third Person"third person"The camera is positioned such that you have a view from just behind your avatar. The camera moves and rotates with + * your avatar.
Mirror"mirror"The camera is positioned such that you are looking directly at your avatar. The camera moves and rotates with your + * avatar.
Independent"independent"The camera's position and orientation don't change with your avatar movement. Instead, they can be set via + * scripting.
Entity"entity"The camera's position and orientation are set to be the same as a specified entity's, and move with the entity as + * it moves. + *
+ * @typedef {string} Camera.Mode + */ CameraMode stringToMode(const QString& mode) { if (mode == "third person") { return CAMERA_MODE_THIRD_PERSON; @@ -131,6 +177,18 @@ void Camera::loadViewFrustum(ViewFrustum& frustum) const { frustum.calculate(); } +/**jsdoc + * A ViewFrustum has a "keyhole" shape: a regular frustum for stuff that is visible plus a central sphere for stuff that is + * nearby (for physics simulation). + * + * @typedef {object} ViewFrustum + * @property {Vec3} position - The location of the frustum's apex. + * @property {Quat} orientation - The direction that the frustum is looking at. + * @property {number} centerRadius - Center radius of the keyhole in meters. + * @property {number} fieldOfView - Horizontal field of view in degrees. + * @property {number} aspectRatio - Aspect ratio of the frustum. + * @property {Mat4} projection - The projection matrix for the view defined by the frustum. + */ QVariantMap Camera::getViewFrustum() { ViewFrustum frustum; loadViewFrustum(frustum); diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index 3ad08bd719..ea2e9cddab 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -37,12 +37,18 @@ class Camera : public QObject { Q_OBJECT /**jsdoc + * The Camera API provides access to the "camera" that defines your view in desktop and HMD display modes. + * * @namespace Camera - * @property position {Vec3} The position of the camera. - * @property orientation {Quat} The orientation of the camera. - * @property mode {string} The current camera mode. - * @property frustum {Object} The frustum of the camera. + * @property position {Vec3} The position of the camera. You can set this value only when the camera is in independent mode. + * @property orientation {Quat} The orientation of the camera. You can set this value only when the camera is in independent + * mode. + * @property mode {Camera.Mode} The camera mode. + * @property frustum {ViewFrustum} The camera frustum. + * @property cameraEntity {Uuid} The ID of the entity that is used for the camera position and orientation when the camera + * is in entity mode. */ + // FIXME: The cameraEntity property definition is copied from FancyCamera.h. Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) Q_PROPERTY(QString mode READ getModeString WRITE setModeString) @@ -69,52 +75,118 @@ public: QVariantMap getViewFrustum(); public slots: + /**jsdoc + * Get the current camera mode. You can also get the mode using the Camera.mode property. + * @function Camera.getModeString + * @returns {Camera.Mode} The current camera mode. + */ QString getModeString() const; + + /**jsdoc + * Set the camera mode. You can also set the mode using the Camera.mode property. + * @function Camera.setModeString + * @param {Camera.Mode} mode - The mode to set the camera to. + */ void setModeString(const QString& mode); + /**jsdoc + * Get the current camera position. You can also get the position using the Camera.position property. + * @function Camera.getPosition + * @returns {Vec3} The current camera position. + */ glm::vec3 getPosition() const { return _position; } + + /**jsdoc + * Set the camera position. You can also set the position using the Camera.position property. Only works if the + * camera is in independent mode. + * @function Camera.setPosition + * @param {Vec3} position - The position to set the camera at. + */ void setPosition(const glm::vec3& position); + /**jsdoc + * Get the current camera orientation. You can also get the orientation using the Camera.orientation property. + * @function Camera.getOrientation + * @returns {Quat} The current camera orientation. + */ glm::quat getOrientation() const { return _orientation; } + + /**jsdoc + * Set the camera orientation. You can also set the orientation using the Camera.orientation property. Only + * works if the camera is in independent mode. + * @function Camera.setOrientation + * @param {Quat} orientation - The orientation to set the camera to. + */ void setOrientation(const glm::quat& orientation); /**jsdoc - * Compute a {PickRay} based on the current camera configuration and the position x,y on the screen. + * Compute a {@link PickRay} based on the current camera configuration and the specified x, y position on the + * screen. The {@link PickRay} can be used in functions such as {@link Entities.findRayIntersection} and + * {@link Overlays.findRayIntersection}. * @function Camera.computePickRay - * @param {float} x X-coordinate on screen. - * @param {float} y Y-coordinate on screen. - * @return {PickRay} The computed {PickRay}. + * @param {number} x - X-coordinate on screen. + * @param {number} y - Y-coordinate on screen. + * @returns {PickRay} The computed {@link PickRay}. + * @example Use a PickRay to detect mouse clicks on entities. + * function onMousePressEvent(event) { + * var pickRay = Camera.computePickRay(event.x, event.y); + * var intersection = Entities.findRayIntersection(pickRay); + * if (intersection.intersects) { + * print ("You clicked on entity " + intersection.entityID); + * } + * } + * + * Controller.mousePressEvent.connect(onMousePressEvent); */ virtual PickRay computePickRay(float x, float y) const = 0; /**jsdoc - * Set the camera to look at position position. Only works while in independent. - * camera mode. + * Rotate the camera to look at the specified position. Only works if the camera is in independent mode. * @function Camera.lookAt - * @param {Vec3} Position Position to look at. + * @param {Vec3} position - Position to look at. + * @example Rotate your camera to look at entities as you click on them with your mouse. + * function onMousePressEvent(event) { + * var pickRay = Camera.computePickRay(event.x, event.y); + * var intersection = Entities.findRayIntersection(pickRay); + * if (intersection.intersects) { + * // Switch to independent mode. + * Camera.mode = "independent"; + * // Look at the entity that was clicked. + * var properties = Entities.getEntityProperties(intersection.entityID, "position"); + * Camera.lookAt(properties.position); + * } + * } + * + * Controller.mousePressEvent.connect(onMousePressEvent); */ void lookAt(const glm::vec3& position); /**jsdoc - * Set the camera to continue looking at position position. - * Only works while in `independent` camera mode. + * Set the camera to continue looking at the specified position even while the camera moves. Only works if the + * camera is in independent mode. * @function Camera.keepLookingAt - * @param {Vec3} position Position to keep looking at. + * @param {Vec3} position - Position to keep looking at. */ void keepLookingAt(const glm::vec3& position); /**jsdoc - * Stops the camera from continually looking at a position that was set with - * `keepLookingAt` + * Stops the camera from continually looking at the position that was set with Camera.keepLookingAt. * @function Camera.stopLookingAt */ void stopLooking() { _isKeepLookingAt = false; } signals: /**jsdoc - * Triggered when camera mode has changed. + * Triggered when the camera mode changes. * @function Camera.modeUpdated - * @return {Signal} + * @param {Camera.Mode} newMode - The new camera mode. + * @returns {Signal} + * @example Report camera mode changes. + * function onCameraModeUpdated(newMode) { + * print("The camera mode has changed to " + newMode); + * } + * + * Camera.modeUpdated.connect(onCameraModeUpdated); */ void modeUpdated(const QString& newMode); diff --git a/libraries/shared/src/shared/FileUtils.cpp b/libraries/shared/src/shared/FileUtils.cpp index 8c962dfd6d..dba0af7b16 100644 --- a/libraries/shared/src/shared/FileUtils.cpp +++ b/libraries/shared/src/shared/FileUtils.cpp @@ -12,6 +12,7 @@ #include "FileUtils.h" +#include #include #include #include @@ -20,6 +21,8 @@ #include #include +#include "../SharedLogging.h" + QString FileUtils::readFile(const QString& filename) { QFile file(filename); @@ -82,20 +85,54 @@ QString FileUtils::standardPath(QString subfolder) { // standard path // Mac: ~/Library/Application Support/Interface QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); - if (!subfolder.startsWith("/")) { subfolder.prepend("/"); } - if (!subfolder.endsWith("/")) { subfolder.append("/"); } - path.append(subfolder); QDir logDir(path); if (!logDir.exists(path)) { logDir.mkpath(path); } - return path; } + +QString FileUtils::replaceDateTimeTokens(const QString& originalPath) { + // Filter for specific tokens potentially present in the path: + auto now = QDateTime::currentDateTime(); + QString path = originalPath; + path.replace("{DATE}", now.date().toString("yyyyMMdd")); + path.replace("{TIME}", now.time().toString("HHmm")); + return path; +} + + +QString FileUtils::computeDocumentPath(const QString& originalPath) { + // If the filename is relative, turn it into an absolute path relative to the document directory. + QString path = originalPath; + QFileInfo originalFileInfo(originalPath); + if (originalFileInfo.isRelative()) { + QString docsLocation = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + path = docsLocation + "/" + originalPath; + } + return path; +} + +bool FileUtils::canCreateFile(const QString& fullPath) { + // If the file exists and we can't remove it, fail early + QFileInfo fileInfo(fullPath); + if (fileInfo.exists() && !QFile::remove(fullPath)) { + qDebug(shared) << "unable to overwrite file '" << fullPath << "'"; + return false; + } + QDir dir(fileInfo.absolutePath()); + if (!dir.exists()) { + if (!dir.mkpath(fullPath)) { + qDebug(shared) << "unable to create dir '" << dir.absolutePath() << "'"; + return false; + } + } + return true; +} diff --git a/libraries/shared/src/shared/FileUtils.h b/libraries/shared/src/shared/FileUtils.h index 4f2c1b7af5..d68fcd8a44 100644 --- a/libraries/shared/src/shared/FileUtils.h +++ b/libraries/shared/src/shared/FileUtils.h @@ -21,6 +21,9 @@ public: static QString standardPath(QString subfolder); static QString readFile(const QString& filename); static QStringList readLines(const QString& filename, QString::SplitBehavior splitBehavior = QString::KeepEmptyParts); + static QString replaceDateTimeTokens(const QString& path); + static QString computeDocumentPath(const QString& path); + static bool canCreateFile(const QString& fullPath); }; #endif // hifi_FileUtils_h diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 16a62cea4b..905a224ef4 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -134,9 +134,6 @@ void OffscreenUi::create() { myContext->setContextProperty("OffscreenUi", this); myContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags()); myContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); - auto tabletScriptingInterface = DependencyManager::get(); - TabletProxy* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); - myContext->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); } void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 14d8ec8985..90b91c5ec2 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -61,8 +61,9 @@ QVariantMap QmlWindowClass::parseArguments(QScriptContext* context) { } QUrl url { properties[SOURCE_PROPERTY].toString() }; + // If the passed URL doesn't correspond to a known scheme, assume it's a local file path if (url.scheme() != "http" && url.scheme() != "https" && url.scheme() != "file" && url.scheme() != "about" && - url.scheme() != "atp") { + url.scheme() != "atp" && url.scheme() != "qrc") { properties[SOURCE_PROPERTY] = QUrl::fromLocalFile(url.toString()).toString(); } diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 942c9f71a5..7848cb6372 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -51,6 +51,8 @@ #include "types/HFWebEngineProfile.h" #include "types/SoundEffect.h" +#include "TabletScriptingInterface.h" +#include "ToolbarScriptingInterface.h" #include "Logging.h" Q_LOGGING_CATEGORY(trace_render_qml, "trace.render.qml") @@ -65,7 +67,10 @@ public: void addWhitelistContextHandler(const std::initializer_list& urls, const QmlContextCallback& callback) { withWriteLock([&] { - for (const auto& url : urls) { + for (auto url : urls) { + if (url.isRelative()) { + url = QUrl(PathUtils::qmlBasePath() + url.toString()); + } _callbacks[url].push_back(callback); } }); @@ -102,7 +107,7 @@ void OffscreenQmlSurface::addWhitelistContextHandler(const std::initializer_list } -QmlContextCallback OffscreenQmlSurface::DEFAULT_CONTEXT_CALLBACK = [](QQmlContext*, QObject*) {}; +QmlContextObjectCallback OffscreenQmlSurface::DEFAULT_CONTEXT_CALLBACK = [](QQmlContext*, QQuickItem*) {}; struct TextureSet { // The number of surfaces with this size @@ -443,6 +448,15 @@ void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); rootContext->setContextProperty("Paths", DependencyManager::get().data()); + static std::once_flag once; + std::call_once(once, [&] { + qRegisterMetaType(); + qRegisterMetaType(); + }); + rootContext->setContextProperty("Tablet", DependencyManager::get().data()); + rootContext->setContextProperty("Toolbars", DependencyManager::get().data()); + TabletProxy* tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + engine->setObjectOwnership(tablet, QQmlEngine::CppOwnership); } QQmlEngine* acquireEngine(QQuickWindow* window) { @@ -659,10 +673,11 @@ void OffscreenQmlSurface::create() { auto qmlEngine = acquireEngine(_quickWindow); _qmlContext = new QQmlContext(qmlEngine->rootContext()); - + _qmlContext->setBaseUrl(QUrl{ PathUtils::qmlBasePath() }); _qmlContext->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); _qmlContext->setContextProperty("eventBridge", this); _qmlContext->setContextProperty("webEntity", this); + _qmlContext->setContextProperty("QmlSurface", this); // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper // Find a way to flag older scripts using this mechanism and wanr that this is deprecated @@ -679,7 +694,9 @@ void OffscreenQmlSurface::create() { // Setup the update of the QML media components with the current audio output device QObject::connect(&_audioOutputUpdateTimer, &QTimer::timeout, this, [this]() { - new AudioHandler(sharedFromThis(), _currentAudioOutputDevice); + if (_currentAudioOutputDevice.size() > 0) { + new AudioHandler(sharedFromThis(), _currentAudioOutputDevice); + } }); int waitForAudioQmlMs = 200; _audioOutputUpdateTimer.setInterval(waitForAudioQmlMs); @@ -695,6 +712,7 @@ void OffscreenQmlSurface::create() { } void OffscreenQmlSurface::changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate) { + _currentAudioOutputDevice = deviceName; if (_rootItem != nullptr && !isHtmlUpdate) { QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); } @@ -702,18 +720,16 @@ void OffscreenQmlSurface::changeAudioOutputDevice(const QString& deviceName, boo } void OffscreenQmlSurface::forceHtmlAudioOutputDeviceUpdate() { - auto audioIO = DependencyManager::get(); - QString deviceName = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); - QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::QueuedConnection, - Q_ARG(QString, deviceName), Q_ARG(bool, true)); + if (_currentAudioOutputDevice.size() > 0) { + QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::QueuedConnection, + Q_ARG(QString, _currentAudioOutputDevice), Q_ARG(bool, true)); + } } void OffscreenQmlSurface::forceQmlAudioOutputDeviceUpdate() { if (QThread::currentThread() != qApp->thread()) { QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); } else { - auto audioIO = DependencyManager::get(); - _currentAudioOutputDevice = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); if (_audioOutputUpdateTimer.isActive()) { _audioOutputUpdateTimer.stop(); } @@ -800,55 +816,70 @@ QQuickItem* OffscreenQmlSurface::getRootItem() { return _rootItem; } -void OffscreenQmlSurface::setBaseUrl(const QUrl& baseUrl) { - _qmlContext->setBaseUrl(baseUrl); +QQmlContext* OffscreenQmlSurface::contextForUrl(const QUrl& qmlSource, QQuickItem* parent, bool forceNewContext) { + // Get any whitelist functionality + QList callbacks = getQmlWhitelist()->getCallbacksForUrl(qmlSource); + // If we have whitelisted content, we must load a new context + forceNewContext |= !callbacks.empty(); + + QQmlContext* targetContext = parent ? QQmlEngine::contextForObject(parent) : _qmlContext; + if (!targetContext) { + targetContext = _qmlContext; + } + if (_rootItem && forceNewContext) { + targetContext = new QQmlContext(targetContext); + } + + for (const auto& callback : callbacks) { + callback(targetContext); + } + + return targetContext; } -void OffscreenQmlSurface::load(const QUrl& qmlSource, bool createNewContext, const QmlContextCallback& onQmlLoadedCallback) { +void OffscreenQmlSurface::load(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback) { + loadInternal(qmlSource, false, parent, [callback](QQmlContext* context, QQuickItem* newItem) { + QJSValue(callback).call(QJSValueList() << context->engine()->newQObject(newItem)); + }); +} + +void OffscreenQmlSurface::load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& onQmlLoadedCallback) { + loadInternal(qmlSource, createNewContext, nullptr, onQmlLoadedCallback); +} + +void OffscreenQmlSurface::loadInternal(const QUrl& qmlSource, bool createNewContext, QQuickItem* parent, const QmlContextObjectCallback& onQmlLoadedCallback) { if (QThread::currentThread() != thread()) { qCWarning(uiLogging) << "Called load on a non-surface thread"; } // Synchronous loading may take a while; restart the deadlock timer QMetaObject::invokeMethod(qApp, "updateHeartbeat", Qt::DirectConnection); - // Get any whitelist functionality - QList callbacks = getQmlWhitelist()->getCallbacksForUrl(qmlSource); - // If we have whitelisted content, we must load a new context - createNewContext |= !callbacks.empty(); - callbacks.push_back(onQmlLoadedCallback); - - QQmlContext* targetContext = _qmlContext; - if (_rootItem && createNewContext) { - targetContext = new QQmlContext(targetContext); - } - - - // FIXME eliminate loading of relative file paths for QML QUrl finalQmlSource = qmlSource; if ((qmlSource.isRelative() && !qmlSource.isEmpty()) || qmlSource.scheme() == QLatin1String("file")) { finalQmlSource = _qmlContext->resolvedUrl(qmlSource); } + auto targetContext = contextForUrl(finalQmlSource, parent, createNewContext); auto qmlComponent = new QQmlComponent(_qmlContext->engine(), finalQmlSource, QQmlComponent::PreferSynchronous); if (qmlComponent->isLoading()) { connect(qmlComponent, &QQmlComponent::statusChanged, this, [=](QQmlComponent::Status) { - finishQmlLoad(qmlComponent, targetContext, callbacks); + finishQmlLoad(qmlComponent, targetContext, parent, onQmlLoadedCallback); }); return; } - finishQmlLoad(qmlComponent, targetContext, callbacks); + finishQmlLoad(qmlComponent, targetContext, parent, onQmlLoadedCallback); } -void OffscreenQmlSurface::loadInNewContext(const QUrl& qmlSource, const QmlContextCallback& onQmlLoadedCallback) { +void OffscreenQmlSurface::loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback) { load(qmlSource, true, onQmlLoadedCallback); } -void OffscreenQmlSurface::load(const QUrl& qmlSource, const QmlContextCallback& onQmlLoadedCallback) { +void OffscreenQmlSurface::load(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback) { load(qmlSource, false, onQmlLoadedCallback); } -void OffscreenQmlSurface::load(const QString& qmlSourceFile, const QmlContextCallback& onQmlLoadedCallback) { +void OffscreenQmlSurface::load(const QString& qmlSourceFile, const QmlContextObjectCallback& onQmlLoadedCallback) { return load(QUrl(qmlSourceFile), onQmlLoadedCallback); } @@ -856,7 +887,8 @@ void OffscreenQmlSurface::clearCache() { _qmlContext->engine()->clearComponentCache(); } -void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, const QList& callbacks) { + +void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, QQuickItem* parent, const QmlContextObjectCallback& callback) { disconnect(qmlComponent, &QQmlComponent::statusChanged, this, 0); if (qmlComponent->isError()) { for (const auto& error : qmlComponent->errors()) { @@ -878,6 +910,22 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext return; } + if (!newObject) { + if (!_rootItem) { + qFatal("Could not load object as root item"); + return; + } + qCWarning(uiLogging) << "Unable to load QML item"; + return; + } + + QObject* eventBridge = qmlContext->contextProperty("eventBridge").value(); + if (qmlContext != _qmlContext && eventBridge && eventBridge != this) { + // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper + // Find a way to flag older scripts using this mechanism and wanr that this is deprecated + qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(eventBridge, qmlContext)); + } + qmlContext->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); // All quick items should be focusable @@ -888,41 +936,30 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext newItem->setFlag(QQuickItem::ItemIsFocusScope, true); } + // Make sure we will call callback for this codepath // Call this before qmlComponent->completeCreate() otherwise ghost window appears - if (newItem && _rootItem) { - for (const auto& callback : callbacks) { - callback(qmlContext, newObject); - } - } + // If we already have a root, just set a couple of flags and the ancestry + if (_rootItem) { + callback(qmlContext, newItem); - QObject* eventBridge = qmlContext->contextProperty("eventBridge").value(); - if (qmlContext != _qmlContext && eventBridge && eventBridge != this) { - // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper - // Find a way to flag older scripts using this mechanism and wanr that this is deprecated - qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(eventBridge, qmlContext)); + if (!parent) { + parent = _rootItem; + } + // Allow child windows to be destroyed from JS + QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); + newObject->setParent(parent); + newItem->setParentItem(parent); } qmlComponent->completeCreate(); qmlComponent->deleteLater(); - // If we already have a root, just set a couple of flags and the ancestry - if (newItem && _rootItem) { - // Allow child windows to be destroyed from JS - QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); - newObject->setParent(_rootItem); - if (newItem) { - newItem->setParentItem(_rootItem); - } + if (_rootItem) { QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); return; } - if (!newItem) { - qFatal("Could not load object as root item"); - return; - } - connect(newItem, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(QVariant))); // The root item is ready. Associate it with the window. @@ -930,11 +967,16 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext _rootItem->setParentItem(_quickWindow->contentItem()); _rootItem->setSize(_quickWindow->renderTargetSize()); - // Call this callback after rootitem is set, otherwise VrMenu wont work - for (const auto& callback : callbacks) { - callback(qmlContext, newObject); + if (_rootItem->objectName() == "tabletRoot") { + _qmlContext->setContextProperty("tabletRoot", QVariant::fromValue(_rootItem)); + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", this); + QObject* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); + _qmlContext->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); } QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); + // Call this callback after rootitem is set, otherwise VrMenu wont work + callback(qmlContext, newItem); } void OffscreenQmlSurface::updateQuick() { diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index 4c23c62c12..943d4f019b 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -33,12 +33,14 @@ class QQmlContext; class QQmlComponent; class QQuickWindow; class QQuickItem; +class QJSValue; // GPU resources are typically buffered for one copy being used by the renderer, // one copy in flight, and one copy being used by the receiver #define GPU_RESOURCE_BUFFER_SIZE 3 -using QmlContextCallback = std::function; +using QmlContextCallback = std::function; +using QmlContextObjectCallback = std::function; class OffscreenQmlSurface : public QObject, public QEnableSharedFromThis { Q_OBJECT @@ -46,7 +48,7 @@ class OffscreenQmlSurface : public QObject, public QEnableSharedFromThis& urls, const QmlContextCallback& callback); static void addWhitelistContextHandler(const QUrl& url, const QmlContextCallback& callback) { addWhitelistContextHandler({ { url } }, callback); }; @@ -59,10 +61,15 @@ public: void resize(const QSize& size, bool forceResize = false); QSize size() const; - Q_INVOKABLE void load(const QUrl& qmlSource, bool createNewContext, const QmlContextCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); - Q_INVOKABLE void loadInNewContext(const QUrl& qmlSource, const QmlContextCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); - Q_INVOKABLE void load(const QUrl& qmlSource, const QmlContextCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); - Q_INVOKABLE void load(const QString& qmlSourceFile, const QmlContextCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); + // Usable from QML code as QmlSurface.load(url, parent, function(newItem){ ... }) + Q_INVOKABLE void load(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback); + + // For C++ use + Q_INVOKABLE void load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); + Q_INVOKABLE void loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); + Q_INVOKABLE void load(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); + Q_INVOKABLE void load(const QString& qmlSourceFile, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); + void clearCache(); void setMaxFps(uint8_t maxFps) { _maxFps = maxFps; } // Optional values for event handling @@ -77,7 +84,6 @@ public: bool isPaused() const; bool getCleaned() { return _isCleaned; } - void setBaseUrl(const QUrl& baseUrl); QQuickItem* getRootItem(); QQuickWindow* getWindow(); QObject* getEventHandler(); @@ -142,13 +148,13 @@ protected: private: static QOpenGLContext* getSharedContext(); - void finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, const QList& callbacks); + QQmlContext* contextForUrl(const QUrl& url, QQuickItem* parent, bool forceNewContext = false); + void loadInternal(const QUrl& qmlSource, bool createNewContext, QQuickItem* parent, const QmlContextObjectCallback& onQmlLoadedCallback); + void finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, QQuickItem* parent, const QmlContextObjectCallback& onQmlLoadedCallback); QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); - void setupFbo(); bool allowNewFrame(uint8_t fps); void render(); void cleanup(); - QJsonObject getGLContextData(); void disconnectAudioOutputTimer(); private slots: diff --git a/libraries/ui/src/ui/OffscreenQmlSurfaceCache.cpp b/libraries/ui/src/ui/OffscreenQmlSurfaceCache.cpp index 2a0ca4a2e9..9b6b031dd9 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurfaceCache.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurfaceCache.cpp @@ -45,7 +45,6 @@ void OffscreenQmlSurfaceCache::release(const QString& rootSource, const QSharedP QSharedPointer OffscreenQmlSurfaceCache::buildSurface(const QString& rootSource) { auto surface = QSharedPointer(new OffscreenQmlSurface()); surface->create(); - surface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); surface->load(rootSource); surface->resize(QSize(100, 100)); return surface; diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index dd26dd7be7..0e793fef21 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -32,6 +32,14 @@ const QString SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; const QString TabletScriptingInterface::QML = "hifi/tablet/TabletRoot.qml"; +static QString getUsername() { + QString username = "Unknown user"; + auto accountManager = DependencyManager::get(); + if (accountManager->isLoggedIn()) { + username = accountManager->getAccountInfo().getUsername(); + } + return username; +} static Setting::Handle tabletSoundsButtonClick("TabletSounds", QStringList { "/sounds/Button06.wav", "/sounds/Button04.wav", @@ -39,6 +47,51 @@ static Setting::Handle tabletSoundsButtonClick("TabletSounds", QStr "/sounds/Tab01.wav", "/sounds/Tab02.wav" }); +TabletButtonListModel::TabletButtonListModel() { + +} + +TabletButtonListModel::~TabletButtonListModel() { + +} + +enum ButtonDeviceRole { + ButtonProxyRole = Qt::UserRole, +}; + +QHash TabletButtonListModel::_roles{ + { ButtonProxyRole, "buttonProxy" }, +}; + +Qt::ItemFlags TabletButtonListModel::_flags{ Qt::ItemIsSelectable | Qt::ItemIsEnabled }; + +QVariant TabletButtonListModel::data(const QModelIndex& index, int role) const { + if (!index.isValid() || index.row() >= rowCount() || role != ButtonProxyRole) { + return QVariant(); + } + + return QVariant::fromValue(_buttons.at(index.row()).data()); +} + +TabletButtonProxy* TabletButtonListModel::addButton(const QVariant& properties) { + auto tabletButtonProxy = QSharedPointer(new TabletButtonProxy(properties.toMap())); + beginResetModel(); + _buttons.push_back(tabletButtonProxy); + endResetModel(); + return tabletButtonProxy.data(); +} + +void TabletButtonListModel::removeButton(TabletButtonProxy* button) { + auto itr = std::find(_buttons.begin(), _buttons.end(), button); + if (itr == _buttons.end()) { + qCWarning(uiLogging) << "TabletProxy::removeButton() could not find button " << button; + return; + } + beginResetModel(); + _buttons.erase(itr); + endResetModel(); +} + TabletScriptingInterface::TabletScriptingInterface() { qmlRegisterType("TabletScriptingInterface", 1, 0, "TabletEnums"); } @@ -210,9 +263,9 @@ QObject* TabletScriptingInterface::getFlags() { // TabletProxy // -static const char* TABLET_SOURCE_URL = "Tablet.qml"; -static const char* WEB_VIEW_SOURCE_URL = "TabletWebView.qml"; -static const char* VRMENU_SOURCE_URL = "TabletMenu.qml"; +static const char* TABLET_HOME_SOURCE_URL = "hifi/tablet/TabletHome.qml"; +static const char* WEB_VIEW_SOURCE_URL = "hifi/tablet/TabletWebView.qml"; +static const char* VRMENU_SOURCE_URL = "hifi/tablet/TabletMenu.qml"; class TabletRootWindow : public QmlWindowClass { virtual QString qmlSource() const override { return "hifi/tablet/WindowRoot.qml"; } @@ -247,9 +300,6 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { auto offscreenUi = DependencyManager::get(); if (toolbarMode) { - removeButtonsFromHomeScreen(); - addButtonsToToolbar(); - // create new desktop window auto tabletRootWindow = new TabletRootWindow(); tabletRootWindow->initQml(QVariantMap()); @@ -264,11 +314,7 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { // forward qml surface events to interface js connect(tabletRootWindow, &QmlWindowClass::fromQml, this, &TabletProxy::fromQml); } else { - removeButtonsFromToolbar(); - - if (_currentPathLoaded == TABLET_SOURCE_URL) { - addButtonsToHomeScreen(); - } else { + if (_currentPathLoaded != TABLET_HOME_SOURCE_URL) { loadHomeScreen(true); } //check if running scripts window opened and save it for reopen in Tablet @@ -282,40 +328,8 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { _desktopWindow = nullptr; } } -} -static void addButtonProxyToQmlTablet(QQuickItem* qmlTablet, TabletButtonProxy* buttonProxy) { - Q_ASSERT(QThread::currentThread() == qApp->thread()); - if (buttonProxy == NULL){ - qCCritical(uiLogging) << "TabletScriptingInterface addButtonProxyToQmlTablet buttonProxy is NULL"; - return; - } - - QVariant resultVar; - bool hasResult = QMetaObject::invokeMethod(qmlTablet, "addButtonProxy", Qt::DirectConnection, - Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, buttonProxy->getProperties())); - if (!hasResult) { - qCWarning(uiLogging) << "TabletScriptingInterface addButtonProxyToQmlTablet has no result"; - return; - } - - QObject* qmlButton = qvariant_cast(resultVar); - if (!qmlButton) { - qCWarning(uiLogging) << "TabletScriptingInterface addButtonProxyToQmlTablet result not a QObject"; - return; - } - QObject::connect(qmlButton, SIGNAL(clicked()), buttonProxy, SLOT(clickedSlot())); - buttonProxy->setQmlButton(qobject_cast(qmlButton)); -} - -static QString getUsername() { - QString username = "Unknown user"; - auto accountManager = DependencyManager::get(); - if (accountManager->isLoggedIn()) { - return accountManager->getAccountInfo().getUsername(); - } else { - return "Unknown user"; - } + emit toolbarModeChanged(); } void TabletProxy::initialScreen(const QVariant& url) { @@ -363,7 +377,7 @@ void TabletProxy::onTabletShown() { static_cast(parent())->playSound(TabletScriptingInterface::TabletOpen); if (_showRunningScripts) { _showRunningScripts = false; - pushOntoStack("../../hifi/dialogs/TabletRunningScripts.qml"); + pushOntoStack("hifi/dialogs/TabletRunningScripts.qml"); } } } @@ -397,10 +411,7 @@ void TabletProxy::setQmlTabletRoot(OffscreenQmlSurface* qmlOffscreenSurface) { }); if (_toolbarMode) { - // if someone creates the tablet in toolbar mode, make sure to display the home screen on the tablet. - auto loader = _qmlTabletRoot->findChild("loader"); - QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen())); - QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_SOURCE_URL))); + QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_HOME_SOURCE_URL))); } // force to the tablet to go to the homescreen @@ -436,7 +447,6 @@ void TabletProxy::setQmlTabletRoot(OffscreenQmlSurface* qmlOffscreenSurface) { QMetaObject::invokeMethod(_qmlTabletRoot, "setShown", Q_ARG(const QVariant&, QVariant(true))); } } else { - removeButtonsFromHomeScreen(); _state = State::Uninitialized; emit screenChanged(QVariant("Closed"), QVariant("")); _currentPathLoaded = ""; @@ -465,7 +475,6 @@ void TabletProxy::gotoMenuScreen(const QString& submenu) { } if (root) { - removeButtonsFromHomeScreen(); auto offscreenUi = DependencyManager::get(); QObject* menu = offscreenUi->getRootMenu(); QMetaObject::invokeMethod(root, "setMenuProperties", Q_ARG(QVariant, QVariant::fromValue(menu)), Q_ARG(const QVariant&, QVariant(submenu))); @@ -539,7 +548,6 @@ void TabletProxy::loadQMLSource(const QVariant& path, bool resizable) { } if (root) { - removeButtonsFromHomeScreen(); //works only in Tablet QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, path)); _state = State::QML; if (path != _currentPathLoaded) { @@ -621,9 +629,7 @@ void TabletProxy::loadHomeScreen(bool forceOntoHomeScreen) { if ((_state != State::Home && _state != State::Uninitialized) || forceOntoHomeScreen) { if (!_toolbarMode && _qmlTabletRoot) { - auto loader = _qmlTabletRoot->findChild("loader"); - QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen())); - QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_SOURCE_URL))); + QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_HOME_SOURCE_URL))); QMetaObject::invokeMethod(_qmlTabletRoot, "playButtonClickSound"); } else if (_toolbarMode && _desktopWindow) { // close desktop window @@ -632,8 +638,8 @@ void TabletProxy::loadHomeScreen(bool forceOntoHomeScreen) { } } _state = State::Home; - emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL)); - _currentPathLoaded = TABLET_SOURCE_URL; + emit screenChanged(QVariant("Home"), QVariant(TABLET_HOME_SOURCE_URL)); + _currentPathLoaded = TABLET_HOME_SOURCE_URL; } } @@ -683,17 +689,18 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS } if (root) { - removeButtonsFromHomeScreen(); if (loadOtherBase) { - QMetaObject::invokeMethod(root, "loadTabletWebBase"); + QMetaObject::invokeMethod(root, "loadTabletWebBase", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); } else { - QMetaObject::invokeMethod(root, "loadWebBase"); + QMetaObject::invokeMethod(root, "loadWebBase", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); } QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); if (_toolbarMode && _desktopWindow) { QMetaObject::invokeMethod(root, "setResizable", Q_ARG(const QVariant&, QVariant(false))); } - QMetaObject::invokeMethod(root, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); + _state = State::Web; + emit screenChanged(QVariant("Web"), QVariant(url)); + _currentPathLoaded = QVariant(url); } else { // tablet is not initialized yet, save information and load when // the tablet root is set @@ -702,10 +709,8 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS _initialWebPathParams.first = injectedJavaScriptUrl; _initialWebPathParams.second = loadOtherBase; _initialScreen = true; + } - _state = State::Web; - emit screenChanged(QVariant("Web"), QVariant(url)); - _currentPathLoaded = QVariant(url); } TabletButtonProxy* TabletProxy::addButton(const QVariant& properties) { @@ -715,24 +720,7 @@ TabletButtonProxy* TabletProxy::addButton(const QVariant& properties) { return result; } - auto tabletButtonProxy = QSharedPointer(new TabletButtonProxy(properties.toMap())); - _tabletButtonProxies.push_back(tabletButtonProxy); - if (!_toolbarMode && _qmlTabletRoot) { - auto tablet = getQmlTablet(); - if (tablet) { - addButtonProxyToQmlTablet(tablet, tabletButtonProxy.data()); - } else { - qCCritical(uiLogging) << "Could not find tablet in TabletRoot.qml"; - } - } else if (_toolbarMode) { - auto toolbarProxy = DependencyManager::get()->getSystemToolbarProxy(); - if (toolbarProxy) { - // copy properties from tablet button proxy to toolbar button proxy. - auto toolbarButtonProxy = toolbarProxy->addButton(tabletButtonProxy->getProperties()); - tabletButtonProxy->setToolbarButtonProxy(toolbarButtonProxy); - } - } - return tabletButtonProxy.data(); + return _buttons.addButton(properties); } bool TabletProxy::onHomeScreen() { @@ -751,35 +739,7 @@ void TabletProxy::removeButton(TabletButtonProxy* tabletButtonProxy) { return; } - auto tablet = getQmlTablet(); - if (!tablet) { - qCCritical(uiLogging) << "Could not find tablet in TabletRoot.qml"; - } - - QSharedPointer buttonProxy; - { - auto iter = std::find(_tabletButtonProxies.begin(), _tabletButtonProxies.end(), tabletButtonProxy); - if (iter == _tabletButtonProxies.end()) { - qCWarning(uiLogging) << "TabletProxy::removeButton() could not find button " << tabletButtonProxy; - return; - } - buttonProxy = *iter; - _tabletButtonProxies.erase(iter); - } - - if (!_toolbarMode && _qmlTabletRoot) { - buttonProxy->setQmlButton(nullptr); - if (tablet) { - QMetaObject::invokeMethod(tablet, "removeButtonProxy", Qt::AutoConnection, Q_ARG(QVariant, buttonProxy->getProperties())); - } - } else if (_toolbarMode) { - auto toolbarProxy = DependencyManager::get()->getSystemToolbarProxy(); - // remove button from toolbarProxy - if (toolbarProxy) { - toolbarProxy->removeButton(buttonProxy->getUuid().toString()); - buttonProxy->setToolbarButtonProxy(nullptr); - } - } + _buttons.removeButton(tabletButtonProxy); } void TabletProxy::emitScriptEvent(const QVariant& msg) { @@ -808,57 +768,16 @@ void TabletProxy::sendToQml(const QVariant& msg) { } } -void TabletProxy::addButtonsToHomeScreen() { - auto tablet = getQmlTablet(); - if (!tablet || _toolbarMode) { - return; - } - for (auto& buttonProxy : _tabletButtonProxies) { - addButtonProxyToQmlTablet(tablet, buttonProxy.data()); - } - auto loader = _qmlTabletRoot->findChild("loader"); - QObject::disconnect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen())); -} OffscreenQmlSurface* TabletProxy::getTabletSurface() { return _qmlOffscreenSurface; } -void TabletProxy::removeButtonsFromHomeScreen() { - auto tablet = getQmlTablet(); - for (auto& buttonProxy : _tabletButtonProxies) { - if (tablet) { - QMetaObject::invokeMethod(tablet, "removeButtonProxy", Qt::AutoConnection, Q_ARG(QVariant, buttonProxy->getProperties())); - } - buttonProxy->setQmlButton(nullptr); - } -} void TabletProxy::desktopWindowClosed() { gotoHomeScreen(); } -void TabletProxy::addButtonsToToolbar() { - Q_ASSERT(QThread::currentThread() == thread()); - ToolbarProxy* toolbarProxy = DependencyManager::get()->getSystemToolbarProxy(); - for (auto& buttonProxy : _tabletButtonProxies) { - // copy properties from tablet button proxy to toolbar button proxy. - buttonProxy->setToolbarButtonProxy(toolbarProxy->addButton(buttonProxy->getProperties())); - } - - // make the toolbar visible - toolbarProxy->writeProperty("visible", QVariant(true)); -} - -void TabletProxy::removeButtonsFromToolbar() { - Q_ASSERT(QThread::currentThread() == thread()); - ToolbarProxy* toolbarProxy = DependencyManager::get()->getSystemToolbarProxy(); - for (auto& buttonProxy : _tabletButtonProxies) { - // remove button from toolbarProxy - toolbarProxy->removeButton(buttonProxy->getUuid().toString()); - buttonProxy->setToolbarButtonProxy(nullptr); - } -} QQuickItem* TabletProxy::getQmlTablet() const { if (!_qmlTabletRoot) { @@ -928,25 +847,6 @@ TabletButtonProxy::~TabletButtonProxy() { } } -void TabletButtonProxy::setQmlButton(QQuickItem* qmlButton) { - Q_ASSERT(QThread::currentThread() == qApp->thread()); - if (_qmlButton) { - QObject::disconnect(_qmlButton, &QQuickItem::destroyed, this, nullptr); - } - _qmlButton = qmlButton; - if (_qmlButton) { - QObject::connect(_qmlButton, &QQuickItem::destroyed, this, [this] { _qmlButton = nullptr; }); - } -} - -void TabletButtonProxy::setToolbarButtonProxy(QObject* toolbarButtonProxy) { - Q_ASSERT(QThread::currentThread() == thread()); - _toolbarButtonProxy = toolbarButtonProxy; - if (_toolbarButtonProxy) { - QObject::connect(_toolbarButtonProxy, SIGNAL(clicked()), this, SLOT(clickedSlot())); - } -} - QVariantMap TabletButtonProxy::getProperties() { if (QThread::currentThread() != thread()) { QVariantMap result; @@ -963,20 +863,19 @@ void TabletButtonProxy::editProperties(const QVariantMap& properties) { return; } + bool changed = false; QVariantMap::const_iterator iter = properties.constBegin(); while (iter != properties.constEnd()) { const auto& key = iter.key(); const auto& value = iter.value(); if (!_properties.contains(key) || _properties[key] != value) { - _properties[iter.key()] = iter.value(); - if (_qmlButton) { - QMetaObject::invokeMethod(_qmlButton, "changeProperty", Qt::AutoConnection, Q_ARG(QVariant, QVariant(iter.key())), Q_ARG(QVariant, iter.value())); - } + _properties[key] = value; + changed = true; } ++iter; } - if (_toolbarButtonProxy) { - QMetaObject::invokeMethod(_toolbarButtonProxy, "editProperties", Qt::AutoConnection, Q_ARG(QVariantMap, properties)); + if (changed) { + emit propertiesChanged(); } } diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index ad6a7c8001..d24b3b6947 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -12,13 +12,16 @@ #include #include -#include -#include +#include +#include +#include +#include + #include -#include -#include -#include -#include +#include +#include + +#include #include #include @@ -90,6 +93,31 @@ protected: bool _toolbarMode { false }; }; +class TabletButtonListModel : public QAbstractListModel { + Q_OBJECT + +public: + TabletButtonListModel(); + ~TabletButtonListModel(); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override { Q_UNUSED(parent); return (int)_buttons.size(); } + QHash roleNames() const override { return _roles; } + Qt::ItemFlags flags(const QModelIndex& index) const override { return _flags; } + QVariant data(const QModelIndex& index, int role) const override; + + +protected: + friend class TabletProxy; + TabletButtonProxy* addButton(const QVariant& properties); + void removeButton(TabletButtonProxy* button); + using List = std::list>; + static QHash _roles; + static Qt::ItemFlags _flags; + std::vector> _buttons; +}; + +Q_DECLARE_METATYPE(TabletButtonListModel*); + /**jsdoc * @class TabletProxy * @property name {string} READ_ONLY: name of this tablet @@ -99,9 +127,10 @@ protected: class TabletProxy : public QObject { Q_OBJECT Q_PROPERTY(QString name READ getName) - Q_PROPERTY(bool toolbarMode READ getToolbarMode WRITE setToolbarMode) + Q_PROPERTY(bool toolbarMode READ getToolbarMode WRITE setToolbarMode NOTIFY toolbarModeChanged) Q_PROPERTY(bool landscape READ getLandscape WRITE setLandscape) Q_PROPERTY(bool tabletShown MEMBER _tabletShown NOTIFY tabletShownChanged) + Q_PROPERTY(TabletButtonListModel* buttons READ getButtons CONSTANT) public: TabletProxy(QObject* parent, const QString& name); ~TabletProxy(); @@ -204,6 +233,7 @@ public: QQuickItem* getQmlMenu() const; + TabletButtonListModel* getButtons() { return &_buttons; } signals: /**jsdoc * Signaled when this tablet receives an event from the html/js embedded in the tablet @@ -236,21 +266,19 @@ signals: */ void tabletShownChanged(); + void toolbarModeChanged(); + protected slots: - void addButtonsToHomeScreen(); void desktopWindowClosed(); void emitWebEvent(const QVariant& msg); void onTabletShown(); + protected: - void removeButtonsFromHomeScreen(); void loadHomeScreen(bool forceOntoHomeScreen); - void addButtonsToToolbar(); - void removeButtonsFromToolbar(); bool _initialScreen { false }; QVariant _currentPathLoaded { "" }; QString _name; - std::vector> _tabletButtonProxies; QQuickItem* _qmlTabletRoot { nullptr }; OffscreenQmlSurface* _qmlOffscreenSurface { nullptr }; QmlWindowClass* _desktopWindow { nullptr }; @@ -263,6 +291,8 @@ protected: std::pair _initialWebPathParams; bool _landscape { false }; bool _showRunningScripts { false }; + + TabletButtonListModel _buttons; }; Q_DECLARE_METATYPE(TabletProxy*); @@ -274,13 +304,11 @@ Q_DECLARE_METATYPE(TabletProxy*); class TabletButtonProxy : public QObject { Q_OBJECT Q_PROPERTY(QUuid uuid READ getUuid) + Q_PROPERTY(QVariantMap properties READ getProperties NOTIFY propertiesChanged) public: TabletButtonProxy(const QVariantMap& properties); ~TabletButtonProxy(); - void setQmlButton(QQuickItem* qmlButton); - void setToolbarButtonProxy(QObject* toolbarButtonProxy); - QUuid getUuid() const { return _uuid; } /**jsdoc @@ -297,9 +325,6 @@ public: */ Q_INVOKABLE void editProperties(const QVariantMap& properties); -public slots: - void clickedSlot() { emit clicked(); } - signals: /**jsdoc * Signaled when this button has been clicked on by the user. @@ -307,12 +332,11 @@ signals: * @returns {Signal} */ void clicked(); + void propertiesChanged(); protected: QUuid _uuid; int _stableOrder; - QQuickItem* _qmlButton { nullptr }; - QObject* _toolbarButtonProxy { nullptr }; QVariantMap _properties; }; diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 0a0e67756d..d8658f1c14 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -20,7 +20,7 @@ namespace { bool isAuthableHighFidelityURL(const QUrl& url) { - auto metaverseServerURL = NetworkingConstants::METAVERSE_SERVER_URL; + auto metaverseServerURL = NetworkingConstants::METAVERSE_SERVER_URL(); static const QStringList HF_HOSTS = { "highfidelity.com", "highfidelity.io", metaverseServerURL.toString(), "metaverse.highfidelity.io" diff --git a/scripts/developer/EZrecord.js b/scripts/developer/EZrecord.js index 7fdebada79..049ccbc03d 100644 --- a/scripts/developer/EZrecord.js +++ b/scripts/developer/EZrecord.js @@ -234,7 +234,7 @@ Recorder.setUp(); - // Tablet/toolbar button. + // tablet/toolbar button. button = tablet.addButton({ icon: APP_ICON_INACTIVE, activeIcon: APP_ICON_ACTIVE, diff --git a/scripts/developer/inputRecording.js b/scripts/developer/inputRecording.js index 85bda623b3..6fb8e471cd 100644 --- a/scripts/developer/inputRecording.js +++ b/scripts/developer/inputRecording.js @@ -19,7 +19,7 @@ tablet.gotoHomeScreen(); onRecordingScreen = false; } else { - tablet.loadQMLSource("InputRecorder.qml"); + tablet.loadQMLSource("hifi/tablet/InputRecorder.qml"); onRecordingScreen = true; } } diff --git a/scripts/developer/tests/qmlTest.js b/scripts/developer/tests/qmlTest.js index c891b6a1b7..0eaabac6d1 100644 --- a/scripts/developer/tests/qmlTest.js +++ b/scripts/developer/tests/qmlTest.js @@ -1,7 +1,7 @@ print("Launching web window"); qmlWindow = new OverlayWindow({ title: 'Test Qml', - source: "https://s3.amazonaws.com/DreamingContent/qml/content.qml", + source: "qrc:///qml/OverlayWindowTest.qml", height: 240, width: 320, toolWindow: false, diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 86a16d9a25..1da7871172 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -184,7 +184,11 @@ Rectangle { ListElement { text: "Lightmap"; color: "White" } ListElement { text: "Scattering"; color: "White" } ListElement { text: "Lighting"; color: "White" } - ListElement { text: "Shadow"; color: "White" } + ListElement { text: "Shadow Cascade 0"; color: "White" } + ListElement { text: "Shadow Cascade 1"; color: "White" } + ListElement { text: "Shadow Cascade 2"; color: "White" } + ListElement { text: "Shadow Cascade 3"; color: "White" } + ListElement { text: "Shadow Cascade Indices"; color: "White" } ListElement { text: "Linear Depth"; color: "White" } ListElement { text: "Half Linear Depth"; color: "White" } ListElement { text: "Half Normal"; color: "White" } diff --git a/scripts/developer/utilities/render/shadow.qml b/scripts/developer/utilities/render/shadow.qml index 8548ba4119..a8fcf1aec7 100644 --- a/scripts/developer/utilities/render/shadow.qml +++ b/scripts/developer/utilities/render/shadow.qml @@ -15,15 +15,24 @@ Column { id: root spacing: 8 property var viewConfig: Render.getConfig("RenderMainView.DrawViewFrustum"); - property var shadowConfig: Render.getConfig("RenderMainView.DrawShadowFrustum"); + property var shadow0Config: Render.getConfig("RenderMainView.DrawShadowFrustum0"); + property var shadow1Config: Render.getConfig("RenderMainView.DrawShadowFrustum1"); + property var shadow2Config: Render.getConfig("RenderMainView.DrawShadowFrustum2"); + property var shadow3Config: Render.getConfig("RenderMainView.DrawShadowFrustum3"); Component.onCompleted: { viewConfig.enabled = true; - shadowConfig.enabled = true; + shadow0Config.enabled = true; + shadow1Config.enabled = true; + shadow2Config.enabled = true; + shadow3Config.enabled = true; } Component.onDestruction: { viewConfig.enabled = false; - shadowConfig.enabled = false; + shadow0Config.enabled = false; + shadow1Config.enabled = false; + shadow2Config.enabled = false; + shadow3Config.enabled = false; } CheckBox { @@ -31,7 +40,14 @@ Column { checked: false onCheckedChanged: { viewConfig.isFrozen = checked; - shadowConfig.isFrozen = checked; + shadow0Config.isFrozen = checked; + shadow1Config.isFrozen = checked; + shadow2Config.isFrozen = checked; + shadow3Config.isFrozen = checked; + shadow0BoundConfig.isFrozen = checked; + shadow1BoundConfig.isFrozen = checked; + shadow2BoundConfig.isFrozen = checked; + shadow3BoundConfig.isFrozen = checked; } } Row { @@ -46,5 +62,10 @@ Column { color: "blue" font.italic: true } + Label { + text: "Items" + color: "magenta" + font.italic: true + } } } diff --git a/scripts/system/audio.js b/scripts/system/audio.js index 0a3471fa81..a93177ca38 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -15,7 +15,7 @@ var TABLET_BUTTON_NAME = "AUDIO"; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; -var AUDIO_QML_SOURCE = "../audio/Audio.qml"; +var AUDIO_QML_SOURCE = "hifi/audio/Audio.qml"; var MUTE_ICONS = { icon: "icons/tablet-icons/mic-mute-i.svg", diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index a4cdc5b097..cde44e78c9 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -26,8 +26,8 @@ // Relevant Variables: // -WALLET_QML_SOURCE: The path to the Wallet QML // -onWalletScreen: true/false depending on whether we're looking at the app. - var WALLET_QML_SOURCE = Script.resourcesPath() + "qml/hifi/commerce/wallet/Wallet.qml"; - var MARKETPLACE_PURCHASES_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/purchases/Purchases.qml"; + var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; + var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; var onWalletScreen = false; function onButtonClicked() { if (!tablet) { @@ -61,10 +61,26 @@ function fromQml(message) { switch (message.method) { case 'passphrasePopup_cancelClicked': - case 'walletSetup_cancelClicked': case 'needsLogIn_cancelClicked': tablet.gotoHomeScreen(); break; + case 'walletSetup_cancelClicked': + switch (message.referrer) { + case '': // User clicked "Wallet" app + case undefined: + case null: + tablet.gotoHomeScreen(); + break; + case 'purchases': + case 'marketplace cta': + case 'mainPage': + tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + break; + default: // User needs to return to an individual marketplace item URL + tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.referrer, MARKETPLACES_INJECT_SCRIPT_URL); + break; + } + break; case 'needsLogIn_loginClicked': openLoginWindow(); break; diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 57f3b4fd8b..51f927f224 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -154,6 +154,15 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; this.update = function () { + try { + _this.updateInternal(); + } catch (e) { + print(e); + } + Script.setTimeout(_this.update, BASIC_TIMER_INTERVAL_MS); + }; + + this.updateInternal = function () { if (PROFILE) { Script.beginProfileRange("dispatch.pre"); } @@ -376,7 +385,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (PROFILE) { Script.endProfileRange("dispatch.run"); } - Script.setTimeout(_this.update, BASIC_TIMER_INTERVAL_MS); }; this.setBlacklist = function() { @@ -404,6 +412,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}], posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true), hover: true, + scaleWithAvatar: true, distanceScaleEnd: true, hand: LEFT_HAND }); @@ -413,6 +422,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}], posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true), hover: true, + scaleWithAvatar: true, distanceScaleEnd: true, hand: RIGHT_HAND }); @@ -423,6 +433,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true), triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}], hover: true, + scaleWithAvatar: true, distanceScaleEnd: true, hand: LEFT_HAND }); @@ -433,6 +444,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true), triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}], hover: true, + scaleWithAvatar: true, distanceScaleEnd: true, hand: RIGHT_HAND }); diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index 7733ca1d28..a51cea67f8 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -269,6 +269,7 @@ function Grabber() { joint: "Mouse", filter: Picks.PICK_ENTITIES, faceAvatar: true, + scaleWithAvatar: true, enabled: true, renderStates: renderStates }); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 8a7e36465d..e28f877d85 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -427,7 +427,7 @@ var toolBar = (function () { }); createButton = activeButton; tablet.screenChanged.connect(function (type, url) { - if (isActive && (type !== "QML" || url !== "Edit.qml")) { + if (isActive && (type !== "QML" || url !== "hifi/tablet/Edit.qml")) { that.setActive(false) } }); @@ -656,7 +656,7 @@ var toolBar = (function () { selectionDisplay.triggerMapping.disable(); tablet.landscape = false; } else { - tablet.loadQMLSource("Edit.qml", true); + tablet.loadQMLSource("hifi/tablet/Edit.qml", true); UserActivityLogger.enabledEdit(); entityListTool.setVisible(true); gridTool.setVisible(true); diff --git a/scripts/system/generalSettings.js b/scripts/system/generalSettings.js index 7d97f13757..082528ffc5 100644 --- a/scripts/system/generalSettings.js +++ b/scripts/system/generalSettings.js @@ -18,7 +18,7 @@ var buttonName = "Settings"; var toolBar = null; var tablet = null; - var settings = "TabletGeneralPreferences.qml" + var settings = "hifi/tablet/TabletGeneralPreferences.qml" function onClicked(){ if (tablet) { tablet.loadQMLSource(settings); diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 878c3b51f1..698dd93f29 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -199,7 +199,7 @@ var purchasesElement = document.createElement('a'); var dropDownElement = document.getElementById('user-dropdown'); - $('#user-dropdown').find('.username')[0].style = "max-width:80px;white-space:nowrap;overflow:hidden;" + + $('#user-dropdown').find('.username')[0].style = "max-width:80px;white-space:nowrap;overflow:hidden;" + "text-overflow:ellipsis;display:inline-block;position:relative;top:4px;"; $('#user-dropdown').find('.caret')[0].style = "position:relative;top:-3px;"; @@ -243,13 +243,14 @@ }); } - function buyButtonClicked(id, name, author, price, href) { + function buyButtonClicked(id, name, author, price, href, referrer) { EventBridge.emitWebEvent(JSON.stringify({ type: "CHECKOUT", itemId: id, itemName: name, itemPrice: price ? parseInt(price, 10) : 0, - itemHref: href + itemHref: href, + referrer: referrer })); } @@ -265,8 +266,10 @@ }); $('.grid-item').find('#price-or-edit').find('a').each(function() { - $(this).attr('data-href', $(this).attr('href')); - $(this).attr('href', '#'); + if ($(this).attr('href') !== '#') { // Guard necessary because of the AJAX nature of Marketplace site + $(this).attr('data-href', $(this).attr('href')); + $(this).attr('href', '#'); + } cost = $(this).closest('.col-xs-3').find('.item-cost').text(); $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); @@ -314,7 +317,8 @@ $(this).closest('.grid-item').find('.item-title').text(), $(this).closest('.grid-item').find('.creator').find('.value').text(), $(this).closest('.grid-item').find('.item-cost').text(), - $(this).attr('data-href')); + $(this).attr('data-href'), + "mainPage"); }); } @@ -387,27 +391,41 @@ var href = purchaseButton.attr('href'); purchaseButton.attr('href', '#'); - purchaseButton.css({ - "background": "linear-gradient(#00b4ef, #0093C5)", - "color": "#FFF", - "font-weight": "600", - "padding-bottom": "10px" - }); + var availability = $.trim($('.item-availability').text()); + if (availability === 'available') { + purchaseButton.css({ + "background": "linear-gradient(#00b4ef, #0093C5)", + "color": "#FFF", + "font-weight": "600", + "padding-bottom": "10px" + }); + } else { + purchaseButton.css({ + "background": "linear-gradient(#a2a2a2, #fefefe)", + "color": "#000", + "font-weight": "600", + "padding-bottom": "10px" + }); + } var cost = $('.item-cost').text(); - - if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { + if (availability !== 'available') { + purchaseButton.html('UNAVAILABLE (' + availability + ')'); + } else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { purchaseButton.html('PURCHASE ' + cost); } purchaseButton.on('click', function () { - buyButtonClicked(window.location.pathname.split("/")[3], - $('#top-center').find('h1').text(), - $('#creator').find('.value').text(), - cost, - href); - }); + if ('available' === availability) { + buyButtonClicked(window.location.pathname.split("/")[3], + $('#top-center').find('h1').text(), + $('#creator').find('.value').text(), + cost, + href, + "itemPage"); + } + }); maybeAddPurchasesButton(); } } diff --git a/scripts/system/libraries/accountUtils.js b/scripts/system/libraries/accountUtils.js index 6df0aa3a87..69092b30fc 100644 --- a/scripts/system/libraries/accountUtils.js +++ b/scripts/system/libraries/accountUtils.js @@ -10,7 +10,7 @@ openLoginWindow = function openLoginWindow() { || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { Menu.triggerOption("Login / Sign Up"); } else { - tablet.loadQMLOnTop("../../dialogs/TabletLoginDialog.qml"); + tablet.loadQMLOnTop("dialogs/TabletLoginDialog.qml"); HMD.openTablet(); } }; diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 7dddd46562..d9003ffeaa 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -46,25 +46,26 @@ var PARTICLE_EFFECT_PROPS = { "alpha": 0.8, "azimuthFinish": Math.PI, - "azimuthStart": -1 * Math.PI, + "azimuthStart": -Math.PI, "emitRate": 500, - "emitSpeed": 0.0, "emitterShouldTrail": 1, "isEmitting": 1, "lifespan": 3, "maxParticles": 1000, "particleRadius": 0.003, - "polarStart": 1, - "polarFinish": 1, + "polarStart": Math.PI / 2, + "polarFinish": Math.PI / 2, "radiusFinish": 0.008, "radiusStart": 0.0025, - "speedSpread": 0.025, + "emitSpeed": 0.02, + "speedSpread": 0.015, "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", "color": {"red": 255, "green": 255, "blue": 255}, "colorFinish": {"red": 0, "green": 164, "blue": 255}, "colorStart": {"red": 255, "green": 255, "blue": 255}, "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "emitDimensions": { "x": 0.15, "y": 0.15, "z": 0.01 }, "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" @@ -111,10 +112,20 @@ var connectingHandJointIndex = -1; var waitingList = {}; var particleEffect; - var particleRotationAngle = 0.0; + var particleEmitRate; + var PARTICLE_INITIAL_EMIT_RATE = 250; + var PARTICLE_MINIMUM_EMIT_RATE = 50; + var PARTICLE_DECAY_RATE = 0.5; + var particleEffectUpdateTimer = null; + var PARTICLE_EFFECT_UPDATE_INTERVAL = 200; var makingConnectionParticleEffect; - var makingConnectionEmitRate = 2000; - var particleEmitRate = 500; + var makingConnectionEmitRate; + var isMakingConnectionEmitting; + var MAKING_CONNECTION_INITIAL_EMIT_RATE = 500; + var MAKING_CONNECTION_MINIMUM_EMIT_RATE = 20; + var MAKING_CONNECTION_DECAY_RATE = 0.5; + var makingConnectionUpdateTimer = null; + var MAKING_CONNECTION_UPDATE_INTERVAL = 200; var handshakeInjector; var successfulHandshakeInjector; var handshakeSound; @@ -239,12 +250,20 @@ } function deleteParticleEffect() { + if (particleEffectUpdateTimer) { + Script.clearTimeout(particleEffectUpdateTimer); + particleEffectUpdateTimer = null; + } if (particleEffect) { particleEffect = Entities.deleteEntity(particleEffect); } } function deleteMakeConnectionParticleEffect() { + if (makingConnectionUpdateTimer) { + Script.clearTimeout(makingConnectionUpdateTimer); + makingConnectionUpdateTimer = null; + } if (makingConnectionParticleEffect) { makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); } @@ -257,15 +276,31 @@ } } - function calcParticlePos(myHandPosition, otherHandPosition, otherOrientation, reset) { - if (reset) { - particleRotationAngle = 0.0; + function updateMakingConnection() { + makingConnectionEmitRate = Math.max(makingConnectionEmitRate * MAKING_CONNECTION_DECAY_RATE, + MAKING_CONNECTION_MINIMUM_EMIT_RATE); + isMakingConnectionEmitting = true; + Entities.editEntity(makingConnectionParticleEffect, { + emitRate: makingConnectionEmitRate, + isEmitting: true + }); + if (makingConnectionEmitRate > MAKING_CONNECTION_MINIMUM_EMIT_RATE) { + makingConnectionUpdateTimer = Script.setTimeout(makingConnectionUpdateTimer, MAKING_CONNECTION_UPDATE_INTERVAL); + } else { + makingConnectionUpdateTimer = null; + } + } + + function updateParticleEffect() { + particleEmitRate = Math.max(PARTICLE_MINIMUM_EMIT_RATE, particleEmitRate * PARTICLE_DECAY_RATE); + Entities.editEntity(particleEffect, { + emitRate: particleEmitRate + }); + if (particleEmitRate > PARTICLE_MINIMUM_EMIT_RATE) { + particleEffectUpdateTimer = Script.setTimeout(updateParticleEffect, PARTICLE_EFFECT_UPDATE_INTERVAL); + } else { + particleEffectUpdateTimer = null; } - var position = positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); - particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz - var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360); - var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); - return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); } // this is called frequently, but usually does nothing @@ -301,41 +336,37 @@ positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); // now manage the rest of the entity if (!particleEffect) { - particleRotationAngle = 0.0; - particleEmitRate = 500; + particleEmitRate = PARTICLE_INITIAL_EMIT_RATE; particleProps = PARTICLE_EFFECT_PROPS; - particleProps.isEmitting = 0; - particleProps.position = calcParticlePos(myHandPosition, otherHandPosition, otherOrientation); + particleProps.position = positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); + particleProps.rotation = Vec3.mix(Quat.getFront(MyAvatar.orientation), + Quat.inverse(Quat.getFront(otherOrientation)), 0.5); particleProps.parentID = MyAvatar.sessionUUID; particleEffect = Entities.addEntity(particleProps, true); - } else { - particleProps.position = calcParticlePos(myHandPosition, otherHandPosition, otherOrientation); - particleProps.isEmitting = 1; - Entities.editEntity(particleEffect, particleProps); } if (!makingConnectionParticleEffect) { var props = MAKING_CONNECTION_PARTICLE_PROPS; props.parentID = MyAvatar.sessionUUID; - makingConnectionEmitRate = 2000; + makingConnectionEmitRate = MAKING_CONNECTION_INITIAL_EMIT_RATE; props.emitRate = makingConnectionEmitRate; + props.isEmitting = false; props.position = myHandPosition; makingConnectionParticleEffect = Entities.addEntity(props, true); - } else { - makingConnectionEmitRate *= 0.5; - Entities.editEntity(makingConnectionParticleEffect, { - emitRate: makingConnectionEmitRate, - position: myHandPosition, - isEmitting: true - }); + makingConnectionUpdateTimer = Script.setTimeout(updateMakingConnection, MAKING_CONNECTION_UPDATE_INTERVAL); } break; case STATES.MAKING_CONNECTION: - particleEmitRate = Math.max(50, particleEmitRate * 0.5); - Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); - Entities.editEntity(particleEffect, { - position: calcParticlePos(myHandPosition, otherHandPosition, otherOrientation), - emitRate: particleEmitRate - }); + if (makingConnectionUpdateTimer) { + Script.clearTimeout(makingConnectionUpdateTimer); + makingConnectionUpdateTimer = null; + } + if (isMakingConnectionEmitting) { + Entities.editEntity(makingConnectionParticleEffect, { isEmitting: false }); + isMakingConnectionEmitting = false; + } + if (!particleEffectUpdateTimer && particleEmitRate > PARTICLE_MINIMUM_EMIT_RATE) { + particleEffectUpdateTimer = Script.setTimeout(updateParticleEffect, PARTICLE_EFFECT_UPDATE_INTERVAL); + } break; default: debug("unexpected state", state); @@ -861,12 +892,13 @@ } function keyPressEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { + if ((event.text.toUpperCase() === "X") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl + && !event.isAlt) { updateTriggers(1.0, true, Controller.Standard.RightHand); } } function keyReleaseEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { + if (event.text.toUpperCase() === "X" && !event.isAutoRepeat) { updateTriggers(0.0, true, Controller.Standard.RightHand); } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 6cfe97cc11..a5360974f6 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -24,10 +24,9 @@ var selectionDisplay = null; // for gridTool.js to ignore var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); - var MARKETPLACE_CHECKOUT_QML_PATH_BASE = "qml/hifi/commerce/checkout/Checkout.qml"; - var MARKETPLACE_CHECKOUT_QML_PATH = Script.resourcesPath() + MARKETPLACE_CHECKOUT_QML_PATH_BASE; - var MARKETPLACE_PURCHASES_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/purchases/Purchases.qml"; - var MARKETPLACE_WALLET_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/wallet/Wallet.qml"; + var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; + var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; + var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "commerce/inspectionCertificate/InspectionCertificate.qml"; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; @@ -115,7 +114,7 @@ var selectionDisplay = null; // for gridTool.js to ignore function onScreenChanged(type, url) { onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; onWalletScreen = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; - onCommerceScreen = type === "QML" && (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH_BASE) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH + onCommerceScreen = type === "QML" && (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); wireEventBridge(onMarketplaceScreen || onCommerceScreen || onWalletScreen); @@ -491,7 +490,7 @@ var selectionDisplay = null; // for gridTool.js to ignore wireEventBridge(true); tablet.sendToQml({ method: 'updateWalletReferrer', - referrer: message.itemId + referrer: message.referrer === "itemPage" ? message.itemId : message.referrer }); openWallet(); break; @@ -546,7 +545,7 @@ var selectionDisplay = null; // for gridTool.js to ignore Menu.setIsOptionChecked("Disable Preview", isHmdPreviewDisabled); break; case 'purchases_openGoTo': - tablet.loadQMLSource("TabletAddressDialog.qml"); + tablet.loadQMLSource("hifi/tablet/TabletAddressDialog.qml"); break; case 'purchases_itemCertificateClicked': setCertificateInfo("", message.itemCertificateId); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index b5551cf596..ed7059f9f3 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -40,7 +40,7 @@ var HOVER_TEXTURES = { var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6}; var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29}; var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now -var PAL_QML_SOURCE = "../Pal.qml"; +var PAL_QML_SOURCE = "hifi/Pal.qml"; var conserveResources = true; Script.include("/~/system/libraries/controllers.js"); diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 929c6e0e5c..2a0e827932 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -24,7 +24,7 @@ print('tablet-goto.js:', [].map.call(arguments, JSON.stringify)); } - var gotoQmlSource = "TabletAddressDialog.qml"; + var gotoQmlSource = "hifi/tablet/TabletAddressDialog.qml"; var buttonName = "GOTO"; var onGotoScreen = false; var shouldActivateButton = false; diff --git a/tests/shared/src/PathUtilsTests.cpp b/tests/shared/src/PathUtilsTests.cpp new file mode 100644 index 0000000000..1c445908f7 --- /dev/null +++ b/tests/shared/src/PathUtilsTests.cpp @@ -0,0 +1,27 @@ +// +// Created by Bradley Austin Davis on 2017/11/08 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include "PathUtilsTests.h" + +#include + +#include + +QTEST_MAIN(PathUtilsTests) + +void PathUtilsTests::testPathUtils() { + QString result = PathUtils::qmlBasePath(); +#if DEV_BUILD + QVERIFY(result.startsWith("file:///")); +#else + QVERIFY(result.startsWith("qrc:///")); +#endif + QVERIFY(result.endsWith("/")); +} + diff --git a/tests/shared/src/PathUtilsTests.h b/tests/shared/src/PathUtilsTests.h new file mode 100644 index 0000000000..8ae410d6cb --- /dev/null +++ b/tests/shared/src/PathUtilsTests.h @@ -0,0 +1,20 @@ +// +// Created by Bradley Austin Davis on 2017/11/08 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_PathUtilsTests_h +#define hifi_PathUtilsTests_h + +#include + +class PathUtilsTests : public QObject { + Q_OBJECT +private slots: + void testPathUtils(); +}; + +#endif // hifi_PathUtilsTests_h diff --git a/tools/ac-client/src/ACClientApp.cpp b/tools/ac-client/src/ACClientApp.cpp index e00560158f..88884a4fee 100644 --- a/tools/ac-client/src/ACClientApp.cpp +++ b/tools/ac-client/src/ACClientApp.cpp @@ -106,7 +106,7 @@ ACClientApp::ACClientApp(int argc, char* argv[]) : auto accountManager = DependencyManager::get(); accountManager->setIsAgent(true); - accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); auto nodeList = DependencyManager::get(); diff --git a/tools/atp-client/src/ATPClientApp.cpp b/tools/atp-client/src/ATPClientApp.cpp index c5edf27b67..9fd1bf8d4f 100644 --- a/tools/atp-client/src/ATPClientApp.cpp +++ b/tools/atp-client/src/ATPClientApp.cpp @@ -145,7 +145,7 @@ ATPClientApp::ATPClientApp(int argc, char* argv[]) : auto accountManager = DependencyManager::get(); accountManager->setIsAgent(true); - accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); auto nodeList = DependencyManager::get(); diff --git a/tools/jsdoc/README.md b/tools/jsdoc/README.md index c43f95cabe..5cce6bb2a6 100644 --- a/tools/jsdoc/README.md +++ b/tools/jsdoc/README.md @@ -5,9 +5,9 @@ * Install node.js * Install jsdoc via npm. `npm install jsdoc -g` -To generate html documentation for the High Fidelity JavaScript API +To generate html documentation for the High Fidelity JavaScript API: -`cd scripts/jsdoc` -`jsdoc . -c config.json` +* `cd tools/jsdoc` +* `jsdoc . -c config.json` -The out folder should contain index.html +The out folder should contain index.html. diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index 3170352ac8..3af5fbeee3 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -25,7 +25,9 @@ exports.handlers = { '../../libraries/entities/src', '../../libraries/networking/src', '../../libraries/pointers/src', + '../../libraries/render-utils/src', '../../libraries/shared/src', + '../../libraries/shared/src/shared', '../../libraries/script-engine/src', ]; var exts = ['.h', '.cpp']; diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 321f81ba8f..00344179bd 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -8,13 +8,14 @@ setup_memory_debugger() if (WIN32) package_libraries_for_deployment() -endif () - -if (UNIX) +elseif (UNIX AND NOT APPLE) find_package(Threads REQUIRED) if(THREADS_HAVE_PTHREAD_ARG) target_compile_options(PUBLIC oven "-pthread") endif() -endif () +elseif (APPLE) + # Fix up the rpath so macdeployqt works + set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks") +endif() install_beside_console() diff --git a/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js b/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js index e7a135ec9e..7bc65722cd 100644 --- a/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js +++ b/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js @@ -24,7 +24,7 @@ if (onSkyboxChangerScreen) { tablet.gotoHomeScreen(); } else { - tablet.loadQMLSource("../SkyboxChanger.qml"); + tablet.loadQMLSource("hifi/SkyboxChanger.qml"); } } diff --git a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js index 9eb543e768..730a5808b8 100644 --- a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js +++ b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js @@ -393,7 +393,7 @@ // Relevant Variables: // -SPECTATOR_CAMERA_QML_SOURCE: The path to the SpectatorCamera QML // -onSpectatorCameraScreen: true/false depending on whether we're looking at the spectator camera app. - var SPECTATOR_CAMERA_QML_SOURCE = Script.resourcesPath() + "qml/hifi/SpectatorCamera.qml"; + var SPECTATOR_CAMERA_QML_SOURCE = "hifi/SpectatorCamera.qml"; var onSpectatorCameraScreen = false; function onTabletButtonClicked() { if (!tablet) {