diff --git a/android/build_android.sh b/android/build_android.sh index 9c68b8969b..a066332f9a 100755 --- a/android/build_android.sh +++ b/android/build_android.sh @@ -3,7 +3,7 @@ set -xeuo pipefail ./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET} # This is the actual output from gradle, which no longer attempts to muck with the naming of the APK -OUTPUT_APK=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_DIR}/${ANDROID_APP}-${ANDROID_BUILD_DIR}.apk +OUTPUT_APK=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_DIR}/${ANDROID_BUILT_APK_NAME} # This is the APK name requested by Jenkins TARGET_APK=./${ANDROID_APK_NAME} # Make sure this matches up with the new ARTIFACT_EXPRESSION for jenkins builds, which should be "android/*.apk" diff --git a/android/containerized_build.sh b/android/containerized_build.sh index 60d5c9db98..8b2f26cb50 100755 --- a/android/containerized_build.sh +++ b/android/containerized_build.sh @@ -16,6 +16,7 @@ docker run \ -e RELEASE_NUMBER \ -e RELEASE_TYPE \ -e ANDROID_APP \ + -e ANDROID_BUILT_APK_NAME \ -e ANDROID_APK_NAME \ -e ANDROID_BUILD_TARGET \ -e ANDROID_BUILD_DIR \ diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index f28dc90b7d..b7afc3ed9e 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -14,7 +14,7 @@ link_hifi_libraries( audio avatars octree gpu graphics shaders fbx hfm entities networking animation recording shared script-engine embedded-webserver controllers physics plugins midi image - model-networking ktx shaders + material-networking model-networking ktx shaders ) add_dependencies(${TARGET_NAME} oven) diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index ab357f4146..af36df1eba 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -324,12 +324,6 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* if (isFullScene) { // we're forcing a full scene, clear the force in OctreeQueryNode so we don't force it next time again nodeData->setShouldForceFullScene(false); - } else { - // we aren't forcing a full scene, check if something else suggests we should - isFullScene = nodeData->haveJSONParametersChanged() || - (nodeData->hasConicalViews() && - (nodeData->getViewFrustumJustStoppedChanging() || - nodeData->hasLodChanged())); } if (nodeData->isPacketWaiting()) { diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 74e25662c7..bb8ebbd2a0 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -205,8 +205,8 @@ endif() # link required hifi libraries link_hifi_libraries( shared workload task octree ktx gpu gl procedural graphics graphics-scripting render - pointers - recording hfm fbx networking model-networking model-baker entities avatars trackers + pointers recording hfm fbx networking material-networking + model-networking model-baker entities avatars trackers audio audio-client animation script-engine physics render-utils entities-renderer avatars-renderer ui qml auto-updater midi controllers plugins image trackers diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 9428e3ab6e..629027ab2a 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -75,7 +75,7 @@ ModalWindow { QtObject { id: d - readonly property int minWidth: 480 + readonly property int minWidth: 1100 readonly property int maxWidth: 1280 readonly property int minHeight: 120 readonly property int maxHeight: 720 @@ -95,6 +95,7 @@ ModalWindow { id: mainTextContainer onTextChanged: d.resize(); wrapMode: Text.WordWrap + width: messageBox.width size: hifi.fontSizes.menuItem color: hifi.colors.baseGrayHighlight anchors { diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index b7d3a22e44..ca6838efea 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -31,8 +31,9 @@ Rectangle { id: root property string activeView: "initialize" - property int currentSortIndex: 0 + property int currentSortIndex: 1 property string sortString: "recent" + property bool isAscending: false property string categoryString: "" property string searchString: "" property bool keyboardEnabled: HMD.active @@ -316,23 +317,28 @@ Rectangle { font.pixelSize: hifi.fontSizes.textFieldInput placeholderText: "Search Marketplace" + Timer { + id: keypressTimer + running: false + repeat: false + interval: 300 + onTriggered: searchField.accepted() + + } + // workaround for https://bugreports.qt.io/browse/QTBUG-49297 Keys.onPressed: { switch (event.key) { case Qt.Key_Return: case Qt.Key_Enter: event.accepted = true; + searchField.text = ""; - // emit accepted signal manually - if (acceptableInput) { - searchField.accepted(); - searchField.forceActiveFocus(); - } + getMarketplaceItems(); + searchField.forceActiveFocus(); break; - case Qt.Key_Backspace: - if (searchField.text === "") { - primaryFilter_index = -1; - } + default: + keypressTimer.restart(); break; } } @@ -498,6 +504,7 @@ Rectangle { "", "", root.sortString, + root.isAscending, WalletScriptingInterface.limitedCommerce, marketBrowseModel.currentPageToRetrieve, marketBrowseModel.itemsPerPage @@ -726,7 +733,8 @@ Rectangle { top: parent.top leftMargin: 20 } - width: root.isLoggedIn ? 322 : 242 + + width: root.isLoggedIn ? 342 : 262 height: parent.height radius: 4 @@ -737,27 +745,27 @@ Rectangle { id: sortModel ListElement { - name: "Name"; - glyph: ";" + name: "Name" sortString: "alpha" + ascending: true } ListElement { - name: "Date"; - glyph: ";"; - sortString: "recent"; + name: "Date" + sortString: "recent" + ascending: false } ListElement { - name: "Popular"; - glyph: ";"; - sortString: "likes"; + name: "Popular" + sortString: "likes" + ascending: false } ListElement { - name: "My Likes"; - glyph: ";"; - sortString: "my_likes"; + name: "My Likes" + sortString: "my_likes" + ascending: false } } @@ -783,10 +791,10 @@ Rectangle { currentIndex: 1; delegate: SortButton { - width: 80 + width: 85 height: parent.height - glyph: model.glyph + ascending: model.ascending text: model.name visible: root.isLoggedIn || model.sortString != "my_likes" @@ -794,6 +802,12 @@ Rectangle { checked: ListView.isCurrentItem onClicked: { + if(root.currentSortIndex == index) { + ascending = !ascending; + } else { + ascending = model.ascending; + } + root.isAscending = ascending; root.currentSortIndex = index; sortListView.positionViewAtIndex(index, ListView.Beginning); sortListView.currentIndex = index; @@ -802,7 +816,7 @@ Rectangle { } } highlight: Rectangle { - width: 80 + width: 85 height: parent.height color: hifi.colors.faintGray @@ -1125,6 +1139,8 @@ Rectangle { fill: parent } + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + RalewayRegular { id: licenseText diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index bbf9be908a..45ed213660 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -526,7 +526,7 @@ Rectangle { } else if (root.license === "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)") { url = "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.txt" } else if (root.license === "Proof of Provenance License (PoP License)") { - url = "https://digitalassetregistry.com/PoP-License/v1/" + url = "licenses/Popv1.txt" } if(url) { showLicense(url) diff --git a/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml index 37ad2735ce..2673043b6b 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml @@ -28,58 +28,60 @@ Item { id: root; + property string ascGlyph: "\u2193" + property string descGlyph: "\u2191" + property string text: "" + property bool ascending: false + property bool checked: false + signal clicked() - property string glyph: ""; - property string text: ""; - property bool checked: false; - signal clicked(); - - width: childrenRect.width; - height: parent.height; + width: childrenRect.width + height: parent.height Rectangle { - anchors.top: parent.top; - anchors.left: parent.left; - height: parent.height; - width: 2; - color: hifi.colors.faintGray; - visible: index > 0; + anchors.top: parent.top + anchors.left: parent.left + height: parent.height + width: 2 + color: hifi.colors.faintGray + visible: index > 0 } - HiFiGlyphs { - id: buttonGlyph; - text: root.glyph; + RalewayRegular { + id: buttonGlyph + text: root.ascending ? root.ascGlyph : root.descGlyph // Size - size: 14; + size: 14 // Anchors - anchors.left: parent.left; - anchors.leftMargin: 0; - anchors.top: parent.top; - anchors.verticalCenter: parent.verticalCenter; - height: parent.height; - horizontalAlignment: Text.AlignHCenter; + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.top: parent.top + anchors.topMargin: 6 + anchors.bottom: parent.bottom + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop // Style - color: hifi.colors.lightGray; + color: hifi.colors.lightGray } RalewayRegular { - id: buttonText; - text: root.text; + id: buttonText + text: root.text // Text size - size: 14; + size: 14 // Style - color: hifi.colors.lightGray; - elide: Text.ElideRight; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + color: hifi.colors.lightGray + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter // Anchors - anchors.left: parent.left; - anchors.leftMargin: 20; - anchors.top: parent.top; - height: parent.height; + anchors.left: buttonGlyph.right + anchors.leftMargin: 5 + anchors.top: parent.top + height: parent.height } MouseArea { - anchors.fill: parent; - hoverEnabled: enabled; + anchors.fill: parent + hoverEnabled: enabled onClicked: { root.clicked(); } diff --git a/interface/resources/qml/hifi/commerce/marketplace/licenses/Popv1.txt b/interface/resources/qml/hifi/commerce/marketplace/licenses/Popv1.txt new file mode 100644 index 0000000000..1f44fc19e6 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/licenses/Popv1.txt @@ -0,0 +1,12 @@ + +

Proof of Provenance License (PoP License) v1.0

+
+

+ Subject to the terms and conditions of this license, the Copyright Holder grants a worldwide, non-exclusive, non-sublicensable, non-transferable (except by transfer of the Certificate or beneficial ownership thereof) license (i) to the Certificate Holder to display ONE COPY of the Item at a time across any and all virtual worlds WITHOUT MODIFICATION; (ii) to any party to view and interact with the Item as displayed by the Certificate Holder. Redistributions of source code must retain the all copyright notices. Notwithstanding the foregoing, modification of the Item may be permitted pursuant to terms provided in the Certificate. +

+

+ THE ITEM IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR A CONTRIBUTOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ITEM, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +

+

+ Reference to the “Certificate” means the Proof of Provenance Certificate containing a hash of the code used to generate the Item; ‘Item’ means the visual representation produced by the execution of the code hashed in the Certificate (which term includes the code itself); and “Certificate Holder” means a single holder of the private key for the Certificate. +

diff --git a/interface/src/AboutUtil.h b/interface/src/AboutUtil.h index 1495045b85..c06255aaa5 100644 --- a/interface/src/AboutUtil.h +++ b/interface/src/AboutUtil.h @@ -16,15 +16,23 @@ #include /**jsdoc + * The HifiAbout API provides information about the version of Interface that is currently running. It also + * provides the ability to open a Web page in an Interface browser window. + * * @namespace HifiAbout * * @hifi-interface * @hifi-client-entity * @hifi-avatar * - * @property {string} buildDate - * @property {string} buildVersion - * @property {string} qtVersion + * @property {string} buildDate - The build date of Interface that is currently running. Read-only. + * @property {string} buildVersion - The build version of Interface that is currently running. Read-only. + * @property {string} qtVersion - The Qt version used in Interface that is currently running. Read-only. + * + * @example Report build information for the version of Interface currently running. + * print("HiFi build date: " + HifiAbout.buildDate); // 11 Feb 2019 + * print("HiFi version: " + HifiAbout.buildVersion); // 0.78.0 + * print("Qt version: " + HifiAbout.qtVersion); // 5.10.1 */ class AboutUtil : public QObject { @@ -44,8 +52,9 @@ public: public slots: /**jsdoc + * Display a Web page in an Interface browser window. * @function HifiAbout.openUrl - * @param {string} url + * @param {string} url - The URL of the Web page to display. */ void openUrl(const QString &url) const; private: diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bbe4d70ab6..b65f39ffd7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -102,7 +102,7 @@ #include #include #include -#include +#include #include #include #include @@ -154,7 +154,7 @@ #include #include #include -#include +#include #include "recording/ClipCache.h" #include "AudioClient.h" @@ -6988,17 +6988,19 @@ void Application::nodeActivated(SharedNodePointer node) { #if !defined(DISABLE_QML) auto offscreenUi = getOffscreenUI(); - auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; - if (assetDialog) { + if (offscreenUi) { auto nodeList = DependencyManager::get(); if (nodeList->getThisNodeCanWriteAssets()) { // call reload on the shown asset browser dialog to get the mappings (if permissions allow) - QMetaObject::invokeMethod(assetDialog, "reload"); + auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; + if (assetDialog) { + QMetaObject::invokeMethod(assetDialog, "reload"); + } } else { // we switched to an Asset Server that we can't modify, hide the Asset Browser - assetDialog->setVisible(false); + offscreenUi->hide("AssetServer"); } } #endif diff --git a/interface/src/commerce/QmlMarketplace.cpp b/interface/src/commerce/QmlMarketplace.cpp index 23ba418a2d..8197b20275 100644 --- a/interface/src/commerce/QmlMarketplace.cpp +++ b/interface/src/commerce/QmlMarketplace.cpp @@ -50,9 +50,10 @@ void QmlMarketplace::getMarketplaceItems( const QString& adminFilter, const QString& adminFilterCost, const QString& sort, - const bool isFree, - const int& page, - const int& perPage) { + bool isAscending, + bool isFree, + int page, + int perPage) { QString endpoint = "items"; QUrlQuery request; @@ -62,6 +63,7 @@ void QmlMarketplace::getMarketplaceItems( request.addQueryItem("adminFilter", adminFilter); request.addQueryItem("adminFilterCost", adminFilterCost); request.addQueryItem("sort", sort); + request.addQueryItem("sort_dir", isAscending ? "asc" : "desc"); if (isFree) { request.addQueryItem("isFree", "true"); } diff --git a/interface/src/commerce/QmlMarketplace.h b/interface/src/commerce/QmlMarketplace.h index 5794d4f53c..76b3d41449 100644 --- a/interface/src/commerce/QmlMarketplace.h +++ b/interface/src/commerce/QmlMarketplace.h @@ -46,9 +46,10 @@ protected: const QString& adminFilter = QString("published"), const QString& adminFilterCost = QString(), const QString& sort = QString(), - const bool isFree = false, - const int& page = 1, - const int& perPage = 20); + bool isAscending = false, + bool isFree = false, + int page = 1, + int perPage = 20); Q_INVOKABLE void getMarketplaceItem(const QString& marketplaceItemId); Q_INVOKABLE void marketplaceItemLike(const QString& marketplaceItemId, const bool like = true); Q_INVOKABLE void getMarketplaceCategories(); diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index c096713058..5e5e6c9b4f 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -54,6 +54,29 @@ public: void clear(); void getWalletStatus(); + + /**jsdoc + *

A WalletStatus may have one of the following values:

+ * + * + * + * + * + * + * + * + * + * + * + * + *
ValueMeaningDescription
0Not logged inThe user isn't logged in.
1Not set upThe user's wallet isn't set up.
2Pre-existingThere is a wallet present on the server but not one + * locally.
3ConflictingThere is a wallet present on the server plus one present locally, + * and they don't match.
4Not authenticatedThere is a wallet present locally but the user hasn't + * logged into it.
5ReadyThe wallet is ready for use.
+ *

Wallets used to be stored locally but now they're stored on the server, unless the computer once had a wallet stored + * locally in which case the wallet may be present in both places.

+ * @typedef {number} WalletScriptingInterface.WalletStatus + */ enum WalletStatus { WALLET_STATUS_NOT_LOGGED_IN = 0, WALLET_STATUS_NOT_SET_UP, diff --git a/interface/src/scripting/AccountServicesScriptingInterface.cpp b/interface/src/scripting/AccountServicesScriptingInterface.cpp index 3939fce91d..a3597886e9 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.cpp +++ b/interface/src/scripting/AccountServicesScriptingInterface.cpp @@ -112,6 +112,12 @@ DownloadInfoResult::DownloadInfoResult() : { } +/**jsdoc + * Information on the assets currently being downloaded and pending download. + * @typedef {object} AccountServices.DownloadInfoResult + * @property {number[]} downloading - The percentage complete for each asset currently being downloaded. + * @property {number} pending - The number of assets waiting to be download. + */ QScriptValue DownloadInfoResultToScriptValue(QScriptEngine* engine, const DownloadInfoResult& result) { QScriptValue object = engine->newObject(); diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index 55445a864d..c08181d7c9 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -38,17 +38,25 @@ class AccountServicesScriptingInterface : public QObject { Q_OBJECT /**jsdoc - * The AccountServices API contains helper functions related to user connectivity + * The AccountServices API provides functions related to user connectivity, visibility, and asset download + * progress. * * @hifi-interface * @hifi-client-entity * @hifi-avatar * * @namespace AccountServices - * @property {string} username Read-only. - * @property {boolean} loggedIn Read-only. - * @property {string} findableBy - * @property {string} metaverseServerURL Read-only. + * @property {string} username - The user name if the user is logged in, otherwise "Unknown user". + * Read-only. + * @property {boolean} loggedIn - true if the user is logged in, otherwise false. + * Read-only. + * @property {string} findableBy - The user's visibility to other people:
+ * "none" - user appears offline.
+ * "friends" - user is visible only to friends.
+ * "connections" - user is visible to friends and connections.
+ * "all" - user is visible to everyone. + * @property {string} metaverseServerURL - The metaverse server that the user is authenticated against when logged in + * — typically "https://metaverse.highfidelity.com". Read-only. */ Q_PROPERTY(QString username READ getUsername NOTIFY myUsernameChanged) @@ -66,29 +74,38 @@ public: public slots: /**jsdoc + * Get information on the progress of downloading assets in the domain. * @function AccountServices.getDownloadInfo - * @returns {DownloadInfoResult} + * @returns {AccountServices.DownloadInfoResult} Information on the progress of assets download. */ DownloadInfoResult getDownloadInfo(); /**jsdoc + * Cause a {@link AccountServices.downloadInfoChanged|downloadInfoChanged} signal to be triggered with information on the + * current progress of the download of assets in the domain. * @function AccountServices.updateDownloadInfo */ void updateDownloadInfo(); /**jsdoc + * Check whether the user is logged in. * @function AccountServices.isLoggedIn - * @returns {boolean} + * @returns {boolean} true if the user is logged in, false otherwise. + * @example Report whether you are logged in. + * var isLoggedIn = AccountServices.isLoggedIn(); + * print("You are logged in: " + isLoggedIn); // true or false */ bool isLoggedIn(); /**jsdoc + * Prompts the user to log in (the login dialog is displayed) if they're not already logged in. * @function AccountServices.checkAndSignalForAccessToken - * @returns {boolean} + * @returns {boolean} true if the user is already logged in, false otherwise. */ bool checkAndSignalForAccessToken(); /**jsdoc + * Logs the user out. * @function AccountServices.logOut */ void logOut(); @@ -106,43 +123,75 @@ private slots: signals: /**jsdoc + * Not currently used. * @function AccountServices.connected * @returns {Signal} */ void connected(); /**jsdoc + * Triggered when the user logs out. * @function AccountServices.disconnected - * @param {string} reason + * @param {string} reason - Has the value, "logout". * @returns {Signal} */ void disconnected(const QString& reason); /**jsdoc + * Triggered when the username logged in with changes, i.e., when the user logs in or out. * @function AccountServices.myUsernameChanged - * @param {string} username + * @param {string} username - The username logged in with if the user is logged in, otherwise "". * @returns {Signal} + * @example Report when your username changes. + * AccountServices.myUsernameChanged.connect(function (username) { + * print("Username changed: " + username); + * }); */ void myUsernameChanged(const QString& username); /**jsdoc + * Triggered when the progress of the download of assets for the domain changes. * @function AccountServices.downloadInfoChanged - * @param {} info + * @param {AccountServices.DownloadInfoResult} downloadInfo - Information on the progress of assets download. * @returns {Signal} */ void downloadInfoChanged(DownloadInfoResult info); /**jsdoc + * Triggered when the user's visibility to others changes. * @function AccountServices.findableByChanged - * @param {string} discoverabilityMode + * @param {string} findableBy - The user's visibility to other people:
+ * "none" - user appears offline.
+ * "friends" - user is visible only to friends.
+ * "connections" - user is visible to friends and connections.
+ * "all" - user is visible to everyone. * @returns {Signal} + * @example Report when your visiblity changes. + * AccountServices.findableByChanged.connect(function (findableBy) { + * print("Findable by changed: " + findableBy); + * }); + * + * var originalFindableBy = AccountServices.findableBy; + * Script.setTimeout(function () { + * // Change visiblity. + * AccountServices.findableBy = originalFindableBy === "none" ? "all" : "none"; + * }, 2000); + * Script.setTimeout(function () { + * // Restore original visibility. + * AccountServices.findableBy = originalFindableBy; + * }, 4000); */ void findableByChanged(const QString& discoverabilityMode); /**jsdoc + * Triggered when the login status of the user changes. * @function AccountServices.loggedInChanged - * @param {boolean} loggedIn + * @param {boolean} loggedIn - true if the user is logged in, otherwise false. * @returns {Signal} + * @example Report when your login status changes. + * AccountServices.loggedInChanged.connect(function(loggedIn) { + * print("Logged in: " + loggedIn); + * }); */ void loggedInChanged(bool loggedIn); diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 005af7f558..7482b8be00 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -1,4 +1,4 @@ - +// // WalletScriptingInterface.h // interface/src/scripting // @@ -30,14 +30,19 @@ public: }; /**jsdoc - * @namespace Wallet + * The WalletScriptingInterface API provides functions related to the user's wallet and verification of certified + * avatar entities. + * + * @namespace WalletScriptingInterface * * @hifi-interface * @hifi-client-entity * @hifi-avatar * - * @property {number} walletStatus - * @property {bool} limitedCommerce + * @property {WalletScriptingInterface.WalletStatus} walletStatus - The status of the user's wallet. Read-only. + * @property {boolean} limitedCommerce - true if Interface is running in limited commerce mode. In limited commerce + * mode, certain Interface functionality is disabled, e.g., users can't buy non-free items from the Marketplace. The Oculus + * Store version of Interface runs in limited commerce mode. Read-only. */ class WalletScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -50,19 +55,56 @@ public: WalletScriptingInterface(); /**jsdoc + * Check and update the user's wallet status. * @function WalletScriptingInterface.refreshWalletStatus */ Q_INVOKABLE void refreshWalletStatus(); /**jsdoc + * Get the current status of the user's wallet. * @function WalletScriptingInterface.getWalletStatus - * @returns {number} + * @returns {WalletScriptingInterface.WalletStatus} + * @example Two ways to report your wallet status. + * print("Wallet status: " + WalletScriptingInterface.walletStatus); // Same value as next line. + * print("Wallet status: " + WalletScriptingInterface.getWalletStatus()); */ Q_INVOKABLE uint getWalletStatus() { return _walletStatus; } /**jsdoc + * Check that a certified avatar entity is owned by the avatar whose entity it is. The result of the check is provided via + * the {@link WalletScriptingInterface.ownershipVerificationSuccess|ownershipVerificationSuccess} and + * {@link WalletScriptingInterface.ownershipVerificationFailed|ownershipVerificationFailed} signals.
+ * Warning: Neither of these signals fire if the entity is not an avatar entity or it's not a certified + * entity. * @function WalletScriptingInterface.proveAvatarEntityOwnershipVerification - * @param {Uuid} entityID + * @param {Uuid} entityID - The ID of the avatar entity to check. + * @example Check ownership of all nearby certified avatar entities. + * // Set up response handling. + * function ownershipSuccess(entityID) { + * print("Ownership test succeeded for: " + entityID); + * } + * function ownershipFailed(entityID) { + * print("Ownership test failed for: " + entityID); + * } + * WalletScriptingInterface.ownershipVerificationSuccess.connect(ownershipSuccess); + * WalletScriptingInterface.ownershipVerificationFailed.connect(ownershipFailed); + * + * // Check ownership of all nearby certified avatar entities. + * var entityIDs = Entities.findEntities(MyAvatar.position, 10); + * var i, length; + * for (i = 0, length = entityIDs.length; i < length; i++) { + * var properties = Entities.getEntityProperties(entityIDs[i], ["entityHostType", "certificateID"]); + * if (properties.entityHostType === "avatar" && properties.certificateID !== "") { + * print("Prove ownership of: " + entityIDs[i]); + * WalletScriptingInterface.proveAvatarEntityOwnershipVerification(entityIDs[i]); + * } + * } + * + * // Tidy up. + * Script.scriptEnding.connect(function () { + * WalletScriptingInterface.ownershipVerificationFailed.disconnect(ownershipFailed); + * WalletScriptingInterface.ownershipVerificationSuccess.disconnect(ownershipSuccess); + * }); */ Q_INVOKABLE void proveAvatarEntityOwnershipVerification(const QUuid& entityID); @@ -76,33 +118,45 @@ public: signals: /**jsdoc + * Triggered when the status of the user's wallet changes. * @function WalletScriptingInterface.walletStatusChanged * @returns {Signal} + * @example Report when your wallet status changes, e.g., when you log in and out. + * WalletScriptingInterface.walletStatusChanged.connect(function () { + * print("Wallet status changed to: " + WalletScriptingInterface.walletStatus"); + * }); */ void walletStatusChanged(); /**jsdoc + * Triggered when the user's limited commerce status changes. * @function WalletScriptingInterface.limitedCommerceChanged * @returns {Signal} */ void limitedCommerceChanged(); /**jsdoc + * Triggered when the user rezzes a certified entity but the user's wallet is not ready and so the certified location of the + * entity cannot be updated in the metaverse. * @function WalletScriptingInterface.walletNotSetup * @returns {Signal} */ void walletNotSetup(); /**jsdoc + * Triggered when a certified avatar entity's ownership check requested via + * {@link WalletScriptingInterface.proveAvatarEntityOwnershipVerification|proveAvatarEntityOwnershipVerification} succeeds. * @function WalletScriptingInterface.ownershipVerificationSuccess - * @param {Uuid} entityID + * @param {Uuid} entityID - The ID of the avatar entity checked. * @returns {Signal} */ void ownershipVerificationSuccess(const QUuid& entityID); /**jsdoc + * Triggered when a certified avatar entity's ownership check requested via + * {@link WalletScriptingInterface.proveAvatarEntityOwnershipVerification|proveAvatarEntityOwnershipVerification} fails. * @function WalletScriptingInterface.ownershipVerificationFailed - * @param {Uuid} entityID + * @param {Uuid} entityID - The ID of the avatar entity checked. * @returns {Signal} */ void ownershipVerificationFailed(const QUuid& entityID); diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 4e988334f9..237fd3da02 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -41,7 +41,7 @@ QSharedPointer AnimationCache::createResource(const QUrl& url) { } QSharedPointer AnimationCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new Animation(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new Animation(*resource.staticCast()), &Resource::deleter); } AnimationReader::AnimationReader(const QUrl& url, const QByteArray& data) : diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp index 343de46e9a..c36897c766 100644 --- a/libraries/audio/src/SoundCache.cpp +++ b/libraries/audio/src/SoundCache.cpp @@ -40,5 +40,5 @@ QSharedPointer SoundCache::createResource(const QUrl& url) { } QSharedPointer SoundCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new Sound(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new Sound(*resource.staticCast()), &Resource::deleter); } \ No newline at end of file diff --git a/libraries/avatars-renderer/CMakeLists.txt b/libraries/avatars-renderer/CMakeLists.txt index 06a3804ece..de1ac1a7c2 100644 --- a/libraries/avatars-renderer/CMakeLists.txt +++ b/libraries/avatars-renderer/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME avatars-renderer) setup_hifi_library(Network Script) -link_hifi_libraries(shared shaders gpu graphics animation model-networking script-engine render render-utils image trackers entities-renderer) +link_hifi_libraries(shared shaders gpu graphics animation material-networking model-networking script-engine render render-utils image trackers entities-renderer) include_hifi_library_headers(avatars) include_hifi_library_headers(networking) include_hifi_library_headers(hfm) diff --git a/libraries/display-plugins/CMakeLists.txt b/libraries/display-plugins/CMakeLists.txt index 8e966ed9ea..ad6503b22d 100644 --- a/libraries/display-plugins/CMakeLists.txt +++ b/libraries/display-plugins/CMakeLists.txt @@ -2,6 +2,7 @@ set(TARGET_NAME display-plugins) setup_hifi_library(Gui) link_hifi_libraries(shared shaders plugins ui-plugins gl ui render-utils ${PLATFORM_GL_BACKEND}) include_hifi_library_headers(gpu) +include_hifi_library_headers(material-networking) include_hifi_library_headers(model-networking) include_hifi_library_headers(networking) include_hifi_library_headers(graphics) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index e4deaf8f4b..20fc9a2290 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -720,7 +720,7 @@ void OpenGLDisplayPlugin::present() { } gpu::Backend::freeGPUMemSize.set(gpu::gl::getFreeDedicatedMemory()); - } else { + } else if (alwaysPresent()) { internalPresent(); } _movingAveragePresent.addSample((float)(usecTimestampNow() - startPresent)); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 5c653f8a0a..49a38ecb4c 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -88,6 +88,11 @@ protected: glm::uvec2 getSurfaceSize() const; glm::uvec2 getSurfacePixels() const; + // Some display plugins require us to always execute some present logic, + // whether we have a frame or not (Oculus Mobile plugin) + // Such plugins must be prepared to do the right thing if the `_currentFrame` + // is not populated + virtual bool alwaysPresent() const { return false; } void updateCompositeFramebuffer(); diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index b08856e8a8..e1896cf674 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME entities-renderer) setup_hifi_library(Network Script) -link_hifi_libraries(shared workload gpu shaders procedural graphics model-networking script-engine render render-utils image qml ui pointers) +link_hifi_libraries(shared workload gpu shaders procedural graphics material-networking model-networking script-engine render render-utils image qml ui pointers) include_hifi_library_headers(networking) include_hifi_library_headers(gl) include_hifi_library_headers(ktx) diff --git a/libraries/entities/CMakeLists.txt b/libraries/entities/CMakeLists.txt index e359c7132f..56adb59914 100644 --- a/libraries/entities/CMakeLists.txt +++ b/libraries/entities/CMakeLists.txt @@ -6,4 +6,4 @@ include_hifi_library_headers(fbx) include_hifi_library_headers(gpu) include_hifi_library_headers(image) include_hifi_library_headers(ktx) -link_hifi_libraries(shared shaders networking octree avatars graphics model-networking) +link_hifi_libraries(shared shaders networking octree avatars graphics material-networking model-networking) diff --git a/libraries/entities/src/MaterialEntityItem.h b/libraries/entities/src/MaterialEntityItem.h index ba142d7719..069c71c1d6 100644 --- a/libraries/entities/src/MaterialEntityItem.h +++ b/libraries/entities/src/MaterialEntityItem.h @@ -13,7 +13,7 @@ #include "MaterialMappingMode.h" #include -#include +#include class MaterialEntityItem : public EntityItem { using Pointer = std::shared_ptr; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 207ee2982d..9e7f422b40 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1326,7 +1326,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr hfmModel.meshExtents.reset(); // Create the Material Library - consolidateHFMMaterials(mapping); + consolidateHFMMaterials(); // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key diff --git a/libraries/fbx/src/FBXSerializer.h b/libraries/fbx/src/FBXSerializer.h index b95bb729e7..379b1ac743 100644 --- a/libraries/fbx/src/FBXSerializer.h +++ b/libraries/fbx/src/FBXSerializer.h @@ -153,7 +153,7 @@ public: QHash _hfmMaterials; QHash _materialParams; - void consolidateHFMMaterials(const QVariantHash& mapping); + void consolidateHFMMaterials(); bool _loadLightmaps { true }; float _lightmapOffset { 0.0f }; diff --git a/libraries/fbx/src/FBXSerializer_Material.cpp b/libraries/fbx/src/FBXSerializer_Material.cpp index 9caf713e75..b47329e483 100644 --- a/libraries/fbx/src/FBXSerializer_Material.cpp +++ b/libraries/fbx/src/FBXSerializer_Material.cpp @@ -75,15 +75,7 @@ HFMTexture FBXSerializer::getTexture(const QString& textureID, const QString& ma return texture; } -void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { - QJsonObject materialMap; - if (mapping.contains("materialMap")) { - QByteArray materialMapValue = mapping.value("materialMap").toByteArray(); - materialMap = QJsonDocument::fromJson(materialMapValue).object(); - if (materialMap.isEmpty()) { - qCDebug(modelformat) << "fbx Material Map found but did not produce valid JSON:" << materialMapValue; - } - } +void FBXSerializer::consolidateHFMMaterials() { for (QHash::iterator it = _hfmMaterials.begin(); it != _hfmMaterials.end(); it++) { HFMMaterial& material = (*it); @@ -266,23 +258,6 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { } qCDebug(modelformat) << " fbx material Name:" << material.name; - if (materialMap.contains(material.name)) { - QJsonObject materialOptions = materialMap.value(material.name).toObject(); - qCDebug(modelformat) << "Mapping fbx material:" << material.name << " with HifiMaterial: " << materialOptions; - - if (materialOptions.contains("scattering")) { - float scattering = (float) materialOptions.value("scattering").toDouble(); - material._material->setScattering(scattering); - } - - if (materialOptions.contains("scatteringMap")) { - QByteArray scatteringMap = materialOptions.value("scatteringMap").toVariant().toByteArray(); - material.scatteringTexture = HFMTexture(); - material.scatteringTexture.name = material.name + ".scatteringMap"; - material.scatteringTexture.filename = scatteringMap; - } - } - if (material.opacity <= 0.0f) { material._material->setOpacity(1.0f); } else { diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 82a4361723..736e7831c1 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -892,6 +892,23 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { for (int n = 0; n < colors.size() - 3; n += stride) { mesh.colors.push_back(glm::vec3(colors[n], colors[n + 1], colors[n + 2])); } + } else if (key == "TANGENT") { + QVector tangents; + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + tangents, + accessor.type, + accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF TANGENT data for model " << _url; + continue; + } + int stride = (accessor.type == GLTFAccessorType::VEC4) ? 4 : 3; + for (int n = 0; n < tangents.size() - 3; n += stride) { + float tanW = stride == 4 ? tangents[n + 3] : 1; + mesh.tangents.push_back(glm::vec3(tanW * tangents[n], tangents[n + 1], tangents[n + 2])); + } } else if (key == "TEXCOORD_0") { QVector texcoords; success = addArrayOfType(buffer.blob, @@ -931,7 +948,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { } mesh.parts.push_back(part); - // populate the texture coordenates if they don't exist + // populate the texture coordinates if they don't exist if (mesh.texCoords.size() == 0) { for (int i = 0; i < part.triangleIndices.size(); i++) mesh.texCoords.push_back(glm::vec2(0.0, 1.0)); } diff --git a/libraries/gpu/src/gpu/State.h b/libraries/gpu/src/gpu/State.h index abe0cd7731..2e8a3f2cab 100755 --- a/libraries/gpu/src/gpu/State.h +++ b/libraries/gpu/src/gpu/State.h @@ -246,7 +246,7 @@ public: struct Flags { Flags() : - frontFaceClockwise(false), depthClampEnable(false), scissorEnable(false), multisampleEnable(false), + frontFaceClockwise(false), depthClampEnable(false), scissorEnable(false), multisampleEnable(true), antialisedLineEnable(true), alphaToCoverageEnable(false), _spare1(0) {} bool frontFaceClockwise : 1; bool depthClampEnable : 1; diff --git a/libraries/graphics-scripting/CMakeLists.txt b/libraries/graphics-scripting/CMakeLists.txt index 0f59fb41f8..9bb34adda1 100644 --- a/libraries/graphics-scripting/CMakeLists.txt +++ b/libraries/graphics-scripting/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME graphics-scripting) setup_hifi_library() -link_hifi_libraries(shared networking graphics fbx image model-networking script-engine) +link_hifi_libraries(shared networking graphics fbx image material-networking model-networking script-engine) include_hifi_library_headers(gpu) diff --git a/libraries/material-networking/CMakeLists.txt b/libraries/material-networking/CMakeLists.txt new file mode 100644 index 0000000000..2bf8ea213d --- /dev/null +++ b/libraries/material-networking/CMakeLists.txt @@ -0,0 +1,5 @@ +set(TARGET_NAME material-networking) +setup_hifi_library() +link_hifi_libraries(shared shaders networking graphics ktx image gl) +include_hifi_library_headers(gpu) +include_hifi_library_headers(hfm) \ No newline at end of file diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/material-networking/src/material-networking/KTXCache.cpp similarity index 100% rename from libraries/model-networking/src/model-networking/KTXCache.cpp rename to libraries/material-networking/src/material-networking/KTXCache.cpp diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/material-networking/src/material-networking/KTXCache.h similarity index 100% rename from libraries/model-networking/src/model-networking/KTXCache.h rename to libraries/material-networking/src/material-networking/KTXCache.h diff --git a/libraries/model-networking/src/model-networking/MaterialCache.cpp b/libraries/material-networking/src/material-networking/MaterialCache.cpp similarity index 63% rename from libraries/model-networking/src/model-networking/MaterialCache.cpp rename to libraries/material-networking/src/material-networking/MaterialCache.cpp index aaa9767397..6561fc697e 100644 --- a/libraries/model-networking/src/model-networking/MaterialCache.cpp +++ b/libraries/material-networking/src/material-networking/MaterialCache.cpp @@ -425,5 +425,311 @@ QSharedPointer MaterialCache::createResource(const QUrl& url) { } QSharedPointer MaterialCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkMaterialResource(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new NetworkMaterialResource(*resource.staticCast()), &Resource::deleter); +} + +NetworkMaterial::NetworkMaterial(const NetworkMaterial& m) : + Material(m), + _textures(m._textures), + _albedoTransform(m._albedoTransform), + _lightmapTransform(m._lightmapTransform), + _lightmapParams(m._lightmapParams), + _isOriginal(m._isOriginal) +{} + +const QString NetworkMaterial::NO_TEXTURE = QString(); + +const QString& NetworkMaterial::getTextureName(MapChannel channel) { + if (_textures[channel].texture) { + return _textures[channel].name; + } + return NO_TEXTURE; +} + +QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const HFMTexture& texture) { + if (texture.content.isEmpty()) { + // External file: search relative to the baseUrl, in case filename is relative + return baseUrl.resolved(QUrl(texture.filename)); + } else { + // Inlined file: cache under the fbx file to avoid namespace clashes + // NOTE: We cannot resolve the path because filename may be an absolute path + assert(texture.filename.size() > 0); + auto baseUrlStripped = baseUrl.toDisplayString(QUrl::RemoveFragment | QUrl::RemoveQuery | QUrl::RemoveUserInfo); + if (texture.filename.at(0) == '/') { + return baseUrlStripped + texture.filename; + } else { + return baseUrlStripped + '/' + texture.filename; + } + } +} + +graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, + image::TextureUsage::Type type, MapChannel channel) { + + if (baseUrl.isEmpty()) { + return nullptr; + } + + const auto url = getTextureUrl(baseUrl, hfmTexture); + const auto texture = DependencyManager::get()->getTexture(url, type, hfmTexture.content, hfmTexture.maxNumPixels, hfmTexture.sourceChannel); + _textures[channel] = Texture { hfmTexture.name, texture }; + + auto map = std::make_shared(); + if (texture) { + map->setTextureSource(texture->_textureSource); + } + map->setTextureTransform(hfmTexture.transform); + + return map; +} + +graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel) { + auto textureCache = DependencyManager::get(); + if (textureCache && !url.isEmpty()) { + auto texture = textureCache->getTexture(url, type); + _textures[channel].texture = texture; + + auto map = std::make_shared(); + if (texture) { + map->setTextureSource(texture->_textureSource); + } + + return map; + } + return nullptr; +} + +void NetworkMaterial::setAlbedoMap(const QUrl& url, bool useAlphaChannel) { + auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + if (map) { + map->setUseAlphaChannel(useAlphaChannel); + setTextureMap(MapChannel::ALBEDO_MAP, map); + } +} + +void NetworkMaterial::setNormalMap(const QUrl& url, bool isBumpmap) { + auto map = fetchTextureMap(url, isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); + if (map) { + setTextureMap(MapChannel::NORMAL_MAP, map); + } +} + +void NetworkMaterial::setRoughnessMap(const QUrl& url, bool isGloss) { + auto map = fetchTextureMap(url, isGloss ? image::TextureUsage::GLOSS_TEXTURE : image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + if (map) { + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } +} + +void NetworkMaterial::setMetallicMap(const QUrl& url, bool isSpecular) { + auto map = fetchTextureMap(url, isSpecular ? image::TextureUsage::SPECULAR_TEXTURE : image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + if (map) { + setTextureMap(MapChannel::METALLIC_MAP, map); + } +} + +void NetworkMaterial::setOcclusionMap(const QUrl& url) { + auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + if (map) { + setTextureMap(MapChannel::OCCLUSION_MAP, map); + } +} + +void NetworkMaterial::setEmissiveMap(const QUrl& url) { + auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + if (map) { + setTextureMap(MapChannel::EMISSIVE_MAP, map); + } +} + +void NetworkMaterial::setScatteringMap(const QUrl& url) { + auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + if (map) { + setTextureMap(MapChannel::SCATTERING_MAP, map); + } +} + +void NetworkMaterial::setLightmapMap(const QUrl& url) { + auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + if (map) { + //map->setTextureTransform(_lightmapTransform); + //map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + setTextureMap(MapChannel::LIGHTMAP_MAP, map); + } +} + +NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) : + graphics::Material(*material._material), + _textures(MapChannel::NUM_MAP_CHANNELS) +{ + _name = material.name.toStdString(); + if (!material.albedoTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + if (map) { + _albedoTransform = material.albedoTexture.transform; + map->setTextureTransform(_albedoTransform); + + if (!material.opacityTexture.filename.isEmpty()) { + if (material.albedoTexture.filename == material.opacityTexture.filename) { + // Best case scenario, just indicating that the albedo map contains transparency + // TODO: Different albedo/opacity maps are not currently supported + map->setUseAlphaChannel(true); + } + } + } + + setTextureMap(MapChannel::ALBEDO_MAP, map); + } + + + if (!material.normalTexture.filename.isEmpty()) { + auto type = (material.normalTexture.isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE); + auto map = fetchTextureMap(textureBaseUrl, material.normalTexture, type, MapChannel::NORMAL_MAP); + setTextureMap(MapChannel::NORMAL_MAP, map); + } + + if (!material.roughnessTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } else if (!material.glossTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, image::TextureUsage::GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } + + if (!material.metallicTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } else if (!material.specularTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, image::TextureUsage::SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } + + if (!material.occlusionTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + if (map) { + map->setTextureTransform(material.occlusionTexture.transform); + } + setTextureMap(MapChannel::OCCLUSION_MAP, map); + } + + if (!material.emissiveTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + setTextureMap(MapChannel::EMISSIVE_MAP, map); + } + + if (!material.scatteringTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.scatteringTexture, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + setTextureMap(MapChannel::SCATTERING_MAP, map); + } + + if (!material.lightmapTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + if (map) { + _lightmapTransform = material.lightmapTexture.transform; + _lightmapParams = material.lightmapParams; + map->setTextureTransform(_lightmapTransform); + map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + } + setTextureMap(MapChannel::LIGHTMAP_MAP, map); + } +} + +void NetworkMaterial::setTextures(const QVariantMap& textureMap) { + _isOriginal = false; + + const auto& albedoName = getTextureName(MapChannel::ALBEDO_MAP); + const auto& normalName = getTextureName(MapChannel::NORMAL_MAP); + const auto& roughnessName = getTextureName(MapChannel::ROUGHNESS_MAP); + const auto& metallicName = getTextureName(MapChannel::METALLIC_MAP); + const auto& occlusionName = getTextureName(MapChannel::OCCLUSION_MAP); + const auto& emissiveName = getTextureName(MapChannel::EMISSIVE_MAP); + const auto& lightmapName = getTextureName(MapChannel::LIGHTMAP_MAP); + const auto& scatteringName = getTextureName(MapChannel::SCATTERING_MAP); + + if (!albedoName.isEmpty()) { + auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + if (map) { + map->setTextureTransform(_albedoTransform); + // when reassigning the albedo texture we also check for the alpha channel used as opacity + map->setUseAlphaChannel(true); + } + setTextureMap(MapChannel::ALBEDO_MAP, map); + } + + if (!normalName.isEmpty()) { + auto url = textureMap.contains(normalName) ? textureMap[normalName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); + setTextureMap(MapChannel::NORMAL_MAP, map); + } + + if (!roughnessName.isEmpty()) { + auto url = textureMap.contains(roughnessName) ? textureMap[roughnessName].toUrl() : QUrl(); + // FIXME: If passing a gloss map instead of a roughmap how do we know? + auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } + + if (!metallicName.isEmpty()) { + auto url = textureMap.contains(metallicName) ? textureMap[metallicName].toUrl() : QUrl(); + // FIXME: If passing a specular map instead of a metallic how do we know? + auto map = fetchTextureMap(url, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } + + if (!occlusionName.isEmpty()) { + auto url = textureMap.contains(occlusionName) ? textureMap[occlusionName].toUrl() : QUrl(); + // FIXME: we need to handle the occlusion map transform here + auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + setTextureMap(MapChannel::OCCLUSION_MAP, map); + } + + if (!emissiveName.isEmpty()) { + auto url = textureMap.contains(emissiveName) ? textureMap[emissiveName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + setTextureMap(MapChannel::EMISSIVE_MAP, map); + } + + if (!scatteringName.isEmpty()) { + auto url = textureMap.contains(scatteringName) ? textureMap[scatteringName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + setTextureMap(MapChannel::SCATTERING_MAP, map); + } + + if (!lightmapName.isEmpty()) { + auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + if (map) { + map->setTextureTransform(_lightmapTransform); + map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + } + setTextureMap(MapChannel::LIGHTMAP_MAP, map); + } +} + +bool NetworkMaterial::isMissingTexture() { + for (auto& networkTexture : _textures) { + auto& texture = networkTexture.texture; + if (!texture) { + continue; + } + // Failed texture downloads need to be considered as 'loaded' + // or the object will never fade in + bool finished = texture->isFailed() || (texture->isLoaded() && texture->getGPUTexture() && texture->getGPUTexture()->isDefined()); + if (!finished) { + return true; + } + } + return false; +} + +void NetworkMaterial::checkResetOpacityMap() { + // If material textures are loaded, check the material translucency + // FIXME: This should not be done here. The opacity map should already be reset in Material::setTextureMap. + // However, currently that code can be called before the albedo map is defined, so resetOpacityMap will fail. + // Geometry::areTexturesLoaded() is called repeatedly until it returns true, so we do the check here for now + const auto& albedoTexture = _textures[NetworkMaterial::MapChannel::ALBEDO_MAP]; + if (albedoTexture.texture) { + resetOpacityMap(); + } } \ No newline at end of file diff --git a/libraries/material-networking/src/material-networking/MaterialCache.h b/libraries/material-networking/src/material-networking/MaterialCache.h new file mode 100644 index 0000000000..d327aedb22 --- /dev/null +++ b/libraries/material-networking/src/material-networking/MaterialCache.h @@ -0,0 +1,117 @@ +// +// Created by Sam Gondelman on 2/9/2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_MaterialCache_h +#define hifi_MaterialCache_h + +#include "glm/glm.hpp" + +#include +#include +#include + +#include "TextureCache.h" + +class NetworkMaterial : public graphics::Material { +public: + using MapChannel = graphics::Material::MapChannel; + + NetworkMaterial() : _textures(MapChannel::NUM_MAP_CHANNELS) {} + NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl); + NetworkMaterial(const NetworkMaterial& material); + + void setAlbedoMap(const QUrl& url, bool useAlphaChannel); + void setNormalMap(const QUrl& url, bool isBumpmap); + void setRoughnessMap(const QUrl& url, bool isGloss); + void setMetallicMap(const QUrl& url, bool isSpecular); + void setOcclusionMap(const QUrl& url); + void setEmissiveMap(const QUrl& url); + void setScatteringMap(const QUrl& url); + void setLightmapMap(const QUrl& url); + + bool isMissingTexture(); + void checkResetOpacityMap(); + +protected: + friend class Geometry; + + class Texture { + public: + QString name; + NetworkTexturePointer texture; + }; + using Textures = std::vector; + + Textures _textures; + + static const QString NO_TEXTURE; + const QString& getTextureName(MapChannel channel); + + void setTextures(const QVariantMap& textureMap); + + const bool& isOriginal() const { return _isOriginal; } + +private: + // Helpers for the ctors + QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture); + graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, + image::TextureUsage::Type type, MapChannel channel); + graphics::TextureMapPointer fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel); + + Transform _albedoTransform; + Transform _lightmapTransform; + vec2 _lightmapParams; + + bool _isOriginal { true }; +}; + +class NetworkMaterialResource : public Resource { +public: + NetworkMaterialResource() : Resource() {} + NetworkMaterialResource(const QUrl& url); + + QString getType() const override { return "NetworkMaterial"; } + + virtual void downloadFinished(const QByteArray& data) override; + + typedef struct ParsedMaterials { + uint version { 1 }; + std::vector names; + std::unordered_map> networkMaterials; + + void reset() { + version = 1; + names.clear(); + networkMaterials.clear(); + } + + } ParsedMaterials; + + ParsedMaterials parsedMaterials; + + static ParsedMaterials parseJSONMaterials(const QJsonDocument& materialJSON, const QUrl& baseUrl); + static std::pair> parseJSONMaterial(const QJsonObject& materialJSON, const QUrl& baseUrl); + +private: + static bool parseJSONColor(const QJsonValue& array, glm::vec3& color, bool& isSRGB); +}; + +using NetworkMaterialResourcePointer = QSharedPointer; +using MaterialMapping = std::vector>; + +class MaterialCache : public ResourceCache { +public: + static MaterialCache& instance(); + + NetworkMaterialResourcePointer getMaterial(const QUrl& url); + +protected: + virtual QSharedPointer createResource(const QUrl& url) override; + QSharedPointer createResourceCopy(const QSharedPointer& resource) override; +}; + +#endif diff --git a/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.cpp b/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.cpp new file mode 100644 index 0000000000..9a99c21240 --- /dev/null +++ b/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.cpp @@ -0,0 +1,11 @@ +// +// Created by Sam Gondelman on 2/7/2019 +// Copyright 2019 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 "MaterialNetworkingLogging.h" + +Q_LOGGING_CATEGORY(materialnetworking, "hifi.gpu-material-network") diff --git a/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.h b/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.h new file mode 100644 index 0000000000..a3f220d027 --- /dev/null +++ b/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.h @@ -0,0 +1,11 @@ +// +// Created by Sam Gondelman on 2/7/2019 +// Copyright 2019 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 + +Q_DECLARE_LOGGING_CATEGORY(materialnetworking) diff --git a/libraries/model-networking/src/model-networking/ShaderCache.cpp b/libraries/material-networking/src/material-networking/ShaderCache.cpp similarity index 94% rename from libraries/model-networking/src/model-networking/ShaderCache.cpp rename to libraries/material-networking/src/material-networking/ShaderCache.cpp index 8d060c42f2..4c8d659315 100644 --- a/libraries/model-networking/src/model-networking/ShaderCache.cpp +++ b/libraries/material-networking/src/material-networking/ShaderCache.cpp @@ -29,5 +29,5 @@ QSharedPointer ShaderCache::createResource(const QUrl& url) { } QSharedPointer ShaderCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkShader(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new NetworkShader(*resource.staticCast()), &Resource::deleter); } diff --git a/libraries/model-networking/src/model-networking/ShaderCache.h b/libraries/material-networking/src/material-networking/ShaderCache.h similarity index 100% rename from libraries/model-networking/src/model-networking/ShaderCache.h rename to libraries/material-networking/src/material-networking/ShaderCache.h diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp similarity index 98% rename from libraries/model-networking/src/model-networking/TextureCache.cpp rename to libraries/material-networking/src/material-networking/TextureCache.cpp index a268f4ad0a..ee3c88f02c 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -44,7 +44,7 @@ #include #include "NetworkLogging.h" -#include "ModelNetworkingLogging.h" +#include "MaterialNetworkingLogging.h" #include "NetworkingConstants.h" #include #include @@ -198,16 +198,16 @@ public: namespace std { template <> struct hash { - size_t operator()(const QByteArray& a) const { - return qHash(a); + size_t operator()(const QByteArray& byteArray) const { + return qHash(byteArray); } }; template <> struct hash { - size_t operator()(const TextureExtra& a) const { + size_t operator()(const TextureExtra& textureExtra) const { size_t result = 0; - hash_combine(result, (int)a.type, a.content, a.maxNumPixels, (int)a.sourceChannel); + hash_combine(result, (int)textureExtra.type, textureExtra.content, textureExtra.maxNumPixels, (int)textureExtra.sourceChannel); return result; } }; @@ -329,15 +329,14 @@ QSharedPointer TextureCache::createResource(const QUrl& url) { } QSharedPointer TextureCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkTexture(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new NetworkTexture(*resource.staticCast()), &Resource::deleter); } int networkTexturePointerMetaTypeId = qRegisterMetaType>(); NetworkTexture::NetworkTexture(const QUrl& url, bool resourceTexture) : Resource(url), - Texture(), - _maxNumPixels(100) + Texture() { if (resourceTexture) { _textureSource = std::make_shared(url); @@ -955,7 +954,7 @@ void NetworkTexture::handleFinishedInitialLoad() { cache::FilePointer file; auto& ktxCache = textureCache->_ktxCache; if (!memKtx || !(file = ktxCache->writeFile(data, KTXCache::Metadata(filename, length)))) { - qCWarning(modelnetworking) << url << " failed to write cache file"; + qCWarning(materialnetworking) << url << " failed to write cache file"; QMetaObject::invokeMethod(resource.data(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), @@ -1145,7 +1144,7 @@ void ImageReader::listSupportedImageFormats() { static std::once_flag once; std::call_once(once, []{ auto supportedFormats = QImageReader::supportedImageFormats(); - qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); + qCDebug(materialnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); }); } @@ -1194,7 +1193,7 @@ void ImageReader::read() { if (texture) { texture = textureCache->cacheTextureByHash(hash, texture); } else { - qCWarning(modelnetworking) << "Invalid cached KTX " << _url << " under hash " << hash.c_str() << ", recreating..."; + qCWarning(materialnetworking) << "Invalid cached KTX " << _url << " under hash " << hash.c_str() << ", recreating..."; } } } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/material-networking/src/material-networking/TextureCache.h similarity index 100% rename from libraries/model-networking/src/model-networking/TextureCache.h rename to libraries/material-networking/src/material-networking/TextureCache.h diff --git a/libraries/model-networking/src/model-networking/TextureCacheScriptingInterface.cpp b/libraries/material-networking/src/material-networking/TextureCacheScriptingInterface.cpp similarity index 100% rename from libraries/model-networking/src/model-networking/TextureCacheScriptingInterface.cpp rename to libraries/material-networking/src/material-networking/TextureCacheScriptingInterface.cpp diff --git a/libraries/model-networking/src/model-networking/TextureCacheScriptingInterface.h b/libraries/material-networking/src/material-networking/TextureCacheScriptingInterface.h similarity index 100% rename from libraries/model-networking/src/model-networking/TextureCacheScriptingInterface.h rename to libraries/material-networking/src/material-networking/TextureCacheScriptingInterface.h diff --git a/libraries/model-baker/CMakeLists.txt b/libraries/model-baker/CMakeLists.txt index aabd6eba3a..22c240b487 100644 --- a/libraries/model-baker/CMakeLists.txt +++ b/libraries/model-baker/CMakeLists.txt @@ -1,6 +1,7 @@ set(TARGET_NAME model-baker) setup_hifi_library() -link_hifi_libraries(shared task gpu graphics hfm) - +link_hifi_libraries(shared shaders task gpu graphics hfm material-networking) +include_hifi_library_headers(networking) include_hifi_library_headers(image) +include_hifi_library_headers(ktx) \ No newline at end of file diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 1c2a2f5c63..dfb18eef86 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -101,7 +101,7 @@ namespace baker { class BuildModelTask { public: - using Input = VaryingSet5, std::vector, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; + using Input = VaryingSet5, std::vector, QMap, QHash>; using Output = hfm::Model::Pointer; using JobModel = Job::ModelIO; @@ -118,9 +118,9 @@ namespace baker { class BakerEngineBuilder { public: using Input = VaryingSet2; - using Output = hfm::Model::Pointer; + using Output = VaryingSet2; using JobModel = Task::ModelIO; - void build(JobModel& model, const Varying& input, Varying& hfmModelOut) { + void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); const auto& mapping = input.getN(1); @@ -154,13 +154,18 @@ namespace baker { const auto jointRotationOffsets = jointInfoOut.getN(1); const auto jointIndices = jointInfoOut.getN(2); + // Parse material mapping + const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); + // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); const auto blendshapesPerMeshOut = model.addJob("BuildBlendshapes", buildBlendshapesInputs); const auto buildMeshesInputs = BuildMeshesTask::Input(meshesIn, graphicsMeshes, normalsPerMesh, tangentsPerMesh, blendshapesPerMeshOut).asVarying(); const auto meshesOut = model.addJob("BuildMeshes", buildMeshesInputs); const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices).asVarying(); - hfmModelOut = model.addJob("BuildModel", buildModelInputs); + const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs); + + output = Output(hfmModelOut, materialMapping); } }; @@ -172,7 +177,8 @@ namespace baker { void Baker::run() { _engine->run(); - hfmModel = _engine->getOutput().get(); + hfmModel = _engine->getOutput().get().get0(); + materialMapping = _engine->getOutput().get().get1(); } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 41989d73df..542be0b559 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -18,6 +18,8 @@ #include "Engine.h" +#include "ParseMaterialMappingTask.h" + namespace baker { class Baker { public: @@ -27,6 +29,7 @@ namespace baker { // Outputs, available after run() is called hfm::Model::Pointer hfmModel; + MaterialMapping materialMapping; protected: EnginePointer _engine; diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp new file mode 100644 index 0000000000..7a923a3702 --- /dev/null +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -0,0 +1,76 @@ +// +// Created by Sam Gondelman on 2/7/2019 +// Copyright 2019 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 "ParseMaterialMappingTask.h" + +#include "ModelBakerLogging.h" + +void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& mapping, Output& output) { + MaterialMapping materialMapping; + + auto mappingIter = mapping.find("materialMap"); + if (mappingIter != mapping.end()) { + QByteArray materialMapValue = mappingIter.value().toByteArray(); + QJsonObject materialMap = QJsonDocument::fromJson(materialMapValue).object(); + if (materialMap.isEmpty()) { + qCDebug(model_baker) << "Material Map found but did not produce valid JSON:" << materialMapValue; + } else { + auto mappingKeys = materialMap.keys(); + for (auto mapping : mappingKeys) { + auto mappingJSON = materialMap[mapping]; + if (mappingJSON.isObject()) { + auto mappingValue = mappingJSON.toObject(); + + // Old subsurface scattering mapping + { + auto scatteringIter = mappingValue.find("scattering"); + auto scatteringMapIter = mappingValue.find("scatteringMap"); + if (scatteringIter != mappingValue.end() || scatteringMapIter != mappingValue.end()) { + std::shared_ptr material = std::make_shared(); + + if (scatteringIter != mappingValue.end()) { + float scattering = (float)scatteringIter.value().toDouble(); + material->setScattering(scattering); + } + + if (scatteringMapIter != mappingValue.end()) { + QString scatteringMap = scatteringMapIter.value().toString(); + material->setScatteringMap(scatteringMap); + } + + material->setDefaultFallthrough(true); + + NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); }); + materialResource->moveToThread(qApp->thread()); + materialResource->parsedMaterials.names.push_back("scattering"); + materialResource->parsedMaterials.networkMaterials["scattering"] = material; + + materialMapping.push_back(std::pair("mat::" + mapping.toStdString(), materialResource)); + continue; + } + } + + // Material JSON description + { + NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); }); + materialResource->moveToThread(qApp->thread()); + // TODO: add baseURL to allow FSTs to reference relative files next to them + materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), QUrl()); + materialMapping.push_back(std::pair(mapping.toStdString(), materialResource)); + } + + } else if (mappingJSON.isString()) { + auto mappingValue = mappingJSON.toString(); + materialMapping.push_back(std::pair(mapping.toStdString(), MaterialCache::instance().getMaterial(mappingValue))); + } + } + } + } + + output = materialMapping; +} diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h new file mode 100644 index 0000000000..69e00b0324 --- /dev/null +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h @@ -0,0 +1,29 @@ +// +// Created by Sam Gondelman on 2/7/2019 +// Copyright 2019 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_ParseMaterialMappingTask_h +#define hifi_ParseMaterialMappingTask_h + +#include + +#include + +#include "Engine.h" + +#include + +class ParseMaterialMappingTask { +public: + using Input = QVariantHash; + using Output = MaterialMapping; + using JobModel = baker::Job::ModelIO; + + void run(const baker::BakeContextPointer& context, const Input& input, Output& output); +}; + +#endif // hifi_ParseMaterialMappingTask_h \ No newline at end of file diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index 6a7182cc33..e79d18f779 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,6 +1,8 @@ set(TARGET_NAME model-networking) setup_hifi_library() -link_hifi_libraries(shared shaders networking graphics fbx ktx image gl model-baker) -include_hifi_library_headers(gpu) +link_hifi_libraries(shared shaders networking graphics fbx material-networking model-baker) include_hifi_library_headers(hfm) include_hifi_library_headers(task) +include_hifi_library_headers(gpu) +include_hifi_library_headers(image) +include_hifi_library_headers(ktx) \ No newline at end of file diff --git a/libraries/model-networking/src/model-networking/MaterialCache.h b/libraries/model-networking/src/model-networking/MaterialCache.h deleted file mode 100644 index 6abadfc030..0000000000 --- a/libraries/model-networking/src/model-networking/MaterialCache.h +++ /dev/null @@ -1,60 +0,0 @@ -// -// Created by Sam Gondelman on 2/9/2018 -// Copyright 2018 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_MaterialCache_h -#define hifi_MaterialCache_h - -#include - -#include "glm/glm.hpp" - -#include "ModelCache.h" - -class NetworkMaterialResource : public Resource { -public: - NetworkMaterialResource(const QUrl& url); - - QString getType() const override { return "NetworkMaterial"; } - - virtual void downloadFinished(const QByteArray& data) override; - - typedef struct ParsedMaterials { - uint version { 1 }; - std::vector names; - std::unordered_map> networkMaterials; - - void reset() { - version = 1; - names.clear(); - networkMaterials.clear(); - } - - } ParsedMaterials; - - ParsedMaterials parsedMaterials; - - static ParsedMaterials parseJSONMaterials(const QJsonDocument& materialJSON, const QUrl& baseUrl); - static std::pair> parseJSONMaterial(const QJsonObject& materialJSON, const QUrl& baseUrl); - -private: - static bool parseJSONColor(const QJsonValue& array, glm::vec3& color, bool& isSRGB); -}; - -using NetworkMaterialResourcePointer = QSharedPointer; - -class MaterialCache : public ResourceCache { -public: - static MaterialCache& instance(); - - NetworkMaterialResourcePointer getMaterial(const QUrl& url); - -protected: - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; -}; - -#endif diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 90925d17c3..581196b2cc 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -76,9 +76,9 @@ namespace std { template <> struct hash { - size_t operator()(const GeometryExtra& a) const { + size_t operator()(const GeometryExtra& geometryExtra) const { size_t result = 0; - hash_combine(result, a.mapping, a.textureBaseUrl, a.combineParts); + hash_combine(result, geometryExtra.mapping, geometryExtra.textureBaseUrl, geometryExtra.combineParts); return result; } }; @@ -174,6 +174,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { void GeometryMappingResource::onGeometryMappingLoaded(bool success) { if (success && _geometryResource) { _hfmModel = _geometryResource->_hfmModel; + _materialMapping = _geometryResource->_materialMapping; _meshParts = _geometryResource->_meshParts; _meshes = _geometryResource->_meshes; _materials = _geometryResource->_materials; @@ -341,6 +342,7 @@ void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmMode // Assume ownership of the processed HFMModel _hfmModel = modelBaker.hfmModel; + _materialMapping = modelBaker.materialMapping; // Copy materials QHash materialIDAtlas; @@ -392,7 +394,7 @@ QSharedPointer ModelCache::createResource(const QUrl& url) { } QSharedPointer ModelCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast()), &Resource::deleter); } GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url, @@ -437,6 +439,7 @@ const QVariantMap Geometry::getTextures() const { // FIXME: The materials should only be copied when modified, but the Model currently caches the original Geometry::Geometry(const Geometry& geometry) { _hfmModel = geometry._hfmModel; + _materialMapping = geometry._materialMapping; _meshes = geometry._meshes; _meshParts = geometry._meshParts; @@ -556,310 +559,4 @@ void GeometryResourceWatcher::resourceRefreshed() { // _instance.reset(); } -NetworkMaterial::NetworkMaterial(const NetworkMaterial& m) : - Material(m), - _textures(m._textures), - _albedoTransform(m._albedoTransform), - _lightmapTransform(m._lightmapTransform), - _lightmapParams(m._lightmapParams), - _isOriginal(m._isOriginal) -{} - -const QString NetworkMaterial::NO_TEXTURE = QString(); - -const QString& NetworkMaterial::getTextureName(MapChannel channel) { - if (_textures[channel].texture) { - return _textures[channel].name; - } - return NO_TEXTURE; -} - -QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const HFMTexture& texture) { - if (texture.content.isEmpty()) { - // External file: search relative to the baseUrl, in case filename is relative - return baseUrl.resolved(QUrl(texture.filename)); - } else { - // Inlined file: cache under the fbx file to avoid namespace clashes - // NOTE: We cannot resolve the path because filename may be an absolute path - assert(texture.filename.size() > 0); - auto baseUrlStripped = baseUrl.toDisplayString(QUrl::RemoveFragment | QUrl::RemoveQuery | QUrl::RemoveUserInfo); - if (texture.filename.at(0) == '/') { - return baseUrlStripped + texture.filename; - } else { - return baseUrlStripped + '/' + texture.filename; - } - } -} - -graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, - image::TextureUsage::Type type, MapChannel channel) { - - if (baseUrl.isEmpty()) { - return nullptr; - } - - const auto url = getTextureUrl(baseUrl, hfmTexture); - const auto texture = DependencyManager::get()->getTexture(url, type, hfmTexture.content, hfmTexture.maxNumPixels, hfmTexture.sourceChannel); - _textures[channel] = Texture { hfmTexture.name, texture }; - - auto map = std::make_shared(); - if (texture) { - map->setTextureSource(texture->_textureSource); - } - map->setTextureTransform(hfmTexture.transform); - - return map; -} - -graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel) { - auto textureCache = DependencyManager::get(); - if (textureCache && !url.isEmpty()) { - auto texture = textureCache->getTexture(url, type); - _textures[channel].texture = texture; - - auto map = std::make_shared(); - if (texture) { - map->setTextureSource(texture->_textureSource); - } - - return map; - } - return nullptr; -} - -void NetworkMaterial::setAlbedoMap(const QUrl& url, bool useAlphaChannel) { - auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); - if (map) { - map->setUseAlphaChannel(useAlphaChannel); - setTextureMap(MapChannel::ALBEDO_MAP, map); - } -} - -void NetworkMaterial::setNormalMap(const QUrl& url, bool isBumpmap) { - auto map = fetchTextureMap(url, isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); - if (map) { - setTextureMap(MapChannel::NORMAL_MAP, map); - } -} - -void NetworkMaterial::setRoughnessMap(const QUrl& url, bool isGloss) { - auto map = fetchTextureMap(url, isGloss ? image::TextureUsage::GLOSS_TEXTURE : image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); - if (map) { - setTextureMap(MapChannel::ROUGHNESS_MAP, map); - } -} - -void NetworkMaterial::setMetallicMap(const QUrl& url, bool isSpecular) { - auto map = fetchTextureMap(url, isSpecular ? image::TextureUsage::SPECULAR_TEXTURE : image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); - if (map) { - setTextureMap(MapChannel::METALLIC_MAP, map); - } -} - -void NetworkMaterial::setOcclusionMap(const QUrl& url) { - auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); - if (map) { - setTextureMap(MapChannel::OCCLUSION_MAP, map); - } -} - -void NetworkMaterial::setEmissiveMap(const QUrl& url) { - auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); - if (map) { - setTextureMap(MapChannel::EMISSIVE_MAP, map); - } -} - -void NetworkMaterial::setScatteringMap(const QUrl& url) { - auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); - if (map) { - setTextureMap(MapChannel::SCATTERING_MAP, map); - } -} - -void NetworkMaterial::setLightmapMap(const QUrl& url) { - auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); - if (map) { - //map->setTextureTransform(_lightmapTransform); - //map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); - setTextureMap(MapChannel::LIGHTMAP_MAP, map); - } -} - -NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) : - graphics::Material(*material._material), - _textures(MapChannel::NUM_MAP_CHANNELS) -{ - _name = material.name.toStdString(); - if (!material.albedoTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); - if (map) { - _albedoTransform = material.albedoTexture.transform; - map->setTextureTransform(_albedoTransform); - - if (!material.opacityTexture.filename.isEmpty()) { - if (material.albedoTexture.filename == material.opacityTexture.filename) { - // Best case scenario, just indicating that the albedo map contains transparency - // TODO: Different albedo/opacity maps are not currently supported - map->setUseAlphaChannel(true); - } - } - } - - setTextureMap(MapChannel::ALBEDO_MAP, map); - } - - - if (!material.normalTexture.filename.isEmpty()) { - auto type = (material.normalTexture.isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE); - auto map = fetchTextureMap(textureBaseUrl, material.normalTexture, type, MapChannel::NORMAL_MAP); - setTextureMap(MapChannel::NORMAL_MAP, map); - } - - if (!material.roughnessTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); - setTextureMap(MapChannel::ROUGHNESS_MAP, map); - } else if (!material.glossTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, image::TextureUsage::GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); - setTextureMap(MapChannel::ROUGHNESS_MAP, map); - } - - if (!material.metallicTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); - setTextureMap(MapChannel::METALLIC_MAP, map); - } else if (!material.specularTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, image::TextureUsage::SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); - setTextureMap(MapChannel::METALLIC_MAP, map); - } - - if (!material.occlusionTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); - if (map) { - map->setTextureTransform(material.occlusionTexture.transform); - } - setTextureMap(MapChannel::OCCLUSION_MAP, map); - } - - if (!material.emissiveTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); - setTextureMap(MapChannel::EMISSIVE_MAP, map); - } - - if (!material.scatteringTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.scatteringTexture, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); - setTextureMap(MapChannel::SCATTERING_MAP, map); - } - - if (!material.lightmapTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); - if (map) { - _lightmapTransform = material.lightmapTexture.transform; - _lightmapParams = material.lightmapParams; - map->setTextureTransform(_lightmapTransform); - map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); - } - setTextureMap(MapChannel::LIGHTMAP_MAP, map); - } -} - -void NetworkMaterial::setTextures(const QVariantMap& textureMap) { - _isOriginal = false; - - const auto& albedoName = getTextureName(MapChannel::ALBEDO_MAP); - const auto& normalName = getTextureName(MapChannel::NORMAL_MAP); - const auto& roughnessName = getTextureName(MapChannel::ROUGHNESS_MAP); - const auto& metallicName = getTextureName(MapChannel::METALLIC_MAP); - const auto& occlusionName = getTextureName(MapChannel::OCCLUSION_MAP); - const auto& emissiveName = getTextureName(MapChannel::EMISSIVE_MAP); - const auto& lightmapName = getTextureName(MapChannel::LIGHTMAP_MAP); - const auto& scatteringName = getTextureName(MapChannel::SCATTERING_MAP); - - if (!albedoName.isEmpty()) { - auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); - if (map) { - map->setTextureTransform(_albedoTransform); - // when reassigning the albedo texture we also check for the alpha channel used as opacity - map->setUseAlphaChannel(true); - } - setTextureMap(MapChannel::ALBEDO_MAP, map); - } - - if (!normalName.isEmpty()) { - auto url = textureMap.contains(normalName) ? textureMap[normalName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); - setTextureMap(MapChannel::NORMAL_MAP, map); - } - - if (!roughnessName.isEmpty()) { - auto url = textureMap.contains(roughnessName) ? textureMap[roughnessName].toUrl() : QUrl(); - // FIXME: If passing a gloss map instead of a roughmap how do we know? - auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); - setTextureMap(MapChannel::ROUGHNESS_MAP, map); - } - - if (!metallicName.isEmpty()) { - auto url = textureMap.contains(metallicName) ? textureMap[metallicName].toUrl() : QUrl(); - // FIXME: If passing a specular map instead of a metallic how do we know? - auto map = fetchTextureMap(url, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); - setTextureMap(MapChannel::METALLIC_MAP, map); - } - - if (!occlusionName.isEmpty()) { - auto url = textureMap.contains(occlusionName) ? textureMap[occlusionName].toUrl() : QUrl(); - // FIXME: we need to handle the occlusion map transform here - auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); - setTextureMap(MapChannel::OCCLUSION_MAP, map); - } - - if (!emissiveName.isEmpty()) { - auto url = textureMap.contains(emissiveName) ? textureMap[emissiveName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); - setTextureMap(MapChannel::EMISSIVE_MAP, map); - } - - if (!scatteringName.isEmpty()) { - auto url = textureMap.contains(scatteringName) ? textureMap[scatteringName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); - setTextureMap(MapChannel::SCATTERING_MAP, map); - } - - if (!lightmapName.isEmpty()) { - auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); - if (map) { - map->setTextureTransform(_lightmapTransform); - map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); - } - setTextureMap(MapChannel::LIGHTMAP_MAP, map); - } -} - -bool NetworkMaterial::isMissingTexture() { - for (auto& networkTexture : _textures) { - auto& texture = networkTexture.texture; - if (!texture) { - continue; - } - // Failed texture downloads need to be considered as 'loaded' - // or the object will never fade in - bool finished = texture->isFailed() || (texture->isLoaded() && texture->getGPUTexture() && texture->getGPUTexture()->isDefined()); - if (!finished) { - return true; - } - } - return false; -} - -void NetworkMaterial::checkResetOpacityMap() { - // If material textures are loaded, check the material translucency - // FIXME: This should not be done here. The opacity map should already be reset in Material::setTextureMap. - // However, currently that code can be called before the albedo map is defined, so resetOpacityMap will fail. - // Geometry::areTexturesLoaded() is called repeatedly until it returns true, so we do the check here for now - const auto& albedoTexture = _textures[NetworkMaterial::MapChannel::ALBEDO_MAP]; - if (albedoTexture.texture) { - resetOpacityMap(); - } -} - #include "ModelCache.moc" diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 497cae86a3..4cd7048dca 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -15,17 +15,13 @@ #include #include -#include #include #include "FBXSerializer.h" -#include "TextureCache.h" +#include +#include #include "ModelLoader.h" -// Alias instead of derive to avoid copying - -class NetworkTexture; -class NetworkMaterial; class MeshPart; class GeometryMappingResource; @@ -49,6 +45,7 @@ public: bool isHFMModelLoaded() const { return (bool)_hfmModel; } const HFMModel& getHFMModel() const { return *_hfmModel; } + const MaterialMapping& getMaterialMapping() const { return _materialMapping; } const GeometryMeshes& getMeshes() const { return *_meshes; } const std::shared_ptr getShapeMaterial(int shapeID) const; @@ -64,6 +61,7 @@ protected: // Shared across all geometries, constant throughout lifetime std::shared_ptr _hfmModel; + MaterialMapping _materialMapping; std::shared_ptr _meshes; std::shared_ptr _meshParts; @@ -166,59 +164,6 @@ private: ModelLoader _modelLoader; }; -class NetworkMaterial : public graphics::Material { -public: - using MapChannel = graphics::Material::MapChannel; - - NetworkMaterial() : _textures(MapChannel::NUM_MAP_CHANNELS) {} - NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl); - NetworkMaterial(const NetworkMaterial& material); - - void setAlbedoMap(const QUrl& url, bool useAlphaChannel); - void setNormalMap(const QUrl& url, bool isBumpmap); - void setRoughnessMap(const QUrl& url, bool isGloss); - void setMetallicMap(const QUrl& url, bool isSpecular); - void setOcclusionMap(const QUrl& url); - void setEmissiveMap(const QUrl& url); - void setScatteringMap(const QUrl& url); - void setLightmapMap(const QUrl& url); - - bool isMissingTexture(); - void checkResetOpacityMap(); - -protected: - friend class Geometry; - - class Texture { - public: - QString name; - NetworkTexturePointer texture; - }; - using Textures = std::vector; - - Textures _textures; - - static const QString NO_TEXTURE; - const QString& getTextureName(MapChannel channel); - - void setTextures(const QVariantMap& textureMap); - - const bool& isOriginal() const { return _isOriginal; } - -private: - // Helpers for the ctors - QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture); - graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, - image::TextureUsage::Type type, MapChannel channel); - graphics::TextureMapPointer fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel); - - Transform _albedoTransform; - Transform _lightmapTransform; - vec2 _lightmapParams; - - bool _isOriginal { true }; -}; - class MeshPart { public: MeshPart(int mesh, int part, int material) : meshID { mesh }, partID { part }, materialID { material } {} diff --git a/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp b/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp index 0c44fa33eb..d76efec31a 100644 --- a/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp +++ b/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp @@ -8,4 +8,4 @@ #include "ModelNetworkingLogging.h" -Q_LOGGING_CATEGORY(modelnetworking, "hifi.gpu-network") +Q_LOGGING_CATEGORY(modelnetworking, "hifi.gpu-model-network") diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index f3967d8ffc..2096213273 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -360,13 +360,13 @@ class Resource : public QObject { Q_OBJECT public: - + Resource() : QObject(), _loaded(true) {} Resource(const Resource& other); Resource(const QUrl& url); virtual ~Resource(); virtual QString getType() const { return "Resource"; } - + /// Returns the key last used to identify this resource in the unused map. int getLRUKey() const { return _lruKey; } @@ -375,13 +375,13 @@ public: /// Sets the load priority for one owner. virtual void setLoadPriority(const QPointer& owner, float priority); - + /// Sets a set of priorities at once. virtual void setLoadPriorities(const QHash, float>& priorities); - + /// Clears the load priority for one owner. virtual void clearLoadPriority(const QPointer& owner); - + /// Returns the highest load priority across all owners. float getLoadPriority(); @@ -491,14 +491,14 @@ protected: QWeakPointer _self; QPointer _cache; - qint64 _bytesReceived{ 0 }; - qint64 _bytesTotal{ 0 }; - qint64 _bytes{ 0 }; + qint64 _bytesReceived { 0 }; + qint64 _bytesTotal { 0 }; + qint64 _bytes { 0 }; int _requestID; - ResourceRequest* _request{ nullptr }; + ResourceRequest* _request { nullptr }; - size_t _extraHash; + size_t _extraHash { std::numeric_limits::max() }; public slots: void handleDownloadProgress(uint64_t bytesReceived, uint64_t bytesTotal); diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h index 4a0a21e995..a98989655e 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h @@ -57,6 +57,7 @@ protected: void internalPresent() override; void hmdPresent() override { throw std::runtime_error("Unused"); } bool isHmdMounted() const override; + bool alwaysPresent() const override { return true; } static const char* NAME; mutable gl::Context* _mainContext{ nullptr }; diff --git a/libraries/physics/CMakeLists.txt b/libraries/physics/CMakeLists.txt index 5249ed2950..d7ce40641d 100644 --- a/libraries/physics/CMakeLists.txt +++ b/libraries/physics/CMakeLists.txt @@ -7,6 +7,7 @@ include_hifi_library_headers(avatars) include_hifi_library_headers(audio) include_hifi_library_headers(octree) include_hifi_library_headers(animation) +include_hifi_library_headers(material-networking) include_hifi_library_headers(model-networking) include_hifi_library_headers(image) include_hifi_library_headers(ktx) diff --git a/libraries/procedural/CMakeLists.txt b/libraries/procedural/CMakeLists.txt index f3c3be687a..d08a7aef02 100644 --- a/libraries/procedural/CMakeLists.txt +++ b/libraries/procedural/CMakeLists.txt @@ -1,3 +1,4 @@ set(TARGET_NAME procedural) setup_hifi_library() -link_hifi_libraries(shared gpu shaders networking graphics model-networking ktx image) + +link_hifi_libraries(shared gpu shaders networking graphics material-networking ktx image) diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 3e10678ba7..8477e69afc 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -19,8 +19,8 @@ #include #include #include -#include -#include +#include +#include using UniformLambdas = std::list>; const size_t MAX_PROCEDURAL_TEXTURE_CHANNELS{ 4 }; diff --git a/libraries/recording/src/recording/ClipCache.cpp b/libraries/recording/src/recording/ClipCache.cpp index c08dd40ad8..bc20e4d8eb 100644 --- a/libraries/recording/src/recording/ClipCache.cpp +++ b/libraries/recording/src/recording/ClipCache.cpp @@ -54,5 +54,5 @@ QSharedPointer ClipCache::createResource(const QUrl& url) { } QSharedPointer ClipCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkClipLoader(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new NetworkClipLoader(*resource.staticCast()), &Resource::deleter); } \ No newline at end of file diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 2b092bff2a..833d78bb74 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ set(TARGET_NAME render-utils) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Gui Network Qml Quick Script) -link_hifi_libraries(shared task ktx gpu shaders graphics graphics-scripting model-networking render animation fbx image procedural) +link_hifi_libraries(shared task ktx gpu shaders graphics graphics-scripting material-networking model-networking render animation fbx image procedural) include_hifi_library_headers(audio) include_hifi_library_headers(networking) include_hifi_library_headers(octree) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 0206bd6963..b9b294d0e3 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -756,7 +756,16 @@ scriptable::ScriptableModelBase Model::getScriptableModel() { int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - result.appendMaterial(graphics::MaterialLayer(getGeometry()->getShapeMaterial(shapeID), 0), shapeID, _modelMeshMaterialNames[shapeID]); + auto& materialName = _modelMeshMaterialNames[shapeID]; + result.appendMaterial(graphics::MaterialLayer(getGeometry()->getShapeMaterial(shapeID), 0), shapeID, materialName); + + auto mappedMaterialIter = _materialMapping.find(shapeID); + if (mappedMaterialIter != _materialMapping.end()) { + auto mappedMaterials = mappedMaterialIter->second; + for (auto& mappedMaterial : mappedMaterials) { + result.appendMaterial(mappedMaterial, shapeID, materialName); + } + } shapeID++; } } @@ -956,6 +965,7 @@ bool Model::addToScene(const render::ScenePointer& scene, } if (somethingAdded) { + applyMaterialMapping(); _addedToScene = true; updateRenderItems(); _needsFixupInScene = false; @@ -973,6 +983,7 @@ void Model::removeFromScene(const render::ScenePointer& scene, render::Transacti _modelMeshRenderItems.clear(); _modelMeshMaterialNames.clear(); _modelMeshRenderItemShapes.clear(); + _priorityMap.clear(); _blendshapeOffsets.clear(); _blendshapeOffsetsInitialized = false; @@ -1519,17 +1530,65 @@ std::set Model::getMeshIDsFromMaterialID(QString parentMaterialNam return toReturn; } +void Model::applyMaterialMapping() { + auto renderItemsKey = _renderItemKeyGlobalFlags; + PrimitiveMode primitiveMode = getPrimitiveMode(); + bool useDualQuaternionSkinning = _useDualQuaternionSkinning; + + auto& materialMapping = getMaterialMapping(); + for (auto& mapping : materialMapping) { + std::set shapeIDs = getMeshIDsFromMaterialID(QString(mapping.first.c_str())); + auto networkMaterialResource = mapping.second; + if (!networkMaterialResource || shapeIDs.size() == 0) { + continue; + } + + auto materialLoaded = [this, networkMaterialResource, shapeIDs, renderItemsKey, primitiveMode, useDualQuaternionSkinning]() { + if (networkMaterialResource->isFailed() || networkMaterialResource->parsedMaterials.names.size() == 0) { + return; + } + render::Transaction transaction; + auto networkMaterial = networkMaterialResource->parsedMaterials.networkMaterials[networkMaterialResource->parsedMaterials.names[0]]; + for (auto shapeID : shapeIDs) { + if (shapeID < _modelMeshRenderItemIDs.size()) { + auto itemID = _modelMeshRenderItemIDs[shapeID]; + auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; + bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); + graphics::MaterialLayer material = graphics::MaterialLayer(networkMaterial, ++_priorityMap[shapeID]); + _materialMapping[shapeID].push_back(material); + transaction.updateItem(itemID, [material, renderItemsKey, + invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { + data.addMaterial(material); + // if the material changed, we might need to update our item key or shape key + data.updateKey(renderItemsKey); + data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning); + }); + } + } + AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); + }; + + if (networkMaterialResource->isLoaded()) { + materialLoaded(); + } else { + connect(networkMaterialResource.data(), &Resource::finished, materialLoaded); + } + } +} + void Model::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) { std::set shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str())); + + auto renderItemsKey = _renderItemKeyGlobalFlags; + PrimitiveMode primitiveMode = getPrimitiveMode(); + bool useDualQuaternionSkinning = _useDualQuaternionSkinning; + render::Transaction transaction; for (auto shapeID : shapeIDs) { if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; - auto renderItemsKey = _renderItemKeyGlobalFlags; - PrimitiveMode primitiveMode = getPrimitiveMode(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); - bool useDualQuaternionSkinning = _useDualQuaternionSkinning; transaction.updateItem(itemID, [material, renderItemsKey, invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.addMaterial(material); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index aadfca78ba..d1d4a7d3d1 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -183,6 +183,7 @@ public: /// Provided as a convenience, will crash if !isLoaded() // And so that getHFMModel() isn't chained everywhere const HFMModel& getHFMModel() const { assert(isLoaded()); return _renderGeometry->getHFMModel(); } + const MaterialMapping& getMaterialMapping() const { assert(isLoaded()); return _renderGeometry->getMaterialMapping(); } bool isActive() const { return isLoaded(); } @@ -373,6 +374,10 @@ signals: protected: + std::unordered_map _priorityMap; // only used for materialMapping + std::unordered_map> _materialMapping; // generated during applyMaterialMapping + void applyMaterialMapping(); + void setBlendshapeCoefficients(const QVector& coefficients) { _blendshapeCoefficients = coefficients; } const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index 64037d64b6..385e384efe 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -101,7 +101,7 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs& } } -void CompositeHUD::run(const RenderContextPointer& renderContext) { +void CompositeHUD::run(const RenderContextPointer& renderContext, const gpu::FramebufferPointer& inputs) { assert(renderContext->args); assert(renderContext->args->_context); @@ -119,6 +119,9 @@ void CompositeHUD::run(const RenderContextPointer& renderContext) { renderContext->args->getViewFrustum().evalViewTransform(viewMat); batch.setProjectionTransform(projMat); batch.setViewTransform(viewMat, true); + if (inputs) { + batch.setFramebuffer(inputs); + } if (renderContext->args->_hudOperator) { renderContext->args->_hudOperator(batch, renderContext->args->_hudTexture, renderContext->args->_renderMode == RenderArgs::RenderMode::MIRROR_RENDER_MODE); } @@ -197,7 +200,73 @@ void Blit::run(const RenderContextPointer& renderContext, const gpu::Framebuffer }); } -void ExtractFrustums::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& output) { + +void ResolveFramebuffer::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { + RenderArgs* args = renderContext->args; + auto srcFbo = inputs.get0(); + auto destFbo = inputs.get1(); + + if (!destFbo) { + destFbo = args->_blitFramebuffer; + } + outputs = destFbo; + + // Check valid src and dest + if (!srcFbo || !destFbo) { + return; + } + + // Check valid size for sr and dest + auto frameSize(srcFbo->getSize()); + if (destFbo->getSize() != frameSize) { + return; + } + + gpu::Vec4i rectSrc; + rectSrc.z = frameSize.x; + rectSrc.w = frameSize.y; + gpu::doInBatch("Resolve", args->_context, [&](gpu::Batch& batch) { + batch.blit(srcFbo, rectSrc, destFbo, rectSrc); + }); +} + +void ResolveNewFramebuffer::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { + RenderArgs* args = renderContext->args; + auto srcFbo = inputs; + outputs.reset(); + + // Check valid src + if (!srcFbo) { + return; + } + + // Check valid size for sr and dest + auto frameSize(srcFbo->getSize()); + + // Resizing framebuffers instead of re-building them seems to cause issues with threaded rendering + if (_outputFramebuffer && _outputFramebuffer->getSize() != frameSize) { + _outputFramebuffer.reset(); + } + + if (!_outputFramebuffer) { + _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("resolvedNew.out")); + auto colorFormat = gpu::Element::COLOR_SRGBA_32; + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); + auto colorTexture = gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + _outputFramebuffer->setRenderBuffer(0, colorTexture); + } + + gpu::Vec4i rectSrc; + rectSrc.z = frameSize.x; + rectSrc.w = frameSize.y; + gpu::doInBatch("ResolveNew", args->_context, [&](gpu::Batch& batch) { batch.blit(srcFbo, rectSrc, _outputFramebuffer, rectSrc); }); + + outputs = _outputFramebuffer; +} + + + + void ExtractFrustums::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& output) { assert(renderContext->args); assert(renderContext->args->_context); diff --git a/libraries/render-utils/src/RenderCommonTask.h b/libraries/render-utils/src/RenderCommonTask.h index 4f8f53257d..b43a10aa7b 100644 --- a/libraries/render-utils/src/RenderCommonTask.h +++ b/libraries/render-utils/src/RenderCommonTask.h @@ -77,10 +77,12 @@ protected: class CompositeHUD { public: - using JobModel = render::Job::Model; + // IF specified the input Framebuffer is actively set by the batch of this job before calling the HUDOperator. + // If not, the current Framebuffer is left unchanged. + //using Inputs = gpu::FramebufferPointer; + using JobModel = render::Job::ModelI; - CompositeHUD() {} - void run(const render::RenderContextPointer& renderContext); + void run(const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& inputs); }; class Blit { @@ -90,6 +92,28 @@ public: void run(const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer); }; + +class ResolveFramebuffer { +public: + using Inputs = render::VaryingSet2; + using Outputs = gpu::FramebufferPointer; + using JobModel = render::Job::ModelIO; + + void run(const render::RenderContextPointer& renderContext, const Inputs& source, Outputs& dest); +}; + +class ResolveNewFramebuffer { +public: + using Inputs = gpu::FramebufferPointer; + using Outputs = gpu::FramebufferPointer; + using JobModel = render::Job::ModelIO; + + void run(const render::RenderContextPointer& renderContext, const Inputs& source, Outputs& dest); +private: + gpu::FramebufferPointer _outputFramebuffer; +}; + + class ExtractFrustums { public: diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 673a165105..089d267711 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -257,8 +257,8 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren // Upscale to finale resolution const auto primaryFramebuffer = task.addJob("PrimaryBufferUpscale", scaledPrimaryFramebuffer); - // Composite the HUD and HUD layered objects - task.addJob("HUD"); + // Composite the HUD and HUD overlays + task.addJob("HUD", primaryFramebuffer); const auto nullJitter = Varying(glm::vec2(0.0f, 0.0f)); const auto hudOpaquesInputs = DrawLayered3D::Inputs(hudOpaque, lightingModel, nullJitter).asVarying(); diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index b7078a00a7..df82d4b56d 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -126,14 +126,24 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend task.addJob("DrawZoneStack", debugZoneInputs); } + // Just resolve the msaa + const auto resolveInputs = + ResolveFramebuffer::Inputs(framebuffer, static_cast(nullptr)).asVarying(); + const auto resolvedFramebuffer = task.addJob("Resolve", resolveInputs); + //auto resolvedFramebuffer = task.addJob("Resolve", framebuffer); + +#if defined(Q_OS_ANDROID) +#else // Lighting Buffer ready for tone mapping // Forward rendering on GLES doesn't support tonemapping to and from the same FBO, so we specify // the output FBO as null, which causes the tonemapping to target the blit framebuffer - const auto toneMappingInputs = ToneMappingDeferred::Inputs(framebuffer, static_cast(nullptr) ).asVarying(); + const auto toneMappingInputs = ToneMappingDeferred::Inputs(resolvedFramebuffer, static_cast(nullptr)).asVarying(); task.addJob("ToneMapping", toneMappingInputs); +#endif - // Composite the HUD and HUD layered objects - task.addJob("HUD"); + // Layered Overlays + // Composite the HUD and HUD overlays + task.addJob("HUD", resolvedFramebuffer); const auto hudOpaquesInputs = DrawLayered3D::Inputs(hudOpaque, lightingModel, nullJitter).asVarying(); const auto hudTransparentsInputs = DrawLayered3D::Inputs(hudTransparent, lightingModel, nullJitter).asVarying(); @@ -145,26 +155,32 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // task.addJob("Blit", framebuffer); } +void PrepareFramebuffer::configure(const Config& config) { + _numSamples = config.getNumSamples(); +} + void PrepareFramebuffer::run(const RenderContextPointer& renderContext, gpu::FramebufferPointer& framebuffer) { glm::uvec2 frameSize(renderContext->args->_viewport.z, renderContext->args->_viewport.w); // Resizing framebuffers instead of re-building them seems to cause issues with threaded rendering - if (_framebuffer && _framebuffer->getSize() != frameSize) { + if (_framebuffer && (_framebuffer->getSize() != frameSize || _framebuffer->getNumSamples() != _numSamples)) { _framebuffer.reset(); } if (!_framebuffer) { _framebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("forward")); + int numSamples = _numSamples; + auto colorFormat = gpu::Element::COLOR_SRGBA_32; - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); auto colorTexture = - gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + gpu::Texture::createRenderBufferMultisample(colorFormat, frameSize.x, frameSize.y, numSamples, defaultSampler); _framebuffer->setRenderBuffer(0, colorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format auto depthTexture = - gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + gpu::Texture::createRenderBufferMultisample(depthFormat, frameSize.x, frameSize.y, numSamples, defaultSampler); _framebuffer->setDepthStencilBuffer(depthTexture, depthFormat); } diff --git a/libraries/render-utils/src/RenderForwardTask.h b/libraries/render-utils/src/RenderForwardTask.h index e6a6008319..85b51ad5fa 100755 --- a/libraries/render-utils/src/RenderForwardTask.h +++ b/libraries/render-utils/src/RenderForwardTask.h @@ -27,16 +27,37 @@ public: void build(JobModel& task, const render::Varying& input, render::Varying& output); }; + +class PrepareFramebufferConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(int numSamples WRITE setNumSamples READ getNumSamples NOTIFY dirty) +public: + int getNumSamples() const { return numSamples; } + void setNumSamples(int num) { + numSamples = std::max(1, std::min(32, num)); + emit dirty(); + } + +signals: + void dirty(); + +protected: + int numSamples{ 4 }; +}; + class PrepareFramebuffer { public: using Inputs = gpu::FramebufferPointer; - using JobModel = render::Job::ModelO; + using Config = PrepareFramebufferConfig; + using JobModel = render::Job::ModelO; + void configure(const Config& config); void run(const render::RenderContextPointer& renderContext, gpu::FramebufferPointer& framebuffer); private: gpu::FramebufferPointer _framebuffer; + int _numSamples; }; class PrepareForward { diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 5f3763ac2a..6f6f2ab856 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -15,7 +15,7 @@ #include #include -#include +#include #include #include #include diff --git a/libraries/render-utils/src/TextureCache.h b/libraries/render-utils/src/TextureCache.h index d6c1e419b9..f010666e19 100644 --- a/libraries/render-utils/src/TextureCache.h +++ b/libraries/render-utils/src/TextureCache.h @@ -1,2 +1,2 @@ // Compatibility -#include +#include diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 346c6e50f6..82c408f386 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -17,6 +17,6 @@ if (NOT ANDROID) endif () -link_hifi_libraries(shared networking octree shaders gpu procedural graphics model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi) +link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi) # ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit include_hifi_library_headers(gl) diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index 8e43397c19..dcb2e39e1b 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -11,7 +11,7 @@ if (WIN32 AND (NOT USE_GLES)) setup_hifi_plugin(Gui Qml Multimedia) link_hifi_libraries(shared task gl qml networking controllers ui plugins display-plugins ui-plugins input-plugins script-engine - audio-client render-utils graphics shaders gpu render model-networking model-baker hfm fbx ktx image procedural ${PLATFORM_GL_BACKEND}) + audio-client render-utils graphics shaders gpu render material-networking model-networking model-baker hfm fbx ktx image procedural ${PLATFORM_GL_BACKEND}) include_hifi_library_headers(octree) target_openvr() diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 6a0e7c8343..926eeaeeb3 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1210,6 +1210,26 @@ div#grid-section, body#entity-list-body { width: 200px; padding-top: 1px; } +#filter-type-options-buttons { + top: -22px; + width: 224px; + z-index: 2; + background-color: #afafaf; + padding-bottom: 6px; +} +#filter-type-options input[type=button] { + position: relative; + left: 16px; + z-index: 3; + height: 23px; + min-width: 60px; + font-size: 10px; + color: #000; + background: linear-gradient(#afafaf 20%, #808080 100%); +} +#filter-type-options input[type=button]:enabled:hover { + background: linear-gradient(#afafaf 20%, #575757 100%); +} #filter-search-and-icon { position: relative; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index f525c84cbc..986e5c09b0 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -44,6 +44,10 @@
+
+ + +
diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index b19873a049..8482591771 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -188,6 +188,7 @@ let renameTimeout = null; let renameLastBlur = null; let renameLastEntityID = null; let isRenameFieldBeingMoved = false; +let elFilterTypeInputs = {}; let elEntityTable, elEntityTableHeader, @@ -201,6 +202,9 @@ let elEntityTable, elFilterTypeMultiselectBox, elFilterTypeText, elFilterTypeOptions, + elFilterTypeOptionsButtons, + elFilterTypeSelectAll, + elFilterTypeClearAll, elFilterSearch, elFilterInView, elFilterRadius, @@ -243,6 +247,9 @@ function loaded() { elFilterTypeMultiselectBox = document.getElementById("filter-type-multiselect-box"); elFilterTypeText = document.getElementById("filter-type-text"); elFilterTypeOptions = document.getElementById("filter-type-options"); + elFilterTypeOptionsButtons = document.getElementById("filter-type-options-buttons"); + elFilterTypeSelectAll = document.getElementById('filter-type-select-all'); + elFilterTypeClearAll = document.getElementById('filter-type-clear-all'); elFilterSearch = document.getElementById("filter-search"); elFilterInView = document.getElementById("filter-in-view"); elFilterRadius = document.getElementById("filter-radius"); @@ -276,6 +283,8 @@ function loaded() { }; elRefresh.onclick = refreshEntities; elFilterTypeMultiselectBox.onclick = onToggleTypeDropdown; + elFilterTypeSelectAll.onclick = onSelectAllTypes; + elFilterTypeClearAll.onclick = onClearAllTypes; elFilterSearch.onkeyup = refreshEntityList; elFilterSearch.onsearch = refreshEntityList; elFilterInView.onclick = onToggleFilterInView; @@ -290,13 +299,14 @@ function loaded() { let elDiv = document.createElement('div'); elDiv.onclick = onToggleTypeFilter; - elFilterTypeOptions.appendChild(elDiv); + elFilterTypeOptions.insertBefore(elDiv, elFilterTypeOptionsButtons); let elInput = document.createElement('input'); elInput.setAttribute("type", "checkbox"); elInput.setAttribute("id", typeFilterID); elInput.setAttribute("filterType", type); elInput.checked = true; // all types are checked initially + elFilterTypeInputs[type] = elInput; elDiv.appendChild(elInput); let elLabel = document.createElement('label'); @@ -1065,7 +1075,21 @@ function loaded() { event.stopPropagation(); } - function toggleTypeFilter(elInput, refresh) { + function refreshTypeFilter(refreshList) { + if (typeFilters.length === 0) { + elFilterTypeText.innerText = "No Types"; + } else if (typeFilters.length === FILTER_TYPES.length) { + elFilterTypeText.innerText = "All Types"; + } else { + elFilterTypeText.innerText = "Types..."; + } + + if (refreshList) { + refreshEntityList(); + } + } + + function toggleTypeFilter(elInput, refreshList) { let type = elInput.getAttribute("filterType"); let typeChecked = elInput.checked; @@ -1076,17 +1100,7 @@ function loaded() { typeFilters.push(type); } - if (typeFilters.length === 0) { - elFilterTypeText.innerText = "No Types"; - } else if (typeFilters.length === FILTER_TYPES.length) { - elFilterTypeText.innerText = "All Types"; - } else { - elFilterTypeText.innerText = "Types..."; - } - - if (refresh) { - refreshEntityList(); - } + refreshTypeFilter(refreshList); } function onToggleTypeFilter(event) { @@ -1097,6 +1111,24 @@ function loaded() { event.stopPropagation(); } + function onSelectAllTypes(event) { + for (let type in elFilterTypeInputs) { + elFilterTypeInputs[type].checked = true; + } + typeFilters = FILTER_TYPES; + refreshTypeFilter(true); + event.stopPropagation(); + } + + function onClearAllTypes(event) { + for (let type in elFilterTypeInputs) { + elFilterTypeInputs[type].checked = false; + } + typeFilters = []; + refreshTypeFilter(true); + event.stopPropagation(); + } + function onBodyClick(event) { // if clicking anywhere outside of the multiselect dropdowns (since click event bubbled up to onBodyClick and // propagation wasn't stopped in the toggle type/column callbacks) and the dropdown is open then close it diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js deleted file mode 100644 index 938477fb5b..0000000000 --- a/scripts/system/html/js/marketplacesInject.js +++ /dev/null @@ -1,749 +0,0 @@ -/* global $, window, MutationObserver */ - -// -// marketplacesInject.js -// -// Created by David Rowe on 12 Nov 2016. -// Copyright 2016 High Fidelity, Inc. -// -// Injected into marketplace Web pages. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function () { - // Event bridge messages. - var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; - var CLARA_IO_STATUS = "CLARA.IO STATUS"; - var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; - var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; - var GOTO_DIRECTORY = "GOTO_DIRECTORY"; - var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; - var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; - var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; - var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; - - var canWriteAssets = false; - var xmlHttpRequest = null; - var isPreparing = false; // Explicitly track download request status. - - var limitedCommerce = false; - var commerceMode = false; - var userIsLoggedIn = false; - var walletNeedsSetup = false; - var marketplaceBaseURL = "https://highfidelity.com"; - var messagesWaiting = false; - - function injectCommonCode(isDirectoryPage) { - // Supporting styles from marketplaces.css. - // Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain. - $("head").append( - '' - ); - - // Supporting styles from edit-style.css. - // Font family, size, and position adjusted because Raleway-Bold cannot be used cross-domain. - $("head").append( - '' - ); - - // Footer. - var isInitialHiFiPage = location.href === (marketplaceBaseURL + "/marketplace?"); - $("body").append( - '
' + - (!isInitialHiFiPage ? '' : '') + - (isInitialHiFiPage ? '🛈 Get items from Clara.io!' : '') + - (!isDirectoryPage ? '' : '') + - (isDirectoryPage ? '🛈 Select a marketplace to explore.' : '') + - '
' - ); - - // Footer actions. - $("#back-button").on("click", function () { - if (document.referrer !== "") { - window.history.back(); - } else { - var params = { type: GOTO_MARKETPLACE }; - var itemIdMatch = location.search.match(/itemId=([^&]*)/); - if (itemIdMatch && itemIdMatch.length === 2) { - params.itemId = itemIdMatch[1]; - } - EventBridge.emitWebEvent(JSON.stringify(params)); - } - }); - $("#all-markets").on("click", function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: GOTO_DIRECTORY - })); - }); - } - - function injectDirectoryCode() { - - // Remove e-mail hyperlink. - var letUsKnow = $("#letUsKnow"); - letUsKnow.replaceWith(letUsKnow.html()); - - // Add button links. - - $('#exploreClaraMarketplace').on('click', function () { - window.location = "https://clara.io/library?gameCheck=true&public=true"; - }); - $('#exploreHifiMarketplace').on('click', function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: GOTO_MARKETPLACE - })); - }); - } - - emitWalletSetupEvent = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "WALLET_SETUP" - })); - }; - - function maybeAddSetupWalletButton() { - if (!$('body').hasClass("walletsetup-injected") && userIsLoggedIn && walletNeedsSetup) { - $('body').addClass("walletsetup-injected"); - - var resultsElement = document.getElementById('results'); - var setupWalletElement = document.createElement('div'); - setupWalletElement.classList.add("row"); - setupWalletElement.id = "setupWalletDiv"; - setupWalletElement.style = "height:60px;margin:20px 10px 10px 10px;padding:12px 5px;" + - "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; - - var span = document.createElement('span'); - span.style = "margin:10px 5px;color:#1b6420;font-size:15px;"; - span.innerHTML = "Activate your Wallet to get money and shop in Marketplace."; - - var xButton = document.createElement('a'); - xButton.id = "xButton"; - xButton.setAttribute('href', "#"); - xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; - xButton.innerHTML = "X"; - xButton.onclick = function () { - setupWalletElement.remove(); - dummyRow.remove(); - }; - - setupWalletElement.appendChild(span); - setupWalletElement.appendChild(xButton); - - resultsElement.insertBefore(setupWalletElement, resultsElement.firstChild); - - // Dummy row for padding - var dummyRow = document.createElement('div'); - dummyRow.classList.add("row"); - dummyRow.style = "height:15px;"; - resultsElement.insertBefore(dummyRow, resultsElement.firstChild); - } - } - - function maybeAddLogInButton() { - if (!$('body').hasClass("login-injected") && !userIsLoggedIn) { - $('body').addClass("login-injected"); - var resultsElement = document.getElementById('results'); - if (!resultsElement) { // If we're on the main page, this will evaluate to `true` - resultsElement = document.getElementById('item-show'); - resultsElement.style = 'margin-top:0;'; - } - var logInElement = document.createElement('div'); - logInElement.classList.add("row"); - logInElement.id = "logInDiv"; - logInElement.style = "height:60px;margin:20px 10px 10px 10px;padding:5px;" + - "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; - - var button = document.createElement('a'); - button.classList.add("btn"); - button.classList.add("btn-default"); - button.id = "logInButton"; - button.setAttribute('href', "#"); - button.innerHTML = "LOG IN"; - button.style = "width:80px;height:100%;margin-top:0;margin-left:10px;padding:13px;font-weight:bold;background:linear-gradient(white, #ccc);"; - button.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "LOGIN" - })); - }; - - var span = document.createElement('span'); - span.style = "margin:10px;color:#1b6420;font-size:15px;"; - span.innerHTML = "to get items from the Marketplace."; - - var xButton = document.createElement('a'); - xButton.id = "xButton"; - xButton.setAttribute('href', "#"); - xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; - xButton.innerHTML = "X"; - xButton.onclick = function () { - logInElement.remove(); - dummyRow.remove(); - }; - - logInElement.appendChild(button); - logInElement.appendChild(span); - logInElement.appendChild(xButton); - - resultsElement.insertBefore(logInElement, resultsElement.firstChild); - - // Dummy row for padding - var dummyRow = document.createElement('div'); - dummyRow.classList.add("row"); - dummyRow.style = "height:15px;"; - resultsElement.insertBefore(dummyRow, resultsElement.firstChild); - } - } - - function changeDropdownMenu() { - var logInOrOutButton = document.createElement('a'); - logInOrOutButton.id = "logInOrOutButton"; - logInOrOutButton.setAttribute('href', "#"); - logInOrOutButton.innerHTML = userIsLoggedIn ? "Log Out" : "Log In"; - logInOrOutButton.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "LOGIN" - })); - }; - - $($('.dropdown-menu').find('li')[0]).append(logInOrOutButton); - - $('a[href="/marketplace?view=mine"]').each(function () { - $(this).attr('href', '#'); - $(this).on('click', function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "MY_ITEMS" - })); - }); - }); - } - - function buyButtonClicked(id, referrer, edition) { - EventBridge.emitWebEvent(JSON.stringify({ - type: "CHECKOUT", - itemId: id, - referrer: referrer, - itemEdition: edition - })); - } - - function injectBuyButtonOnMainPage() { - var cost; - - // Unbind original mouseenter and mouseleave behavior - $('body').off('mouseenter', '#price-or-edit .price'); - $('body').off('mouseleave', '#price-or-edit .price'); - - $('.grid-item').find('#price-or-edit').each(function () { - $(this).css({ "margin-top": "0" }); - }); - - $('.grid-item').find('#price-or-edit').find('a').each(function() { - 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(); - var costInt = parseInt(cost, 10); - - $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); - $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); - - var priceElement = $(this).find('.price'); - var available = true; - - if (priceElement.text() === 'invalidated' || - priceElement.text() === 'sold out' || - priceElement.text() === 'not for sale') { - available = false; - priceElement.css({ - "padding": "3px 5px", - "height": "40px", - "width": "100px", - "background": "linear-gradient(#a2a2a2, #fefefe)", - "color": "#000", - "font-weight": "600", - "line-height": "20px" - }); - } else { - priceElement.css({ - "padding": "3px 5px", - "height": "40px", - "background": "linear-gradient(#00b4ef, #0093C5)", - "color": "#FFF", - "font-weight": "600", - "line-height": "34px" - }); - } - - if (parseInt(cost) > 0) { - priceElement.css({ "width": "auto" }); - - if (available) { - priceElement.html(' ' + cost); - } - - priceElement.css({ "min-width": priceElement.width() + 30 }); - } - }); - - // change pricing to GET/BUY on button hover - $('body').on('mouseenter', '#price-or-edit .price', function () { - var $this = $(this); - var buyString = "BUY"; - var getString = "GET"; - // Protection against the button getting stuck in the "BUY"/"GET" state. - // That happens when the browser gets two MOUSEENTER events before getting a - // MOUSELEAVE event. Also, if not available for sale, just return. - if ($this.text() === buyString || - $this.text() === getString || - $this.text() === 'invalidated' || - $this.text() === 'sold out' || - $this.text() === 'not for sale' ) { - return; - } - $this.data('initialHtml', $this.html()); - - var cost = $(this).parent().siblings().text(); - if (parseInt(cost) > 0) { - $this.text(buyString); - } - if (parseInt(cost) == 0) { - $this.text(getString); - } - }); - - $('body').on('mouseleave', '#price-or-edit .price', function () { - var $this = $(this); - $this.html($this.data('initialHtml')); - }); - - - $('.grid-item').find('#price-or-edit').find('a').on('click', function () { - var price = $(this).closest('.grid-item').find('.price').text(); - if (price === 'invalidated' || - price === 'sold out' || - price === 'not for sale') { - return false; - } - buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'), - "mainPage", - -1); - }); - } - - function injectUnfocusOnSearch() { - // unfocus input field on search, thus hiding virtual keyboard - $('#search-box').on('submit', function () { - if (document.activeElement) { - document.activeElement.blur(); - } - }); - } - - // fix for 10108 - marketplace category cannot scroll - function injectAddScrollbarToCategories() { - $('#categories-dropdown').on('show.bs.dropdown', function () { - $('body > div.container').css('display', 'none') - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }); - }); - - $('#categories-dropdown').on('hide.bs.dropdown', function () { - $('body > div.container').css('display', ''); - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }); - }); - } - - function injectHiFiCode() { - if (commerceMode) { - maybeAddLogInButton(); - maybeAddSetupWalletButton(); - - if (!$('body').hasClass("code-injected")) { - - $('body').addClass("code-injected"); - changeDropdownMenu(); - - var target = document.getElementById('templated-items'); - // MutationObserver is necessary because the DOM is populated after the page is loaded. - // We're searching for changes to the element whose ID is '#templated-items' - this is - // the element that gets filled in by the AJAX. - var observer = new MutationObserver(function (mutations) { - mutations.forEach(function (mutation) { - injectBuyButtonOnMainPage(); - }); - }); - var config = { attributes: true, childList: true, characterData: true }; - observer.observe(target, config); - - // Try this here in case it works (it will if the user just pressed the "back" button, - // since that doesn't trigger another AJAX request. - injectBuyButtonOnMainPage(); - } - } - - injectUnfocusOnSearch(); - injectAddScrollbarToCategories(); - } - - function injectHiFiItemPageCode() { - if (commerceMode) { - maybeAddLogInButton(); - - if (!$('body').hasClass("code-injected")) { - - $('body').addClass("code-injected"); - changeDropdownMenu(); - - var purchaseButton = $('#side-info').find('.btn').first(); - - var href = purchaseButton.attr('href'); - purchaseButton.attr('href', '#'); - var cost = $('.item-cost').text(); - var costInt = parseInt(cost, 10); - // One of 'invalidated', 'not for sale', 'sold out', or 'available' - var availability = $.trim($('.item-availability').text()); - if (limitedCommerce && (costInt > 0)) { - availability = ''; - } - var isUpdating = window.location.href.indexOf('edition=') > -1; - // NFS only shows for artist stocking inventory - var isBuyEnabled = ('available' === availability) || isUpdating || ('not for sale' === availability); - if (isBuyEnabled) { - 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 urlParams = new URLSearchParams(window.location.search); - if (isUpdating) { - purchaseButton.html('UPDATE FOR FREE'); - } else if (availability == 'not for sale') { - purchaseButton.html("Free artist's stock to inventory"); - } else if (availability !== 'available') { - purchaseButton.html('UNAVAILABLE ' + (availability ? ('(' + availability + ')') : '')); - } else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { - purchaseButton.html('PURCHASE ' + cost); - } - - purchaseButton.on('click', function () { - if (isBuyEnabled) { - buyButtonClicked(window.location.pathname.split("/")[3], - "itemPage", - urlParams.get('edition')); - } - }); - } - } - - injectUnfocusOnSearch(); - } - - function updateClaraCode() { - // Have to repeatedly update Clara page because its content can change dynamically without location.href changing. - - // Clara library page. - if (location.href.indexOf("clara.io/library") !== -1) { - // Make entries navigate to "Image" view instead of default "Real Time" view. - var elements = $("a.thumbnail"); - for (var i = 0, length = elements.length; i < length; i++) { - var value = elements[i].getAttribute("href"); - if (value.slice(-6) !== "/image") { - elements[i].setAttribute("href", value + "/image"); - } - } - } - - // Clara item page. - if (location.href.indexOf("clara.io/view/") !== -1) { - // Make site navigation links retain gameCheck etc. parameters. - var element = $("a[href^=\'/library\']")[0]; - var parameters = "?gameCheck=true&public=true"; - var href = element.getAttribute("href"); - if (href.slice(-parameters.length) !== parameters) { - element.setAttribute("href", href + parameters); - } - - // Remove unwanted buttons and replace download options with a single "Download to High Fidelity" button. - var buttons = $("a.embed-button").parent("div"); - var downloadFBX; - if (buttons.find("div.btn-group").length > 0) { - buttons.children(".btn-primary, .btn-group , .embed-button").each(function () { this.remove(); }); - if ($("#hifi-download-container").length === 0) { // Button hasn't been moved already. - downloadFBX = $(' Download to High Fidelity'); - buttons.prepend(downloadFBX); - downloadFBX[0].addEventListener("click", startAutoDownload); - } - } - - // Move the "Download to High Fidelity" button to be more visible on tablet. - if ($("#hifi-download-container").length === 0 && window.innerWidth < 700) { - var downloadContainer = $('
'); - $(".top-title .col-sm-4").append(downloadContainer); - downloadContainer.append(downloadFBX); - } - } - } - - // Automatic download to High Fidelity. - function startAutoDownload() { - // One file request at a time. - if (isPreparing) { - console.log("WARNING: Clara.io FBX: Prepare only one download at a time"); - return; - } - - // User must be able to write to Asset Server. - if (!canWriteAssets) { - console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); - EventBridge.emitWebEvent(JSON.stringify({ - type: WARN_USER_NO_PERMISSIONS - })); - return; - } - - // User must be logged in. - var loginButton = $("#topnav a[href='/signup']"); - if (loginButton.length > 0) { - loginButton[0].click(); - return; - } - - // Obtain zip file to download for requested asset. - // Reference: https://clara.io/learn/sdk/api/export - - //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; - // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to - // be successful in generating zip files. - var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; - - var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; - var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); - - xmlHttpRequest = new XMLHttpRequest(); - var responseTextIndex = 0; - var zipFileURL = ""; - - xmlHttpRequest.onreadystatechange = function () { - // Messages are appended to responseText; process the new ones. - var message = this.responseText.slice(responseTextIndex); - var statusMessage = ""; - - if (isPreparing) { // Ignore messages in flight after finished/cancelled. - var lines = message.split(/[\n\r]+/); - - for (var i = 0, length = lines.length; i < length; i++) { - if (lines[i].slice(0, 5) === "data:") { - // Parse line. - var data; - try { - data = JSON.parse(lines[i].slice(5)); - } - catch (e) { - data = {}; - } - - // Extract zip file URL. - if (data.hasOwnProperty("files") && data.files.length > 0) { - zipFileURL = data.files[0].url; - } - } - } - - if (statusMessage !== "") { - // Update the UI with the most recent status message. - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_STATUS, - status: statusMessage - })); - } - } - - responseTextIndex = this.responseText.length; - }; - - // Note: onprogress doesn't have computable total length so can't use it to determine % complete. - - xmlHttpRequest.onload = function () { - var statusMessage = ""; - - if (!isPreparing) { - return; - } - - isPreparing = false; - - var HTTP_OK = 200; - if (this.status !== HTTP_OK) { - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_STATUS, - status: statusMessage - })); - } else if (zipFileURL.slice(-4) !== ".zip") { - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_STATUS, - status: (statusMessage + ": " + zipFileURL) - })); - } else { - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_DOWNLOAD - })); - } - - xmlHttpRequest = null; - } - - isPreparing = true; - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_STATUS, - status: "Initiating download" - })); - - xmlHttpRequest.open("POST", url, true); - xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); - xmlHttpRequest.send(); - } - - function injectClaraCode() { - - // Make space for marketplaces footer in Clara pages. - $("head").append( - '' - ); - - // Condense space. - $("head").append( - '' - ); - - // Move "Download to High Fidelity" button. - $("head").append( - '' - ); - - // Update code injected per page displayed. - var updateClaraCodeInterval = undefined; - updateClaraCode(); - updateClaraCodeInterval = setInterval(function () { - updateClaraCode(); - }, 1000); - - window.addEventListener("unload", function () { - clearInterval(updateClaraCodeInterval); - updateClaraCodeInterval = undefined; - }); - - EventBridge.emitWebEvent(JSON.stringify({ - type: QUERY_CAN_WRITE_ASSETS - })); - } - - function cancelClaraDownload() { - isPreparing = false; - - if (xmlHttpRequest) { - xmlHttpRequest.abort(); - xmlHttpRequest = null; - console.log("Clara.io FBX: File download cancelled"); - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_CANCELLED_DOWNLOAD - })); - } - } - - function injectCode() { - var DIRECTORY = 0; - var HIFI = 1; - var CLARA = 2; - var HIFI_ITEM_PAGE = 3; - var pageType = DIRECTORY; - - if (location.href.indexOf(marketplaceBaseURL + "/") !== -1) { pageType = HIFI; } - if (location.href.indexOf("clara.io/") !== -1) { pageType = CLARA; } - if (location.href.indexOf(marketplaceBaseURL + "/marketplace/items/") !== -1) { pageType = HIFI_ITEM_PAGE; } - - injectCommonCode(pageType === DIRECTORY); - switch (pageType) { - case DIRECTORY: - injectDirectoryCode(); - break; - case HIFI: - injectHiFiCode(); - break; - case CLARA: - injectClaraCode(); - break; - case HIFI_ITEM_PAGE: - injectHiFiItemPageCode(); - break; - - } - } - - function onLoad() { - EventBridge.scriptEventReceived.connect(function (message) { - message = JSON.parse(message); - if (message.type === CAN_WRITE_ASSETS) { - canWriteAssets = message.canWriteAssets; - } else if (message.type === CLARA_IO_CANCEL_DOWNLOAD) { - cancelClaraDownload(); - } else if (message.type === "marketplaces") { - if (message.action === "commerceSetting") { - limitedCommerce = !!message.data.limitedCommerce; - commerceMode = !!message.data.commerceMode; - userIsLoggedIn = !!message.data.userIsLoggedIn; - walletNeedsSetup = !!message.data.walletNeedsSetup; - marketplaceBaseURL = message.data.metaverseServerURL; - if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { - marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); - } - messagesWaiting = message.data.messagesWaiting; - injectCode(); - } - } - }); - - // Request commerce setting - // Code is injected into the webpage after the setting comes back. - EventBridge.emitWebEvent(JSON.stringify({ - type: "REQUEST_SETTING" - })); - } - - // Load / unload. - window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). - window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed -}()); diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 39796311a0..767128ca16 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -285,6 +285,10 @@ SelectionManager = (function() { properties.localPosition = properties.position; properties.localRotation = properties.rotation; } + + properties.localVelocity = Vec3.ZERO; + properties.localAngularVelocity = Vec3.ZERO; + delete properties.actionData; var newEntityID = Entities.addEntity(properties); diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index b9ae635a4f..6cda67db2d 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -20,7 +20,7 @@ endfunction() if (BUILD_TOOLS) # Allow different tools for stable builds - if (STABLE_BUILD) + if (RELEASE_TYPE STREQUAL "PRODUCTION") set(ALL_TOOLS udt-test vhacd-util diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index 63d5af9272..4e83460b9e 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -589,4 +590,4 @@ void AWSInterface::updateAWS() { QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename; process->start("sh", parameters); #endif -} \ No newline at end of file +} diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index d5bc6f6e5a..39800c6bc6 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -40,7 +40,11 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v2.1.2"); + setWindowTitle("Nitpick - v3.0.0"); + + clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; + _ui.clientProfileComboBox->insertItems(0, clientProfiles); + } Nitpick::~Nitpick() { @@ -157,7 +161,7 @@ void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() { } void Nitpick::on_createTestsPushbutton_clicked() { - _test->createTests(); + _test->createTests(_ui.clientProfileComboBox->currentText()); } void Nitpick::on_createMDFilePushbutton_clicked() { diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index 36ec7e534b..80fef934d6 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -126,6 +126,8 @@ private: bool _isRunningFromCommandline{ false }; void* _caller; + + QStringList clientProfiles; }; #endif // hifi_Nitpick_h \ No newline at end of file diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/Test.cpp index f1e950db88..e8e284bf32 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -391,7 +391,7 @@ void Test::includeTest(QTextStream& textStream, const QString& testPathname) { textStream << "Script.include(testsRootPath + \"" << partialPathWithoutTests + "\");" << endl; } -void Test::createTests() { +void Test::createTests(const QString& clientProfile) { // Rename files sequentially, as ExpectedResult_00000.png, ExpectedResult_00001.png and so on // Any existing expected result images will be deleted QString previousSelection = _snapshotDirectory; diff --git a/tools/nitpick/src/Test.h b/tools/nitpick/src/Test.h index 166c71688d..23011d0c31 100644 --- a/tools/nitpick/src/Test.h +++ b/tools/nitpick/src/Test.h @@ -52,7 +52,7 @@ public: void finishTestsEvaluation(); - void createTests(); + void createTests(const QString& clientProfile); void createTestsOutline(); diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 79bdfd158b..47471522db 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -52,8 +52,8 @@ - 210 - 60 + 70 + 40 220 40 @@ -153,6 +153,16 @@ Create all testAuto scripts + + + + 320 + 40 + 120 + 40 + + + diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 8f4d5a7962..7b90145223 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -14,13 +14,14 @@ using System.Collections.Generic; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.1"; + static readonly string AVATAR_EXPORTER_VERSION = "0.2"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; static readonly int MAXIMUM_USER_BONE_COUNT = 256; static readonly string EMPTY_WARNING_TEXT = "None"; + // TODO: use regex static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] { "2018.2.12f1", "2018.2.11f1", @@ -262,8 +263,8 @@ class AvatarExporter : MonoBehaviour { static string assetPath = ""; static string assetName = ""; static HumanDescription humanDescription; - - + static Dictionary dependencyTextures = new Dictionary(); + [MenuItem("High Fidelity/Export New Avatar")] static void ExportNewAvatar() { ExportSelectedAvatar(false); @@ -301,54 +302,38 @@ class AvatarExporter : MonoBehaviour { " the Rig section of it's Inspector window.", "Ok"); return; } - + humanDescription = modelImporter.humanDescription; SetUserBoneInformation(); + string textureWarnings = SetTextureDependencies(); + + // check if we should be substituting a bone for a missing UpperChest mapping + AdjustUpperChestMapping(); // format resulting bone rule failure strings // consider export-blocking bone rules to be errors and show them in an error dialog, - // and also include any other bone rule failures as warnings in the dialog + // and also include any other bone rule failures plus texture warnings as warnings in the dialog string boneErrors = ""; - string boneWarnings = ""; + string warnings = ""; foreach (var failedBoneRule in failedBoneRules) { if (Array.IndexOf(EXPORT_BLOCKING_BONE_RULES, failedBoneRule.Key) >= 0) { boneErrors += failedBoneRule.Value + "\n\n"; } else { - boneWarnings += failedBoneRule.Value + "\n\n"; + warnings += failedBoneRule.Value + "\n\n"; } } + warnings += textureWarnings; if (!string.IsNullOrEmpty(boneErrors)) { // if there are both errors and warnings then warnings will be displayed with errors in the error dialog - if (!string.IsNullOrEmpty(boneWarnings)) { + if (!string.IsNullOrEmpty(warnings)) { boneErrors = "Errors:\n\n" + boneErrors; - boneErrors += "Warnings:\n\n" + boneWarnings; + boneErrors += "Warnings:\n\n" + warnings; } // remove ending newlines from the last rule failure string that was added above boneErrors = boneErrors.Substring(0, boneErrors.LastIndexOf("\n\n")); EditorUtility.DisplayDialog("Error", boneErrors, "Ok"); return; } - - if (!humanoidToUserBoneMappings.ContainsKey("UpperChest")) { - // if parent of Neck is not Chest then map the parent to UpperChest - string neckUserBone; - if (humanoidToUserBoneMappings.TryGetValue("Neck", out neckUserBone)) { - UserBoneInformation neckParentBoneInfo; - string neckParentUserBone = userBoneInfos[neckUserBone].parentName; - if (userBoneInfos.TryGetValue(neckParentUserBone, out neckParentBoneInfo) && !neckParentBoneInfo.HasHumanMapping()) { - neckParentBoneInfo.humanName = "UpperChest"; - humanoidToUserBoneMappings.Add("UpperChest", neckParentUserBone); - } - } - // if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest - string chestUserBone; - if (!humanoidToUserBoneMappings.ContainsKey("UpperChest") && - humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { - userBoneInfos[chestUserBone].humanName = "UpperChest"; - humanoidToUserBoneMappings.Remove("Chest"); - humanoidToUserBoneMappings.Add("UpperChest", chestUserBone); - } - } string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); string hifiFolder = documentsFolder + "\\High Fidelity Projects"; @@ -407,7 +392,13 @@ class AvatarExporter : MonoBehaviour { return; } else if (option == 0) { // Yes - copy model to Unity project // copy the fbx from the project folder to Unity Assets, overwriting the existing fbx, and re-import it - File.Copy(exportModelPath, assetPath, true); + try { + File.Copy(exportModelPath, assetPath, true); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + exportModelPath + " to " + assetPath + + ". Please check the location and try again.", "Ok"); + return; + } AssetDatabase.ImportAsset(assetPath); // set model to Humanoid animation type and force another refresh on it to process Humanoid @@ -455,12 +446,20 @@ class AvatarExporter : MonoBehaviour { } // write out a new fst file in place of the old file - WriteFST(exportFstPath, projectName); + if (!WriteFST(exportFstPath, projectName)) { + return; + } + + // copy any external texture files to the project's texture directory that are considered dependencies of the model + string texturesDirectory = GetTextureDirectory(exportFstPath); + if (!CopyExternalTextures(texturesDirectory)) { + return; + } // display success dialog with any bone rule warnings string successDialog = "Avatar successfully updated!"; - if (!string.IsNullOrEmpty(boneWarnings)) { - successDialog += "\n\nWarnings:\n" + boneWarnings; + if (!string.IsNullOrEmpty(warnings)) { + successDialog += "\n\nWarnings:\n" + warnings; } EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); } else { // Export New Avatar menu option @@ -469,13 +468,13 @@ class AvatarExporter : MonoBehaviour { Directory.CreateDirectory(hifiFolder); } - if (string.IsNullOrEmpty(boneWarnings)) { - boneWarnings = EMPTY_WARNING_TEXT; + if (string.IsNullOrEmpty(warnings)) { + warnings = EMPTY_WARNING_TEXT; } // open a popup window to enter new export project name and project location ExportProjectWindow window = ScriptableObject.CreateInstance(); - window.Init(hifiFolder, boneWarnings, OnExportProjectWindowClose); + window.Init(hifiFolder, warnings, OnExportProjectWindowClose); } } @@ -485,28 +484,34 @@ class AvatarExporter : MonoBehaviour { File.Copy(assetPath, exportModelPath); // create empty Textures and Scripts folders in the project directory - string texturesDirectory = projectDirectory + "\\textures"; + string texturesDirectory = GetTextureDirectory(projectDirectory); string scriptsDirectory = projectDirectory + "\\scripts"; Directory.CreateDirectory(texturesDirectory); Directory.CreateDirectory(scriptsDirectory); // write out the avatar.fst file to the project directory string exportFstPath = projectDirectory + "avatar.fst"; - WriteFST(exportFstPath, projectName); + if (!WriteFST(exportFstPath, projectName)) { + return; + } + + // copy any external texture files to the project's texture directory that are considered dependencies of the model + if (!CopyExternalTextures(texturesDirectory)) { + return; + } // remove any double slashes in texture directory path, display success dialog with any // bone warnings previously mentioned, and suggest user to copy external textures over - texturesDirectory = texturesDirectory.Replace("\\\\", "\\"); string successDialog = "Avatar successfully exported!\n\n"; if (warnings != EMPTY_WARNING_TEXT) { successDialog += "Warnings:\n" + warnings; } successDialog += "Note: If you are using any external textures with your model, " + - "please copy those textures to " + texturesDirectory; + "please ensure those textures are copied to " + texturesDirectory; EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); } - static void WriteFST(string exportFstPath, string projectName) { + static bool WriteFST(string exportFstPath, string projectName) { // write out core fields to top of fst file try { File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " + @@ -514,7 +519,7 @@ class AvatarExporter : MonoBehaviour { } catch { EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath + ". Please check the location and try again.", "Ok"); - return; + return false; } // write out joint mappings to fst file @@ -573,6 +578,8 @@ class AvatarExporter : MonoBehaviour { // open File Explorer to the project directory once finished System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); + + return true; } static void SetUserBoneInformation() { @@ -652,6 +659,29 @@ class AvatarExporter : MonoBehaviour { return result; } + static void AdjustUpperChestMapping() { + if (!humanoidToUserBoneMappings.ContainsKey("UpperChest")) { + // if parent of Neck is not Chest then map the parent to UpperChest + string neckUserBone; + if (humanoidToUserBoneMappings.TryGetValue("Neck", out neckUserBone)) { + UserBoneInformation neckParentBoneInfo; + string neckParentUserBone = userBoneInfos[neckUserBone].parentName; + if (userBoneInfos.TryGetValue(neckParentUserBone, out neckParentBoneInfo) && !neckParentBoneInfo.HasHumanMapping()) { + neckParentBoneInfo.humanName = "UpperChest"; + humanoidToUserBoneMappings.Add("UpperChest", neckParentUserBone); + } + } + // if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest + string chestUserBone; + if (!humanoidToUserBoneMappings.ContainsKey("UpperChest") && + humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { + userBoneInfos[chestUserBone].humanName = "UpperChest"; + humanoidToUserBoneMappings.Remove("Chest"); + humanoidToUserBoneMappings.Add("UpperChest", chestUserBone); + } + } + } + static void SetFailedBoneRules() { failedBoneRules.Clear(); @@ -865,6 +895,50 @@ class AvatarExporter : MonoBehaviour { appendage + " (" + rightCount + ")."); } } + + static string GetTextureDirectory(string basePath) { + string textureDirectory = Path.GetDirectoryName(basePath) + "\\textures"; + textureDirectory = textureDirectory.Replace("\\\\", "\\"); + return textureDirectory; + } + + static string SetTextureDependencies() { + string textureWarnings = ""; + dependencyTextures.Clear(); + + // build the list of all local asset paths for textures that Unity considers dependencies of the model + // for any textures that have duplicate names, return a string of duplicate name warnings + string[] dependencies = AssetDatabase.GetDependencies(assetPath); + foreach (string dependencyPath in dependencies) { + UnityEngine.Object textureObject = AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(Texture2D)); + if (textureObject != null) { + string textureName = Path.GetFileName(dependencyPath); + if (dependencyTextures.ContainsKey(textureName)) { + textureWarnings += "There is more than one texture with the name " + textureName + + " referenced in the selected avatar.\n\n"; + } else { + dependencyTextures.Add(textureName, dependencyPath); + } + } + } + + return textureWarnings; + } + + static bool CopyExternalTextures(string texturesDirectory) { + // copy the found dependency textures from the local asset folder to the textures folder in the target export project + foreach (var texture in dependencyTextures) { + string targetPath = texturesDirectory + "\\" + texture.Key; + try { + File.Copy(texture.Value, targetPath, true); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy texture file " + texture.Value + " to " + targetPath + + ". Please check the location and try again.", "Ok"); + return false; + } + } + return true; + } } class ExportProjectWindow : EditorWindow { diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index f02bc688ae..b81a620406 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.1 +Version 0.2 Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 5e825bd0d9..95c000e7c6 100644 Binary files a/tools/unity-avatar-exporter/avatarExporter.unitypackage and b/tools/unity-avatar-exporter/avatarExporter.unitypackage differ