diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ba5e1264f..551cfd2636 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,9 @@ else() set(MOBILE 0) endif() +# Use default time server if none defined in environment +set_from_env(TIMESERVER_URL TIMESERVER_URL "http://sha256timestamp.ws.symantec.com/sha256/timestamp") + set(HIFI_USE_OPTIMIZED_IK OFF) set(BUILD_CLIENT_OPTION ON) set(BUILD_SERVER_OPTION ON) diff --git a/CODING_STANDARD.md b/CODING_STANDARD.md new file mode 100644 index 0000000000..17f7ecb2f2 --- /dev/null +++ b/CODING_STANDARD.md @@ -0,0 +1,1008 @@ +# Coding Standards + +Note that the current code base does not necessarily follow this with 100% consistency. It will be an ongoing process to try and sanitize the existing code to match these guidelines. + +Basically taken directly from [http://geosoft.no/development/cppstyle.html](http://geosoft.no/development/cppstyle.html) with some subtle changes and omissions. + +## [1] Naming + +### [1.1] General Naming Conventions + +#### [1.1.1] Names representing types must be in mixed case starting with upper case. + +```cpp +Coach, PenaltyBox + +``` + +#### [1.1.2] Private class variables must be in mixed case prefixed with an underscore. + +```cpp +_puck, _team + +``` + +#### [1.1.3] Local variables must be in mixed case (and NOT prefixed with an underscore). + +```cpp +redLine, icingFrequency + +``` + +#### [1.1.4] Constants must be all uppercase using underscore to separate words. + +```cpp +MAX_RINK_LENGTH, COLOR_RED_LINE + +``` + +#### [1.1.5] Methods or functions must be verbs and written in mixed case starting with lower case. + +```cpp +getPlayerNumber(), computeGoalsAgainstAverage() + +``` + +#### [1.1.6] Names representing namespaces should be all lowercase. + +```cpp +puck::geometry, ice::math + +``` + +#### [1.1.7] Names representing template types should be a single uppercase letter. + +```cpp +template, template, template + +``` + +This makes template names stand out relative to all other names used. + +#### [1.1.8] Abbreviations and acronyms must be uppercase when used in a name or lowercase when used at the beginning of a variable + +```cpp +showNHLStandings(); // not showNhlStandings(); +exportASCIIStanleyCup(); // not exportAsciiStanleyCup(); +UDPSocket udpSocket; // not UDPSocket uDPSocket; + +``` + +#### [1.1.9] Global variables should always be referred to using the :: operator. + +```cpp +::jumbotron.powerOn(); +::league.lockout(); + +``` + +#### [1.1.10] Generic variables should have the same name as their type. + +```cpp +void setPuckLogo(Logo* logo) // not void setPuckLogo(Logo* aLogo) + +``` + +These will be discernible from class member variables since they are not prefixed with an underscore. + +#### [1.1.11] All names should be written in English. + +```cpp +int hockeyStick; // NOT: bastonDeHockey + +``` + +#### [1.1.12] The name of the object is implicit, and should be avoided in a method name. + +```cpp +puck.getDensity(); // NOT: puck.getPuckDensity(); + +``` + +### [1.2] Specific Naming Conventions + +#### [1.2.1] The terms get/set must be used where an attribute is accessed directly. + +```cpp +player.getNumber(); +player.setNumber(number); +stick.getFlex(); +stick.setFlex(flex); + +``` + +There is an exception for boolean getters. Naming for boolean attributes should follow [section 1.2.10](https://wiki.highfidelity.com/wiki/Coding_Standards#1-2-10-the-prefix-is-should-be-used-for-boolean-variables-and-methods-). The getter for a boolean attribute does not need to be prefixed with 'get', and should simply match the name of the boolean attribute. The following example is for a member variable `_isCaptain` on the `crosby` object. + +```cpp +crosby.setIsCaptain(true); +crosby.isCaptain(); + +``` + +#### [1.2.2] The term compute can be used in methods where something is computed. + +```cpp +team->computePowerPlayPercentage(); +player->computePointsPerGame(); + +``` + +Give the reader the immediate clue that this is a potentially time-consuming operation, and if used repeatedly, she might consider caching the result. Consistent use of the term enhances readability. + +#### [1.2.3] The term find can be used in methods where something is looked up. + +```cpp +net.findGoalLinePosition(); +team.findHeaviestPlayer(); + +``` + +Give the reader the immediate clue that this is a simple look up method with a minimum of computations involved. Consistent use of the term enhances readability. + +#### [1.2.4] The term initialize can be used where an object or a concept is established. + +``` +rink.initializePaintedLines(); +video.initializeOnScreenScore(); + +``` + +#### [1.2.5] Variables representing GUI components should be suffixed by the component type name. + +```cpp +scoreboardText, mainWindow, fileMenu + +``` + +#### [1.2.6] Plural form should be used on names representing a collection of objects. + +```cpp +std::vector players; +float savePercentages[]; + +``` + +#### [1.2.7] The prefix num should be used for variables representing a number of objects. + +```cpp +numGoals, numAssists + +``` + +#### [1.2.8] The suffix Num should be used for variables representing an entity number. + +```cpp +playerNum, teamNum + +``` + +#### [1.2.9] Iterator variables should be called i, j, k etc. + +```cpp +for (int i = 0; i < numGoals); i++) { + goals[i].playVideo(); +} + +``` + +#### [1.2.10] The prefix is should be used for boolean variables and methods. + +isGoodGoal, isRetired, isWinningTeam Occasionally the has, can, should, and want prefixes will be better choices. + +*Note: "want" should generally be used for optional items that are specified by some third party action, e.g. command line or menu options that enable additional functionality, or protocol versioning where negotiation occurs between client and server.* + +```cpp +hasWonStanleyCup, canPlay, shouldPass, wantDebugLogging + +``` + +#### [1.2.11] Complement names must be used for complement operations + +```cpp +get/set, add/remove, create/destroy, start/stop + +``` + +#### [1.2.12] Abbreviations in names should be avoided. + +```cpp +computeGoalsAgainstAverage(); // NOT: compGlsAgstAvg(); + +``` + +There are domain specific phrases that are more naturally known through their abbreviations/acronym. These phrases should be kept abbreviated. + +Use `html` instead of `hypertextMarkupLanguage`. + +#### [1.2.13] Naming pointers specifically should be avoided. + +```cpp +Puck* puck; // NOT: Puck * puckPtr; + +``` + +Many variables in a C/C++ environment are pointers, so a convention like this is almost impossible to follow. Also objects in C++ are often oblique types where the specific implementation should be ignored by the programmer. Only when the actual type of an object is of special significance, the name should emphasize the type. + +#### [1.2.14] Negated boolean variable names must be avoided. + +```cpp +bool isRetired; // NOT: isNotRetired or isNotPlaying + +``` + +This is done to avoid double negatives when used in conjunction with the logical negation operator. + +#### [1.2.15] Enumeration constants can be prefixed by a common type name. + +```cpp +enum Jersey { + JERSEY_HOME, + JERSEY_AWAY, + JERSEY_ALTERNATE +}; + +``` + +#### [1.2.16] Exception classes should be suffixed with Exception. + +```cpp +class GoalException { + ... +}; + +``` + +## [2] Files + +### [2.1] Source Files + +#### [2.1.1] C++ header files should have the extension .h. Source files should have the extension .cpp. + +```cpp +Puck.h, Puck.cpp + +``` + +#### [2.1.2] A class should always be declared in a header file and defined in a source file where the name of the files match the name of the class. + +`class Puck` defined in `Puck.h`, `Puck.cpp` + +#### [2.1.3] Most function implementations should reside in the source file. + +The header files should declare an interface, the source file should implement it. When looking for an implementation, the programmer should always know that it is found in the source file. + +- Simple getters and setters that just access private member variables should appear inline in the class definition in the header file. +- Simple methods like those making slight mutations (that can fit on the same line in the definition and don't require additional includes in the header file) can be inlined in the class definition. +- Methods that will be called multiple times in tight-loops or other high-performance situations and must be high performance can be included in the header file BELOW the class definition marked as inline. +- All other methods must be in a cpp file. + +```cpp +class Puck { +public: + // simple getters/setters should appear in the header file + int getRadius() const { return _radius; } + void setRadius(int radius) { _radius = radius; } + + // Allowed, ok to include this simple mutation in line + void addBlaToList(Blah* bla) { _blas.append(bla); } + + // Allowed, because this is a simple method + int calculateCircumference() { return PI * pow(_radius, 2.0); } + + // this routine needs to be fast, we'll inline it below + void doSomethingHighPerformance() const; + ... +private: + int _radius; +} + +inline void Puck::doSomethingHighPerformance() const { + ... +} + +``` + +#### [2.1.4] File content must be kept within 128 columns. + +#### [2.1.5] Special characters like TAB and page break must be avoided. + +Use four spaces for indentation. + +#### [2.1.6] The incompleteness of split lines must be made obvious. + +```cpp +teamGoals = iginlaGoals + crosbyGoals + + malkinGoals; + +addToScoreSheet(scorer, directAssister, + indirectAssister); + +setHeadline("Crosby scores 4" + " to force game 7."); + +for (int teamNum = 0; teamNum < numTeams; + teamNum++) { + ... +} + +``` + +Split lines occurs when a statement exceed the 128 column limit given above. It is difficult to provide rigid rules for how lines should be split, but the examples above should give a general hint. + +In general: Break after a comma. Break after an operator. Align the new line with the beginning of the expression on the previous line. + +### [2.2] Include Files and Include Statements + +#### [2.2.1] Header files must contain an include guard. + +Include guards should be in the following format: hifi_$BASENAME_h. + +```cpp +#ifndef hifi_SharedUtil_h +#define hifi_SharedUtil_h + +... + +#endif // hifi_SharedUtil_h + +``` + +#### [2.2.2] Include statements should be sorted and grouped. Sorted by their hierarchical position in the system with low level files included first. Leave an empty line between groups of include statements. + +```cpp +#include +#include + +#include +#include + +#include "Puck.h" +#include "PenaltyBox.h" + +``` + +#### [2.2.3] Include statements must be located at the top of a file only. + +## [3] Statements + +### [3.1] Types + +#### [3.1.1] The parts of a class must be sorted public, protected and private. All sections must be identified explicitly. Not applicable sections should be left out. + +The ordering is "most public first" so people who only wish to use the class can stop reading when they reach the protected/private sections. + +#### [3.1.2] Never rely on implicit type conversion. // NOT: floatValue = intValue; + +##### [3.1.2.1] Primitive types should use C style casting: + +```cpp +int foo = 1; +float bar = (float)foo; +// NOT this: float fubar = float(foo); + +uint8_t* barDataAt = (uint8_t*)&bar; // pointers to primitive types also use C style casting. + +``` + +##### [3.1.2.2] Class pointers must use C++ style casting: + +```cpp +Player* player = getPlayer("forward"); +Forward* forward = static_cast(player); + +``` + +For more info about C++ type casting: [http://stackoverflow.com/questions/1609163/what-is-the-difference-between-static-cast-and-c-style-casting](http://stackoverflow.com/questions/1609163/what-is-the-difference-between-static-cast-and-c-style-casting) + +#### [3.1.3] Use of *const* + +##### [3.1.3.1] Use const types for variables, parameters, return types, and methods whenever possible + +```cpp +void exampleBarAndFoo(const Bar& bar, const char* foo); // doesn't modify bar and foo, use const types +void ClassBar::spam() const { } // doesn't modify instance of ClassBar, use const method + +``` + +##### [3.1.3.2] Place the const keyword before the type + +```cpp +void foo(const Bar& bar); +// NOT: void foo(Bar const& bar); +void spam(const Foo* foo); +// NOT: void foo(Foo const* foo); + +``` + +##### [3.1.3.3] When implementing a getter for a class that returns a class member that is a complex data type, return a const& to that member. + +```cpp +const glm::vec3& AABox::getCorner() const; +// NOT: glm::vec3 AABox::getCorner() const; + +``` + +#### [3.1.4] Type aliases + +##### [3.1.4.1] When creating a type alias, prefer the using keyword. + +```cpp +template +using Vec = std::vector>; +using Nodes = Vec ; +// NOT: typedef std::vector Nodes; + +``` + +### [3.2] Variables + +#### [3.2.1] Variables should be initialized where they are declared. + +This ensures that variables are valid at any time. + +Sometimes it is impossible to initialize a variable to a valid value where it is declared: + +```cpp +Player crosby, dupuis, kunitz; +getLineStats(&crosby, &dupuis, &kunitz); + +``` + +In these cases it should be left uninitialized rather than initialized to some phony value. + +#### [3.2.2] Initialization of member variables with default values + +When possible, initialization of default values for class members should be included in the header file where the member variable is declared, as opposed to the constructor. Use the Universal Initializer format (brace initialization) rather than the assignment operator (equals). + +```cpp +private: + float _goalsPerGame { 0.0f }; // NOT float _goalsPerGame = 0.0f; + +``` + +However, brace initialization should be used with care when using container types that accept an initializer list as a constructor parameters. For instance, + +```cpp +std::vector _foo { 4, 100 } + +``` + +Might refer to `std::vector::vector(std::initializer_list)` or it might refer to `std::vector (size_type n, const T& val = value_type())`. Although the rules of precedence dictate that it will resolve to one of these, it's not immediately obvious to other developers which it is, so avoid such ambiguities. + +Classes that are forward declared and only known to the implementation may be initialized to a default value in the constructor initialization list. + +#### [3.2.3] Use of global variables should be minimized + +[http://stackoverflow.com/questions/484635/are-global-variables-bad](http://stackoverflow.com/questions/484635/are-global-variables-bad) + +#### [3.2.4] Class variables should never be declared public + +Use private variables and access functions instead. + +One exception to this rule is when the class is essentially a data structure, with no behavior (equivalent to a C struct). In this case it is appropriate to make the class' instance variables public. + +*Note that structs are kept in C++ for compatibility with C only, and avoiding them increases the readability of the code by reducing the number of constructs used. Use a class instead.* + +#### [3.2.5] C++ pointers and references should have their reference symbol next to the type rather than to the name. + +```cpp +float* savePercentages; +// NOT: float *savePercentages; or float * savePercentages; + +void checkCups(int& numCups); +// NOT: int &numCups or int & numCups + +``` + +The pointer-ness or reference-ness of a variable is a property of the type rather than the name. Also see [rule 3.1.3.2](https://wiki.highfidelity.com/wiki/Coding_Standards#constplacement) regarding placement the const keyword before the type. + +#### [3.2.6] Implicit test for 0 should not be used other than for boolean variables or non-NULL pointers. + +```cpp +if (numGoals != 0) // NOT: if (numGoals) +if (savePercentage != 0.0) // NOT: if (savePercentage) + +// Testing pointers for non-NULL is prefered, e.g. where +// childNode is Node* and you’re testing for non NULL +if (childNode) + +// Testing for null is also preferred +if (!childNode) + +``` + +It is not necessarily defined by the C++ standard that ints and floats 0 are implemented as binary 0. + +#### [3.2.7] Variables should be declared in the smallest scope possible. + +Keeping the operations on a variable within a small scope, it is easier to control the effects and side effects of the variable. + +### [3.3] Loops + +#### [3.3.1] Loop variables should be initialized immediately before the loop. + +#### [3.3.2] The form while (true) should be used for infinite loops. + +```cpp +while (true) { + : +} + +// NOT: + +for (;;) { + : +} + +while (1) { + : +} + +``` + +### [3.4] Conditionals + +#### [3.4.1] The nominal case should be put in the if-part and the exception in the else-part of an if statement + +```cpp +bool isGoal = pastGoalLine(position); + +if (isGoal) { + ... +} else { + ... +} + +``` + +Makes sure that the exceptions don't obscure the normal path of execution. This is important for both the readability and performance. + +#### [3.4.2] The conditional should be put on a separate line and wrapped in braces. + +```cpp +if (isGoal) { + lightTheLamp(); +} + +// NOT: if (isGoal) lightTheLamp(); + +``` + +#### [3.4.3] Write the expression of a conditional similar to how you would speak it out loud. + +```cpp +if (someVariable == 0) { + doSomething(); +} +// NOT: if (0 == someVariable) + +``` + +### [3.5] Miscellaneous + +#### [3.5.1] Constants and Magic Numbers + +##### [3.5.1.1] The use of magic numbers in the code should be avoided. + +- Numbers other than 0 and 1 should be considered declared as named constants instead. +- If the number does not have an obvious meaning by itself, the readability is enhanced by introducing a named constant instead. +- A different approach is to introduce a method from which the constant can be accessed. + +##### [3.5.1.2] Declare constants closest to the scope of their use. + +```cpp +bool checkValueLimit(int value) { + const int ValueLimit = 10; // I only use this constant here, define it here in context + return (value > ValueLimit); +} + +``` + +##### [3.5.1.3] Use const typed variables instead of #define + +```cpp +const float LARGEST_VALUE = 10000.0f; +// NOT: #define LARGEST_VALUE 10000.0f + +``` + +#### [3.5.2] Floating point constants should always be written with decimal point and at least one decimal. + +```cpp +double stickLength = 0.0; // NOT: double stickLength = 0; + +double penaltyMinutes; +... +penaltyMinutes = (minor + misconduct) * 2.0; + +``` + +#### [3.5.3] Floating point constants should always be written with a digit before the decimal point. + +```cpp +double penaltyMinutes = 0.5; // NOT: double penaltyMinutes = .5; + +``` + +#### [3.5.4] When using a single precision float type, include the trailing f. + +```cpp +float penaltyMinutes = 0.5f; // NOT: float penaltyMinutes = 0.5; + +``` + +## [4] Layout and Comments + +### [4.1] Layout + +#### [4.1.1] Basic indentation should be 4. + +```cpp +if (player.isCaptain) { + player.yellAtReferee(); +} + +``` + +#### [4.1.2] Use inline braces for block layout + +```cpp +while (!puckHeld) { + lookForRebound(); +} + +// NOT: +// while (!puckHeld) +// { +// lookForRebound(); +// } + +``` + +#### [4.1.3] The class declarations should have the following form: + +```cpp +class GoalieStick : public HockeyStick { +public: + ... +protected: + ... +private: + ... +}; + +``` + +#### [4.1.4] Method definitions should have the following form: + +```cpp +void goalCelebration() { + ... +} + +``` + +#### [4.1.5] The if-else class of statements should have the following form: + +```cpp +if (isScorer) { + scoreGoal(); +} + +if (isScorer) { + scoreGoal(); +} else { + saucerPass(); +} + +if (isScorer) { + scoreGoal(); +} else if (isPlaymaker) { + saucerPass(); +} else { + startFight(); +} + +``` + +#### [4.1.6] A for statement should have the following form: + +```cpp +for (int i = 0; i < GRETZKY_NUMBER; i++) { + getActivePlayerWithNumber(i); +} + +``` + +#### [4.1.7] A while statement should have the following form: + +```cpp +while (!whistle) { + keepPlaying(); +} + +``` + +#### [4.1.8] A do-while statement should have the following form: + +```cpp +do { + skate(); +} while (!tired); + +``` + +#### [4.1.9] Switch/Case Statements: + +A switch statements should follow the following basic formatting rules: + +- The case statements are indented one indent (4 spaces) from the switch. +- The code for each case should be indented one indent (4 spaces) from the case statement. +- Each separate case should have a break statement, unless it is explicitly intended for the case to fall through to the subsequent cases. In the event that a case statement executes some code, then falls through to the next case, you must include an explicit comment noting that this is intentional. +- Break statements should be aligned with the code of the case, e.g. indented 4 spaces from the case statement. +- In the event that brackets are required to create local scope, the open bracket should appear on the same line as the case, and the close bracket should appear on the line immediately following the break aligned with the case statement. + +Examples of acceptable form are: + +```cpp +switch (foo) { + case BAR: + doBar(); + break; + + // notice brackets below follow the standard bracket placement for other control structures + case SPAM: { + int spam = 0; + doSomethingElse(spam); + break; + } + + case SPAZZ: + case BAZZ: + doSomething(); + // fall through to next case + + case RAZZ: + default: + doSomethingElseEntirely(); + break; +} + +// or in cases where returns occur at each case, this form is also accpetable +switch (jerseyNumber) { + case 87: + return crosby; + case 66: + return lemieux; + case 99: + return gretzky; + default: + return NULL; +} + +``` + +#### [4.1.10] A try-catch statement should have the following form: + +```cpp +try { + tradePlayer(); +} catch (const NoTradeClauseException& exception) { + negotiateNoTradeClause(); +} + +``` + +#### [4.1.11] Single statement if-else, for or while statements must be written with braces. + +```cpp +// GOOD: +for (int i = 0; i < numItems; i++) { + item[i].manipulate(); +} + +// BAD: braces are missing +for (int i = 0; i < numItems; i++) + item[i].manipulate(); +``` + +### [4.2] White space + +#### [4.2.1] Conventional operators should be surrounded by a space character, except in cases like mathematical expressions where it is easier to visually parse when spaces are used to enhance the grouping. + +```cpp +potential = (age + skill) * injuryChance; +// NOT: potential = (age+skill)*injuryChance; + +// Assignment operators always have spaces around them. +x = 0; + +// Other binary operators usually have spaces around them, but it's +// OK to remove spaces around factors. Parentheses should have no +// internal padding. +v = w * x + y / z; +v = w*x + y/z; +v = w * (x + z); + +``` + +#### [4.2.2] C++ reserved words should be followed by a white space. + +```cpp +setLine(leftWing, center, rightWing, leftDefense, rightDefense); +// NOT: setLine(leftWing,center,rightWing,leftDefense,rightDefense); + +``` + +#### [4.2.3] Semicolons in for statments should be followed by a space character. + +```cpp +for (i = 0; i < 10; i++) { // NOT: for(i=0;i<10;i++){ + +``` + +#### [4.2.4] Declaring and Calling Functions + +- Function names should not be followed by a white space. +- And there should be no space between the open parenthesis and the first parameter, and no space between the last parameter and the close parenthesis. + +Examples: + +```cpp +setCaptain(ovechkin); +// NOT: setCaptain (ovechkin); +// NOT: doSomething( int foo, float bar ); + +``` + +#### [4.2.6] Logical units within a block should be separated by one blank line. + +```cpp +Team penguins = new Team(); + +Player crosby = new Player(); +Player fleury = new Player(); + +penguins.setCaptain(crosby); +penguins.setGoalie(fleury); + +penguins.hireCoach(); + +``` + +#### [4.2.6] Avoid adding optional spaces across multi-line statements and adjacent statements. + +Avoid the following: + +``` +oddsToWin = (averageAge * veteranWeight) + + (numStarPlayers * starPlayerWeight) + + (goalieOverall * goalieWeight); + +theGreatOneSlapShotSpeed = computeShot(stickFlex, chara); +charaSlapShotSpeed = computeShot(stickFlex, weber); + +``` + +A change to the length of a variable in these sections causes unnecessary changes to the other lines. + +#### [4.2.7] Multi-line statements must have all n+1 lines indented at least one level (four spaces). + +Align all n+2 lines with the indentation of the n+1 line. + +When the multiple lines are bound by parentheses (as in arguments to a function call), the prefered style has no whitespace after the opening parenthesis or before the closing parenthesis. The n+1 lines are generally indented to the column immediately after the opening parenthesis (following the style for split expressions in 2.1.6). + +When the multiple lines are bound by braces (as in C++ initializers or JavaScript object notation), the preferred style has a newline after the opening brace and newline before the closing brace. The final line should not end in a comma, and no line should begin with a comma. The closing brace should begin in the same colum as the line that has the opening brace (following the style for split control statements in 4.1). + +Expressions, including C++ initializers and JavaScript object notation literals, can be placed on a single line if they are not deeply nested and end well within the column limit (2.1.4). + +The following are all acceptable: + +```cpp +shootOnNet(puckVelocity, + playerStrength, + randomChance); + +shootOnNet(puckVelocty, + playerStrength, + randomChance); + +if (longBooleanThatHasToDoWithHockey + && anotherBooleanOnANewLine); + +isGoodGoal = playerSlapShotVelocity > 100 + ? true + : false; + +var foo = { + spam: 1.0, + bar: "bar", + complex: { + red: 1, + white: 'blue' + }, + blah: zed +}; + +aJavascriptFunctionOfTwoFunctions(function (entity) { + print(entity); + foo(entity, 3); +}, function (entity) { + print('in second function'); + bar(entity, 4); +}); + +aCPlusPlusFunctionOfTwoLambdas([](gpu::Batch& batch) { + batch.setFramebuffer(nullptr); +}, [this](int count, float amount) { + frob(count, amount); +}); + +``` + +### [4.3] Comments + +#### [4.3.1] All comments should be written in English + +In an international environment English is the preferred language. + +#### [4.3.2] Use // for all comments, including multi-line comments. + +An exception to this rule applies for jsdoc or Doxygen comments. + +```cpp +// Comment spanning +// more than one line. + +``` + +There should be a space between the "//" and the actual comment + +#### [4.3.3] Comments should be included relative to their position in the code + +```cpp +while (true) { + // crosby is always injured + crosbyInjury(); +} + +// NOT: +// crosby is always injured +while (true) { + crosbyInjury(); +} + +``` + +#### [4.3.4] Source files (header and implementation) must include a boilerplate. + +Boilerplates should include the filename, location, creator, copyright, and Apache 2.0 License information and be placed at the top of the file. + +```cpp +// +// NodeList.h +// libraries/shared/src +// +// Created by Stephen Birarda on 2/15/13. +// Copyright 2013 High Fidelity, Inc. +// +// This is where you could place an optional one line comment about the file. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +``` + +#### [4.3.5] Never include Horizontal "line break" style comment blocks + +These types of comments are explicitly not allowed. If you need to break up sections of code, just leave an extra blank line. + +```cpp +////////////////////////////////////////////////////////////////////////////////// + +/********************************************************************************/ + +//-------------------------------------------------------------------------------- +``` + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4654c311cc..f9a54f1adc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ Contributing git checkout -b new_branch_name ``` 4. Code - * Follow the [coding standard](https://docs.highfidelity.com/build-guide/coding-standards) + * Follow the [coding standard](CODING_STANDARD.md) 5. Commit * Use [well formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 6. Update your branch diff --git a/assignment-client/src/AssignmentClientApp.cpp b/assignment-client/src/AssignmentClientApp.cpp index acfbb8571c..a87200dc5b 100644 --- a/assignment-client/src/AssignmentClientApp.cpp +++ b/assignment-client/src/AssignmentClientApp.cpp @@ -64,6 +64,10 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : "UDP port for this assignment client (or monitor)", "port"); parser.addOption(portOption); + const QCommandLineOption minChildListenPort(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION, + "Minimum UDP listen port", "port"); + parser.addOption(minChildListenPort); + const QCommandLineOption walletDestinationOption(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION, "set wallet destination", "wallet-uuid"); parser.addOption(walletDestinationOption); @@ -195,6 +199,11 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : assignmentServerPort = parser.value(assignmentServerPortOption).toInt(); } + quint16 childMinListenPort = 0; + if (argumentVariantMap.contains(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION)) { + childMinListenPort = argumentVariantMap.value(ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION).toUInt(); + } + // check for an overidden listen port quint16 listenPort = 0; if (argumentVariantMap.contains(ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION)) { @@ -234,8 +243,8 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) : if (numForks || minForks || maxForks) { AssignmentClientMonitor* monitor = new AssignmentClientMonitor(numForks, minForks, maxForks, - requestAssignmentType, assignmentPool, - listenPort, walletUUID, assignmentServerHostname, + requestAssignmentType, assignmentPool, listenPort, + childMinListenPort, walletUUID, assignmentServerHostname, assignmentServerPort, httpStatusPort, logDirectory); monitor->setParent(this); connect(this, &QCoreApplication::aboutToQuit, monitor, &AssignmentClientMonitor::aboutToQuit); diff --git a/assignment-client/src/AssignmentClientApp.h b/assignment-client/src/AssignmentClientApp.h index 37d3b9cc1d..1b50922980 100644 --- a/assignment-client/src/AssignmentClientApp.h +++ b/assignment-client/src/AssignmentClientApp.h @@ -20,6 +20,7 @@ const QString ASSIGNMENT_POOL_OPTION = "pool"; const QString ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION = "p"; const QString ASSIGNMENT_WALLET_DESTINATION_ID_OPTION = "wallet"; const QString CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION = "a"; +const QString ASSIGNMENT_MONITOR_MIN_CHILDREN_LISTEN_PORT_OPTION = "min-listen-port"; const QString CUSTOM_ASSIGNMENT_SERVER_PORT_OPTION = "server-port"; const QString ASSIGNMENT_NUM_FORKS_OPTION = "n"; const QString ASSIGNMENT_MIN_FORKS_OPTION = "min"; diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index fefed6e143..6601be849f 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -40,7 +40,7 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen const unsigned int minAssignmentClientForks, const unsigned int maxAssignmentClientForks, Assignment::Type requestAssignmentType, QString assignmentPool, - quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname, + quint16 listenPort, quint16 childMinListenPort, QUuid walletUUID, QString assignmentServerHostname, quint16 assignmentServerPort, quint16 httpStatusServerPort, QString logDirectory) : _httpManager(QHostAddress::LocalHost, httpStatusServerPort, "", this), _numAssignmentClientForks(numAssignmentClientForks), @@ -50,8 +50,8 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen _assignmentPool(assignmentPool), _walletUUID(walletUUID), _assignmentServerHostname(assignmentServerHostname), - _assignmentServerPort(assignmentServerPort) - + _assignmentServerPort(assignmentServerPort), + _childMinListenPort(childMinListenPort) { qDebug() << "_requestAssignmentType =" << _requestAssignmentType; @@ -100,8 +100,13 @@ void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) { } } -void AssignmentClientMonitor::childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus) { - auto message = "Child process " + QString::number(pid) + " has %1 with exit code " + QString::number(exitCode) + "."; +void AssignmentClientMonitor::childProcessFinished(qint64 pid, quint16 listenPort, int exitCode, QProcess::ExitStatus exitStatus) { + auto message = "Child process " + QString::number(pid) + " on port " + QString::number(listenPort) + + "has %1 with exit code " + QString::number(exitCode) + "."; + + if (listenPort) { + _childListenPorts.remove(listenPort); + } if (_childProcesses.remove(pid)) { message.append(" Removed from internal map."); @@ -153,6 +158,23 @@ void AssignmentClientMonitor::aboutToQuit() { void AssignmentClientMonitor::spawnChildClient() { QProcess* assignmentClient = new QProcess(this); + quint16 listenPort = 0; + // allocate a port + + if (_childMinListenPort) { + for (listenPort = _childMinListenPort; _childListenPorts.contains(listenPort); listenPort++) { + if (_maxAssignmentClientForks && + (listenPort >= _maxAssignmentClientForks + _childMinListenPort)) { + listenPort = 0; + qDebug() << "Insufficient listen ports"; + break; + } + } + } + if (listenPort) { + _childListenPorts.insert(listenPort); + } + // unparse the parts of the command-line that the child cares about QStringList _childArguments; if (_assignmentPool != "") { @@ -176,6 +198,11 @@ void AssignmentClientMonitor::spawnChildClient() { _childArguments.append(QString::number(_requestAssignmentType)); } + if (listenPort) { + _childArguments.append("-" + ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION); + _childArguments.append(QString::number(listenPort)); + } + // tell children which assignment monitor port to use // for now they simply talk to us on localhost _childArguments.append("--" + ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION); @@ -247,8 +274,8 @@ void AssignmentClientMonitor::spawnChildClient() { auto pid = assignmentClient->processId(); // make sure we hear that this process has finished when it does connect(assignmentClient, static_cast(&QProcess::finished), - this, [this, pid](int exitCode, QProcess::ExitStatus exitStatus) { - childProcessFinished(pid, exitCode, exitStatus); + this, [this, listenPort, pid](int exitCode, QProcess::ExitStatus exitStatus) { + childProcessFinished(pid, listenPort, exitCode, exitStatus); }); qDebug() << "Spawned a child client with PID" << assignmentClient->processId(); diff --git a/assignment-client/src/AssignmentClientMonitor.h b/assignment-client/src/AssignmentClientMonitor.h index 5e32c50e0d..f5355476b7 100644 --- a/assignment-client/src/AssignmentClientMonitor.h +++ b/assignment-client/src/AssignmentClientMonitor.h @@ -37,14 +37,15 @@ class AssignmentClientMonitor : public QObject, public HTTPRequestHandler { public: AssignmentClientMonitor(const unsigned int numAssignmentClientForks, const unsigned int minAssignmentClientForks, const unsigned int maxAssignmentClientForks, Assignment::Type requestAssignmentType, - QString assignmentPool, quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname, - quint16 assignmentServerPort, quint16 httpStatusServerPort, QString logDirectory); + QString assignmentPool, quint16 listenPort, quint16 childMinListenPort, QUuid walletUUID, + QString assignmentServerHostname, quint16 assignmentServerPort, quint16 httpStatusServerPort, + QString logDirectory); ~AssignmentClientMonitor(); void stopChildProcesses(); private slots: void checkSpares(); - void childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus); + void childProcessFinished(qint64 pid, quint16 port, int exitCode, QProcess::ExitStatus exitStatus); void handleChildStatusPacket(QSharedPointer message); bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; @@ -75,6 +76,9 @@ private: QMap _childProcesses; + quint16 _childMinListenPort; + QSet _childListenPorts; + bool _wantsChildFileLogging { false }; }; diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index c2aec9b058..502cf15aa2 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -107,6 +107,10 @@ BakeVersion currentBakeVersionForAssetType(BakedAssetType type) { } } +QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativeFilePath) { + return AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; +} + const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server"; void AssetServer::bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) { @@ -141,26 +145,27 @@ std::pair AssetServer::getAssetStatus(const A return { AssetUtils::Baked, "" }; } - auto dotIndex = path.lastIndexOf("."); - if (dotIndex == -1) { + BakedAssetType type = assetTypeForFilename(path); + if (type == BakedAssetType::Undefined) { return { AssetUtils::Irrelevant, "" }; } - auto extension = path.mid(dotIndex + 1); + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(hash); - QString bakedFilename; - - if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) { - bakedFilename = BAKED_MODEL_SIMPLE_NAME; - } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) { - bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; - } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) { - bakedFilename = BAKED_SCRIPT_SIMPLE_NAME; - } else { + // We create a meta file for Skyboxes at runtime when they get requested + // Otherwise, textures don't get baked by themselves. + if (type == BakedAssetType::Texture && !loaded) { return { AssetUtils::Irrelevant, "" }; } - auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename; + QString bakedFilename = bakedFilenameForAssetType(type); + auto bakedPath = getBakeMapping(hash, bakedFilename); + if (loaded && !meta.redirectTarget.isEmpty()) { + bakedPath = meta.redirectTarget; + } + auto jt = _fileMappings.find(bakedPath); if (jt != _fileMappings.end()) { if (jt->second == hash) { @@ -168,14 +173,8 @@ std::pair AssetServer::getAssetStatus(const A } else { return { AssetUtils::Baked, "" }; } - } else { - bool loaded; - AssetMeta meta; - - std::tie(loaded, meta) = readMetaFile(hash); - if (loaded && meta.failedLastBake) { - return { AssetUtils::Error, meta.lastBakeErrors }; - } + } else if (loaded && meta.failedLastBake) { + return { AssetUtils::Error, meta.lastBakeErrors }; } return { AssetUtils::Pending, "" }; @@ -227,8 +226,16 @@ bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetU return false; } + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(assetHash); + QString bakedFilename = bakedFilenameForAssetType(type); - auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename; + auto bakedPath = getBakeMapping(assetHash, bakedFilename); + if (loaded && !meta.redirectTarget.isEmpty()) { + bakedPath = meta.redirectTarget; + } + auto mappingIt = _fileMappings.find(bakedPath); bool bakedMappingExists = mappingIt != _fileMappings.end(); @@ -238,10 +245,8 @@ bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetU return false; } - bool loaded; - AssetMeta meta; - std::tie(loaded, meta) = readMetaFile(assetHash); - + // We create a meta file for Skyboxes at runtime when they get requested + // Otherwise, textures don't get baked by themselves. if (type == BakedAssetType::Texture && !loaded) { return false; } @@ -633,36 +638,33 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi if (it != _fileMappings.end()) { // check if we should re-direct to a baked asset - - // first, figure out from the mapping extension what type of file this is - auto assetPathExtension = assetPath.mid(assetPath.lastIndexOf('.') + 1).toLower(); - - auto type = assetTypeForFilename(assetPath); - QString bakedRootFile = bakedFilenameForAssetType(type); - auto originalAssetHash = it->second; QString redirectedAssetHash; - QString bakedAssetPath; quint8 wasRedirected = false; bool bakingDisabled = false; - if (!bakedRootFile.isEmpty()) { - // we ran into an asset for which we could have a baked version, let's check if it's ready - bakedAssetPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile; - auto bakedIt = _fileMappings.find(bakedAssetPath); + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(originalAssetHash); - if (bakedIt != _fileMappings.end()) { - if (bakedIt->second != originalAssetHash) { - qDebug() << "Did find baked version for: " << originalAssetHash << assetPath; - // we found a baked version of the requested asset to serve, redirect to that - redirectedAssetHash = bakedIt->second; - wasRedirected = true; - } else { - qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath << " (disabled)"; - bakingDisabled = true; - } + auto type = assetTypeForFilename(assetPath); + QString bakedRootFile = bakedFilenameForAssetType(type); + QString bakedAssetPath = getBakeMapping(originalAssetHash, bakedRootFile); + + if (loaded && !meta.redirectTarget.isEmpty()) { + bakedAssetPath = meta.redirectTarget; + } + + auto bakedIt = _fileMappings.find(bakedAssetPath); + if (bakedIt != _fileMappings.end()) { + if (bakedIt->second != originalAssetHash) { + qDebug() << "Did find baked version for: " << originalAssetHash << assetPath; + // we found a baked version of the requested asset to serve, redirect to that + redirectedAssetHash = bakedIt->second; + wasRedirected = true; } else { - qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath; + qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath << " (disabled)"; + bakingDisabled = true; } } @@ -684,20 +686,13 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi auto query = QUrlQuery(url.query()); bool isSkybox = query.hasQueryItem("skybox"); - if (isSkybox) { - bool loaded; - AssetMeta meta; - std::tie(loaded, meta) = readMetaFile(originalAssetHash); - - if (!loaded) { - AssetMeta needsBakingMeta; - needsBakingMeta.bakeVersion = NEEDS_BAKING_BAKE_VERSION; - - writeMetaFile(originalAssetHash, needsBakingMeta); - if (!bakingDisabled) { - maybeBake(assetPath, originalAssetHash); - } + if (isSkybox && !loaded) { + AssetMeta needsBakingMeta; + needsBakingMeta.bakeVersion = NEEDS_BAKING_BAKE_VERSION; + writeMetaFile(originalAssetHash, needsBakingMeta); + if (!bakingDisabled) { + maybeBake(assetPath, originalAssetHash); } } } @@ -1297,14 +1292,6 @@ bool AssetServer::renameMapping(AssetUtils::AssetPath oldPath, AssetUtils::Asset } } -static const QString BAKED_ASSET_SIMPLE_FBX_NAME = "asset.fbx"; -static const QString BAKED_ASSET_SIMPLE_TEXTURE_NAME = "texture.ktx"; -static const QString BAKED_ASSET_SIMPLE_JS_NAME = "asset.js"; - -QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativeFilePath) { - return AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; -} - void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, QString errors) { qDebug() << "Failed to bake: " << originalAssetHash << assetPath << "(" << errors << ")"; @@ -1326,12 +1313,78 @@ void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, } void AssetServer::handleCompletedBake(QString originalAssetHash, QString originalAssetPath, - QString bakedTempOutputDir, QVector bakedFilePaths) { + QString bakedTempOutputDir) { + auto reportCompletion = [this, originalAssetPath, originalAssetHash](bool errorCompletingBake, + QString errorReason, + QString redirectTarget) { + auto type = assetTypeForFilename(originalAssetPath); + auto currentTypeVersion = currentBakeVersionForAssetType(type); + + AssetMeta meta; + meta.bakeVersion = currentTypeVersion; + meta.failedLastBake = errorCompletingBake; + meta.redirectTarget = redirectTarget; + + if (errorCompletingBake) { + qWarning() << "Could not complete bake for" << originalAssetHash; + meta.lastBakeErrors = errorReason; + } + + writeMetaFile(originalAssetHash, meta); + + _pendingBakes.remove(originalAssetHash); + }; + bool errorCompletingBake { false }; QString errorReason; + QString redirectTarget; qDebug() << "Completing bake for " << originalAssetHash; + // Find the directory containing the baked content + QDir outputDir(bakedTempOutputDir); + QString outputDirName = outputDir.dirName(); + auto directories = outputDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + QString bakedDirectoryPath; + for (const auto& dirName : directories) { + outputDir.cd(dirName); + if (outputDir.exists("baked") && outputDir.exists("original")) { + bakedDirectoryPath = outputDir.filePath("baked"); + break; + } + outputDir.cdUp(); + } + if (bakedDirectoryPath.isEmpty()) { + errorCompletingBake = true; + errorReason = "Failed to find baking output"; + + // Cleanup temporary output directory + PathUtils::deleteMyTemporaryDir(outputDirName); + reportCompletion(errorCompletingBake, errorReason, redirectTarget); + return; + } + + // Compile list of all the baked files + QDirIterator it(bakedDirectoryPath, QDirIterator::Subdirectories); + QVector bakedFilePaths; + while (it.hasNext()) { + it.next(); + if (it.fileInfo().isFile()) { + bakedFilePaths.push_back(it.filePath()); + } + } + if (bakedFilePaths.isEmpty()) { + errorCompletingBake = true; + errorReason = "Baking output has no files"; + + // Cleanup temporary output directory + PathUtils::deleteMyTemporaryDir(outputDirName); + reportCompletion(errorCompletingBake, errorReason, redirectTarget); + return; + } + + QDir bakedDirectory(bakedDirectoryPath); + for (auto& filePath : bakedFilePaths) { // figure out the hash for the contents of this file QFile file(filePath); @@ -1340,89 +1393,72 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina AssetUtils::AssetHash bakedFileHash; - if (file.open(QIODevice::ReadOnly)) { - QCryptographicHash hasher(QCryptographicHash::Sha256); - - if (hasher.addData(&file)) { - bakedFileHash = hasher.result().toHex(); - } else { - // stop handling this bake, couldn't hash the contents of the file - errorCompletingBake = true; - errorReason = "Failed to finalize bake"; - break; - } - - // first check that we don't already have this bake file in our list - auto bakeFileDestination = _filesDirectory.absoluteFilePath(bakedFileHash); - if (!QFile::exists(bakeFileDestination)) { - // copy each to our files folder (with the hash as their filename) - if (!file.copy(_filesDirectory.absoluteFilePath(bakedFileHash))) { - // stop handling this bake, couldn't copy the bake file into our files directory - errorCompletingBake = true; - errorReason = "Failed to copy baked assets to asset server"; - break; - } - } - - // setup the mapping for this bake file - auto relativeFilePath = QUrl(filePath).fileName(); - qDebug() << "Relative file path is: " << relativeFilePath; - if (relativeFilePath.endsWith(".fbx", Qt::CaseInsensitive)) { - // for an FBX file, we replace the filename with the simple name - // (to handle the case where two mapped assets have the same hash but different names) - relativeFilePath = BAKED_ASSET_SIMPLE_FBX_NAME; - } else if (relativeFilePath.endsWith(".js", Qt::CaseInsensitive)) { - relativeFilePath = BAKED_ASSET_SIMPLE_JS_NAME; - } else if (!originalAssetPath.endsWith(".fbx", Qt::CaseInsensitive)) { - relativeFilePath = BAKED_ASSET_SIMPLE_TEXTURE_NAME; - } - - QString bakeMapping = getBakeMapping(originalAssetHash, relativeFilePath); - - // add a mapping (under the hidden baked folder) for this file resulting from the bake - if (setMapping(bakeMapping, bakedFileHash)) { - qDebug() << "Added" << bakeMapping << "for bake file" << bakedFileHash << "from bake of" << originalAssetHash; - } else { - qDebug() << "Failed to set mapping"; - // stop handling this bake, couldn't add a mapping for this bake file - errorCompletingBake = true; - errorReason = "Failed to finalize bake"; - break; - } - } else { + if (!file.open(QIODevice::ReadOnly)) { qDebug() << "Failed to open baked file: " << filePath; // stop handling this bake, we couldn't open one of the files for reading errorCompletingBake = true; - errorReason = "Failed to finalize bake"; + errorReason = "Could not open baked file " + file.fileName(); break; } - } - for (auto& filePath : bakedFilePaths) { - QFile file(filePath); - if (!file.remove()) { - qWarning() << "Failed to remove temporary file:" << filePath; + QCryptographicHash hasher(QCryptographicHash::Sha256); + + if (!hasher.addData(&file)) { + // stop handling this bake, couldn't hash the contents of the file + errorCompletingBake = true; + errorReason = "Could not hash data for " + file.fileName(); + break; } - } - if (!QDir(bakedTempOutputDir).rmdir(".")) { - qWarning() << "Failed to remove temporary directory:" << bakedTempOutputDir; + + bakedFileHash = hasher.result().toHex(); + + // first check that we don't already have this bake file in our list + auto bakeFileDestination = _filesDirectory.absoluteFilePath(bakedFileHash); + if (!QFile::exists(bakeFileDestination)) { + // copy each to our files folder (with the hash as their filename) + if (!file.copy(_filesDirectory.absoluteFilePath(bakedFileHash))) { + // stop handling this bake, couldn't copy the bake file into our files directory + errorCompletingBake = true; + errorReason = "Failed to copy baked assets to asset server"; + break; + } + } + + // setup the mapping for this bake file + auto relativeFilePath = bakedDirectory.relativeFilePath(filePath); + + QString bakeMapping = getBakeMapping(originalAssetHash, relativeFilePath); + + // Check if this is the file we should redirect to when someone asks for the original asset + if ((relativeFilePath.endsWith(".baked.fst", Qt::CaseInsensitive) && originalAssetPath.endsWith(".fbx")) || + (relativeFilePath.endsWith(".texmeta.json", Qt::CaseInsensitive) && !originalAssetPath.endsWith(".fbx"))) { + if (!redirectTarget.isEmpty()) { + qWarning() << "Found multiple baked redirect target for" << originalAssetPath; + } + redirectTarget = bakeMapping; + } + + // add a mapping (under the hidden baked folder) for this file resulting from the bake + if (!setMapping(bakeMapping, bakedFileHash)) { + qDebug() << "Failed to set mapping"; + // stop handling this bake, couldn't add a mapping for this bake file + errorCompletingBake = true; + errorReason = "Failed to set mapping for baked file " + file.fileName(); + break; + } + + qDebug() << "Added" << bakeMapping << "for bake file" << bakedFileHash << "from bake of" << originalAssetHash; } - auto type = assetTypeForFilename(originalAssetPath); - auto currentTypeVersion = currentBakeVersionForAssetType(type); - AssetMeta meta; - meta.bakeVersion = currentTypeVersion; - meta.failedLastBake = errorCompletingBake; - - if (errorCompletingBake) { - qWarning() << "Could not complete bake for" << originalAssetHash; - meta.lastBakeErrors = errorReason; + if (redirectTarget.isEmpty()) { + errorCompletingBake = true; + errorReason = "Could not find root file for baked output"; } - writeMetaFile(originalAssetHash, meta); - - _pendingBakes.remove(originalAssetHash); + // Cleanup temporary output directory + PathUtils::deleteMyTemporaryDir(outputDirName); + reportCompletion(errorCompletingBake, errorReason, redirectTarget); } void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath) { @@ -1435,6 +1471,7 @@ void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath static const QString BAKE_VERSION_KEY = "bake_version"; static const QString FAILED_LAST_BAKE_KEY = "failed_last_bake"; static const QString LAST_BAKE_ERRORS_KEY = "last_bake_errors"; +static const QString REDIRECT_TARGET_KEY = "redirect_target"; std::pair AssetServer::readMetaFile(AssetUtils::AssetHash hash) { auto metaFilePath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json"; @@ -1461,6 +1498,7 @@ std::pair AssetServer::readMetaFile(AssetUtils::AssetHash hash) auto bakeVersion = root[BAKE_VERSION_KEY]; auto failedLastBake = root[FAILED_LAST_BAKE_KEY]; auto lastBakeErrors = root[LAST_BAKE_ERRORS_KEY]; + auto redirectTarget = root[REDIRECT_TARGET_KEY]; if (bakeVersion.isDouble() && failedLastBake.isBool() @@ -1470,6 +1508,7 @@ std::pair AssetServer::readMetaFile(AssetUtils::AssetHash hash) meta.bakeVersion = bakeVersion.toInt(); meta.failedLastBake = failedLastBake.toBool(); meta.lastBakeErrors = lastBakeErrors.toString(); + meta.redirectTarget = redirectTarget.toString(); return { true, meta }; } else { @@ -1488,6 +1527,7 @@ bool AssetServer::writeMetaFile(AssetUtils::AssetHash originalAssetHash, const A metaFileObject[BAKE_VERSION_KEY] = (int)meta.bakeVersion; metaFileObject[FAILED_LAST_BAKE_KEY] = meta.failedLastBake; metaFileObject[LAST_BAKE_ERRORS_KEY] = meta.lastBakeErrors; + metaFileObject[REDIRECT_TARGET_KEY] = meta.redirectTarget; QJsonDocument metaFileDoc; metaFileDoc.setObject(metaFileObject); @@ -1521,10 +1561,18 @@ bool AssetServer::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool if (type == BakedAssetType::Undefined) { continue; } - QString bakedFilename = bakedFilenameForAssetType(type); auto hash = it->second; + + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(hash); + + QString bakedFilename = bakedFilenameForAssetType(type); auto bakedMapping = getBakeMapping(hash, bakedFilename); + if (loaded && !meta.redirectTarget.isEmpty()) { + bakedMapping = meta.redirectTarget; + } auto it = _fileMappings.find(bakedMapping); bool currentlyDisabled = (it != _fileMappings.end() && it->second == hash); diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index b3d0f18a8f..fe84df5141 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -62,12 +62,10 @@ enum class ScriptBakeVersion : BakeVersion { }; struct AssetMeta { - AssetMeta() { - } - BakeVersion bakeVersion { INITIAL_BAKE_VERSION }; bool failedLastBake { false }; QString lastBakeErrors; + QString redirectTarget; }; class BakeAssetTask; @@ -139,8 +137,7 @@ private: void bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath); /// Move baked content for asset to baked directory and update baked status - void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir, - QVector bakedFilePaths); + void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir); void handleFailedBake(QString originalAssetHash, QString assetPath, QString errors); void handleAbortedBake(QString originalAssetHash, QString assetPath); diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp index ecb4ede5d8..7c845f9b38 100644 --- a/assignment-client/src/assets/BakeAssetTask.cpp +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -36,33 +36,38 @@ BakeAssetTask::BakeAssetTask(const AssetUtils::AssetHash& assetHash, const Asset }); } -void cleanupTempFiles(QString tempOutputDir, std::vector files) { - for (const auto& filename : files) { - QFile f { filename }; - if (!f.remove()) { - qDebug() << "Failed to remove:" << filename; - } - } - if (!tempOutputDir.isEmpty()) { - QDir dir { tempOutputDir }; - if (!dir.rmdir(".")) { - qDebug() << "Failed to remove temporary directory:" << tempOutputDir; - } - } -}; - void BakeAssetTask::run() { if (_isBaking.exchange(true)) { qWarning() << "Tried to start bake asset task while already baking"; return; } + // Make a new temporary directory for the Oven to work in QString tempOutputDir = PathUtils::generateTemporaryDir(); + QString tempOutputDirName = QDir(tempOutputDir).dirName(); + if (tempOutputDir.isEmpty()) { + QString errors = "Could not create temporary working directory"; + emit bakeFailed(_assetHash, _assetPath, errors); + PathUtils::deleteMyTemporaryDir(tempOutputDirName); + return; + } + + // Copy file to bake the temporary dir and give a name the oven can work with + auto assetName = _assetPath.split("/").last(); + auto tempAssetPath = tempOutputDir + "/" + assetName; + auto success = QFile::copy(_filePath, tempAssetPath); + if (!success) { + QString errors = "Couldn't copy file to bake to temporary directory"; + emit bakeFailed(_assetHash, _assetPath, errors); + PathUtils::deleteMyTemporaryDir(tempOutputDirName); + return; + } + auto base = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir(); QString path = base.absolutePath() + "/oven"; QString extension = _assetPath.mid(_assetPath.lastIndexOf('.') + 1); QStringList args { - "-i", _filePath, + "-i", tempAssetPath, "-o", tempOutputDir, "-t", extension, }; @@ -72,10 +77,11 @@ void BakeAssetTask::run() { QEventLoop loop; connect(_ovenProcess.get(), static_cast(&QProcess::finished), - this, [&loop, this, tempOutputDir](int exitCode, QProcess::ExitStatus exitStatus) { + this, [&loop, this, tempOutputDir, tempAssetPath, tempOutputDirName](int exitCode, QProcess::ExitStatus exitStatus) { qDebug() << "Baking process finished: " << exitCode << exitStatus; if (exitStatus == QProcess::CrashExit) { + PathUtils::deleteMyTemporaryDir(tempOutputDirName); if (_wasAborted) { emit bakeAborted(_assetHash, _assetPath); } else { @@ -83,16 +89,10 @@ void BakeAssetTask::run() { emit bakeFailed(_assetHash, _assetPath, errors); } } else if (exitCode == OVEN_STATUS_CODE_SUCCESS) { - QDir outputDir = tempOutputDir; - auto files = outputDir.entryInfoList(QDir::Files); - QVector outputFiles; - for (auto& file : files) { - outputFiles.push_back(file.absoluteFilePath()); - } - - emit bakeComplete(_assetHash, _assetPath, tempOutputDir, outputFiles); + emit bakeComplete(_assetHash, _assetPath, tempOutputDir); } else if (exitStatus == QProcess::NormalExit && exitCode == OVEN_STATUS_CODE_ABORT) { _wasAborted.store(true); + PathUtils::deleteMyTemporaryDir(tempOutputDirName); emit bakeAborted(_assetHash, _assetPath); } else { QString errors; @@ -107,6 +107,7 @@ void BakeAssetTask::run() { errors = "Unknown error occurred while baking"; } } + PathUtils::deleteMyTemporaryDir(tempOutputDirName); emit bakeFailed(_assetHash, _assetPath, errors); } @@ -115,7 +116,10 @@ void BakeAssetTask::run() { qDebug() << "Starting oven for " << _assetPath; _ovenProcess->start(path, args, QIODevice::ReadOnly); - if (!_ovenProcess->waitForStarted(-1)) { + qDebug() << "Running:" << path << args; + if (!_ovenProcess->waitForStarted()) { + PathUtils::deleteMyTemporaryDir(tempOutputDirName); + QString errors = "Oven process failed to start"; emit bakeFailed(_assetHash, _assetPath, errors); return; diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h index 24b070d08a..2d50a26bc1 100644 --- a/assignment-client/src/assets/BakeAssetTask.h +++ b/assignment-client/src/assets/BakeAssetTask.h @@ -37,7 +37,7 @@ public slots: void abort(); signals: - void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir, QVector outputFiles); + void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir); void bakeFailed(QString assetHash, QString assetPath, QString errors); void bakeAborted(QString assetHash, QString assetPath); diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index fd3a8f7c0c..eb487df850 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -588,8 +588,8 @@ void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) { // check the payload to see if we have asked for dynamicJitterBuffer support const QString DYNAMIC_JITTER_BUFFER_JSON_KEY = "dynamic_jitter_buffer"; bool enableDynamicJitterBuffer = audioBufferGroupObject[DYNAMIC_JITTER_BUFFER_JSON_KEY].toBool(); - if (enableDynamicJitterBuffer) { - qCDebug(audio) << "Enabling dynamic jitter buffers."; + if (!enableDynamicJitterBuffer) { + qCDebug(audio) << "Disabling dynamic jitter buffers."; bool ok; const QString DESIRED_JITTER_BUFFER_FRAMES_KEY = "static_desired_jitter_buffer_frames"; @@ -599,7 +599,7 @@ void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) { } qCDebug(audio) << "Static desired jitter buffer frames:" << _numStaticJitterFrames; } else { - qCDebug(audio) << "Disabling dynamic jitter buffers."; + qCDebug(audio) << "Enabling dynamic jitter buffers."; _numStaticJitterFrames = DISABLE_STATIC_JITTER_FRAMES; } diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index cb90df58e5..e5e9f89984 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -549,38 +549,28 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre // grab the stream from the ring buffer AudioRingBuffer::ConstIterator streamPopOutput = streamToAdd->getLastPopOutput(); - // stereo sources are not passed through HRTF if (streamToAdd->isStereo()) { - // apply the avatar gain adjustment - gain *= mixableStream.hrtf->getGainAdjustment(); + streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - const float scale = 1 / 32768.0f; // int16_t to float - - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) { - _mixSamples[2*i+0] += (float)streamPopOutput[2*i+0] * gain * scale; - _mixSamples[2*i+1] += (float)streamPopOutput[2*i+1] * gain * scale; - } + // stereo sources are not passed through HRTF + mixableStream.hrtf->mixStereo(_bufferSamples, _mixSamples, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); ++stats.manualStereoMixes; } else if (isEcho) { + + streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + // echo sources are not passed through HRTF - - const float scale = 1/32768.0f; // int16_t to float - - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) { - float sample = (float)streamPopOutput[i] * gain * scale; - _mixSamples[2*i+0] += sample; - _mixSamples[2*i+1] += sample; - } + mixableStream.hrtf->mixMono(_bufferSamples, _mixSamples, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); ++stats.manualEchoMixes; } else { + streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); mixableStream.hrtf->render(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - ++stats.hrtfRenders; } } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 1b86e0dff2..4be9f4f46f 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -155,6 +155,12 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode) { + // Trying to read more bytes than available, bail + if (message.getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitVersion))) { + qWarning() << "Refusing to process malformed traits packet from" << message.getSenderSockAddr(); + return; + } + // pull the trait version from the message AvatarTraits::TraitVersion packetTraitVersion; message.readPrimitive(&packetTraitVersion); @@ -164,10 +170,22 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, while (message.getBytesLeftToRead() > 0) { // for each trait in the packet, apply it if the trait version is newer than what we have + // Trying to read more bytes than available, bail + if (message.getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitType))) { + qWarning() << "Refusing to process malformed traits packet from" << message.getSenderSockAddr(); + return; + } + AvatarTraits::TraitType traitType; message.readPrimitive(&traitType); if (AvatarTraits::isSimpleTrait(traitType)) { + // Trying to read more bytes than available, bail + if (message.getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitWireSize))) { + qWarning() << "Refusing to process malformed traits packet from" << message.getSenderSockAddr(); + return; + } + AvatarTraits::TraitWireSize traitSize; message.readPrimitive(&traitSize); @@ -179,7 +197,6 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, if (packetTraitVersion > _lastReceivedTraitVersions[traitType]) { _avatar->processTrait(traitType, message.read(traitSize)); _lastReceivedTraitVersions[traitType] = packetTraitVersion; - if (traitType == AvatarTraits::SkeletonModelURL) { // special handling for skeleton model URL, since we need to make sure it is in the whitelist checkSkeletonURLAgainstWhitelist(slaveSharedData, sendingNode, packetTraitVersion); @@ -190,13 +207,15 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, message.seek(message.getPosition() + traitSize); } } else { - AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - - if (message.getBytesLeftToRead() == 0) { - qWarning() << "Received an instanced trait with no size from" << message.getSenderSockAddr(); - break; + // Trying to read more bytes than available, bail + if (message.getBytesLeftToRead() < qint64(NUM_BYTES_RFC4122_UUID + + sizeof(AvatarTraits::TraitWireSize))) { + qWarning() << "Refusing to process malformed traits packet from" << message.getSenderSockAddr(); + return; } + AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + AvatarTraits::TraitWireSize traitSize; message.readPrimitive(&traitSize); diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index e5df411099..54e9fa2fba 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -62,8 +62,8 @@ * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. * @property {string} skeletonModelURL - The avatar's FST file. - * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
- * Deprecated: Use avatar entities instead. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments. + *

Deprecated: This property is deprecated and will be removed. Use avatar entities instead.

* @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the diff --git a/assignment-client/src/octree/OctreeHeadlessViewer.h b/assignment-client/src/octree/OctreeHeadlessViewer.h index a2a49dceb8..67a81b1d2a 100644 --- a/assignment-client/src/octree/OctreeHeadlessViewer.h +++ b/assignment-client/src/octree/OctreeHeadlessViewer.h @@ -56,7 +56,8 @@ public slots: /**jsdoc * @function EntityViewer.setKeyholeRadius * @param {number} radius - * @deprecated Use {@link EntityViewer.setCenterRadius|setCenterRadius} instead. + * @deprecated This function is deprecated and will be removed. Use {@link EntityViewer.setCenterRadius|setCenterRadius} + * instead. */ void setKeyholeRadius(float radius) { _hasViewFrustum = true; _viewFrustum.setCenterRadius(radius); } // TODO: remove this legacy support diff --git a/cmake/macros/OptionalWinExecutableSigning.cmake b/cmake/macros/OptionalWinExecutableSigning.cmake index 069fc12fc5..cbefdaea8f 100644 --- a/cmake/macros/OptionalWinExecutableSigning.cmake +++ b/cmake/macros/OptionalWinExecutableSigning.cmake @@ -22,7 +22,7 @@ macro(optional_win_executable_signing) # setup a post build command to sign the executable add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${SIGNTOOL_EXECUTABLE} sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /td SHA256 ${EXECUTABLE_PATH} + COMMAND ${SIGNTOOL_EXECUTABLE} sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr ${TIMESERVER_URL} /td SHA256 ${EXECUTABLE_PATH} ) else () message(FATAL_ERROR "HF_PFX_PASSPHRASE must be set for executables to be signed.") diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index 1647da045f..cdfcc40eab 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -481,3 +481,15 @@ function prepareAccessTokenPrompt(callback) { swal.close(); }); } + +function getMetaverseUrl(callback) { + $.ajax('/api/metaverse_info', { + success: function(data) { + callback(data.metaverse_url); + }, + error: function() { + callback(URLs.METAVERSE_URL); + } + }); +} + diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 3ae05b447f..08d0550841 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -16,47 +16,55 @@ $(document).ready(function(){ Settings.extraGroupsAtEnd = Settings.extraDomainGroupsAtEnd; Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex; + var METAVERSE_URL = URLs.METAVERSE_URL; Settings.afterReloadActions = function() { - // call our method to setup the HF account button - setupHFAccountButton(); - // call our method to setup the place names table - setupPlacesTable(); + getMetaverseUrl(function(metaverse_url) { + METAVERSE_URL = metaverse_url; - setupDomainNetworkingSettings(); - // setupDomainLabelSetting(); + // call our method to setup the HF account button + setupHFAccountButton(); - setupSettingsBackup(); + // call our method to setup the place names table + setupPlacesTable(); - if (domainIDIsSet()) { - // now, ask the API for what places, if any, point to this domain - reloadDomainInfo(); + setupDomainNetworkingSettings(); + // setupDomainLabelSetting(); - // we need to ask the API what a shareable name for this domain is - getShareName(function(success, shareName) { - if (success) { - var shareLink = "https://hifi.place/" + shareName; - $('#visit-domain-link').attr("href", shareLink).show(); - } - }); - } + setupSettingsBackup(); - if (Settings.data.values.wizard.cloud_domain) { - $('#manage-cloud-domains-link').show(); + if (domainIDIsSet()) { + // now, ask the API for what places, if any, point to this domain + reloadDomainInfo(); - var cloudWizardExit = qs["cloud-wizard-exit"]; - if (cloudWizardExit != undefined) { - $('#cloud-domains-alert').show(); + // we need to ask the API what a shareable name for this domain is + getShareName(function(success, shareName) { + if (success) { + var shareLink = "https://hifi.place/" + shareName; + $('#visit-domain-link').attr("href", shareLink).show(); + } + }); + } else if (accessTokenIsSet()) { + $('#' + Settings.GET_TEMPORARY_NAME_BTN_ID).show(); } - $(Settings.DOMAIN_ID_SELECTOR).siblings('span').append("
Changing the domain ID for a Cloud Domain may result in an incorrect status for the domain on your Cloud Domains page."); - } else { - // append the domain selection modal - appendDomainIDButtons(); - } + if (Settings.data.values.wizard.cloud_domain) { + $('#manage-cloud-domains-link').show(); - handleAction(); + var cloudWizardExit = qs["cloud-wizard-exit"]; + if (cloudWizardExit != undefined) { + $('#cloud-domains-alert').show(); + } + + $(Settings.DOMAIN_ID_SELECTOR).siblings('span').append("
Changing the domain ID for a Cloud Domain may result in an incorrect status for the domain on your Cloud Domains page."); + } else { + // append the domain selection modal + appendDomainIDButtons(); + } + + handleAction(); + }); } Settings.handlePostSettings = function(formJSON) { @@ -258,7 +266,7 @@ $(document).ready(function(){ buttonSetting.button_label = "Connect High Fidelity Account"; buttonSetting.html_id = Settings.CONNECT_ACCOUNT_BTN_ID; - buttonSetting.href = URLs.METAVERSE_URL + "/user/tokens/new?for_domain_server=true"; + buttonSetting.href = METAVERSE_URL + "/user/tokens/new?for_domain_server=true"; // since we do not have an access token we change hide domain ID and auto networking settings // without an access token niether of them can do anything @@ -645,7 +653,7 @@ $(document).ready(function(){ label: 'Places', html_id: Settings.PLACES_TABLE_ID, help: "The following places currently point to this domain.
To point places to this domain, " - + " go to the My Places " + + " go to the My Places " + "page in your High Fidelity Metaverse account.", read_only: true, can_add_new_rows: false, @@ -952,7 +960,7 @@ $(document).ready(function(){ modal_buttons["success"] = { label: 'Create new domain', callback: function() { - window.open(URLs.METAVERSE_URL + "/user/domains", '_blank'); + window.open(METAVERSE_URL + "/user/domains", '_blank'); } } modal_body = "

You do not have any domains in your High Fidelity account." + @@ -1000,7 +1008,7 @@ $(document).ready(function(){ showSpinnerAlert('Creating temporary place name'); // make a get request to get a temporary domain - $.post(URLs.METAVERSE_URL + '/api/v1/domains/temporary', function(data){ + $.post(METAVERSE_URL + '/api/v1/domains/temporary', function(data){ if (data.status == "success") { var domain = data.data.domain; diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index e23d9e57a8..8c7beaa614 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -1010,7 +1010,7 @@ void DomainGatekeeper::refreshGroupsCache() { nodeList->eachNode([this](const SharedNodePointer& node) { if (!node->getPermissions().isAssignment) { // this node is an agent - const QString& verifiedUserName = node->getPermissions().getVerifiedUserName(); + QString verifiedUserName = node->getPermissions().getVerifiedUserName(); if (!verifiedUserName.isEmpty()) { getGroupMemberships(verifiedUserName); } diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 24d55d74b6..2540858742 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -115,7 +115,6 @@ void DomainMetadata::securityChanged(bool send) { auto& state = *static_cast(_metadata[DESCRIPTORS].data()); const QString RESTRICTION_OPEN = "open"; - const QString RESTRICTION_ANON = "anon"; const QString RESTRICTION_HIFI = "hifi"; const QString RESTRICTION_ACL = "acl"; @@ -127,7 +126,7 @@ void DomainMetadata::securityChanged(bool send) { bool hasHifiAccess = settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).can( NodePermissions::Permission::canConnectToDomain); if (hasAnonymousAccess) { - restriction = hasHifiAccess ? RESTRICTION_OPEN : RESTRICTION_ANON; + restriction = RESTRICTION_OPEN; } else if (hasHifiAccess) { restriction = RESTRICTION_HIFI; } else { diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 400dc3642d..f73c59dc32 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1916,6 +1916,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_SETTINGS = "/settings"; const QString URI_CONTENT_UPLOAD = "/content/upload"; const QString URI_RESTART = "/restart"; + const QString URI_API_METAVERSE_INFO = "/api/metaverse_info"; const QString URI_API_PLACES = "/api/places"; const QString URI_API_DOMAINS = "/api/domains"; const QString URI_API_DOMAINS_ID = "/api/domains/"; @@ -2164,6 +2165,15 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url } else if (url.path() == URI_RESTART) { connection->respond(HTTPConnection::StatusCode200); restart(); + return true; + } else if (url.path() == URI_API_METAVERSE_INFO) { + QJsonObject rootJSON { + { "metaverse_url", NetworkingConstants::METAVERSE_SERVER_URL().toString() } + }; + + QJsonDocument docJSON{ rootJSON }; + connectionPtr->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + return true; } else if (url.path() == URI_API_DOMAINS) { return forwardMetaverseAPIRequest(connection, "/api/v1/domains", ""); diff --git a/interface/resources/avatar/animations/idleWS.fbx b/interface/resources/avatar/animations/idleWS.fbx new file mode 100644 index 0000000000..e730165012 Binary files /dev/null and b/interface/resources/avatar/animations/idleWS.fbx differ diff --git a/interface/resources/avatar/animations/idleWS_all.fbx b/interface/resources/avatar/animations/idleWS_all.fbx new file mode 100644 index 0000000000..f9ac3dacfb Binary files /dev/null and b/interface/resources/avatar/animations/idleWS_all.fbx differ diff --git a/interface/resources/avatar/animations/idle_LFF_all.fbx b/interface/resources/avatar/animations/idle_LFF_all.fbx new file mode 100644 index 0000000000..6904773cd5 Binary files /dev/null and b/interface/resources/avatar/animations/idle_LFF_all.fbx differ diff --git a/interface/resources/avatar/animations/idle_RFF_all.fbx b/interface/resources/avatar/animations/idle_RFF_all.fbx new file mode 100644 index 0000000000..77ea06dc70 Binary files /dev/null and b/interface/resources/avatar/animations/idle_RFF_all.fbx differ diff --git a/interface/resources/avatar/animations/idle_lookaround01.fbx b/interface/resources/avatar/animations/idle_lookaround01.fbx new file mode 100644 index 0000000000..fbea065713 Binary files /dev/null and b/interface/resources/avatar/animations/idle_lookaround01.fbx differ diff --git a/interface/resources/avatar/animations/idle_once_armstretch.fbx b/interface/resources/avatar/animations/idle_once_armstretch.fbx new file mode 100644 index 0000000000..23eeed3b26 Binary files /dev/null and b/interface/resources/avatar/animations/idle_once_armstretch.fbx differ diff --git a/interface/resources/avatar/animations/idle_once_bigstretch.fbx b/interface/resources/avatar/animations/idle_once_bigstretch.fbx new file mode 100644 index 0000000000..5e4731279f Binary files /dev/null and b/interface/resources/avatar/animations/idle_once_bigstretch.fbx differ diff --git a/interface/resources/avatar/animations/idle_once_checkwatch.fbx b/interface/resources/avatar/animations/idle_once_checkwatch.fbx new file mode 100644 index 0000000000..888d0bcbfc Binary files /dev/null and b/interface/resources/avatar/animations/idle_once_checkwatch.fbx differ diff --git a/interface/resources/avatar/animations/idle_once_headtilt.fbx b/interface/resources/avatar/animations/idle_once_headtilt.fbx new file mode 100644 index 0000000000..21d1bc43c8 Binary files /dev/null and b/interface/resources/avatar/animations/idle_once_headtilt.fbx differ diff --git a/interface/resources/avatar/animations/idle_once_lookaround.fbx b/interface/resources/avatar/animations/idle_once_lookaround.fbx new file mode 100644 index 0000000000..15be33092c Binary files /dev/null and b/interface/resources/avatar/animations/idle_once_lookaround.fbx differ diff --git a/interface/resources/avatar/animations/idle_once_neckstretch.fbx b/interface/resources/avatar/animations/idle_once_neckstretch.fbx new file mode 100644 index 0000000000..3968b96615 Binary files /dev/null and b/interface/resources/avatar/animations/idle_once_neckstretch.fbx differ diff --git a/interface/resources/avatar/animations/idle_once_slownod.fbx b/interface/resources/avatar/animations/idle_once_slownod.fbx new file mode 100644 index 0000000000..ad4f4e17bf Binary files /dev/null and b/interface/resources/avatar/animations/idle_once_slownod.fbx differ diff --git a/interface/resources/avatar/animations/talk04.fbx b/interface/resources/avatar/animations/talk04.fbx new file mode 100644 index 0000000000..be2ba0a11f Binary files /dev/null and b/interface/resources/avatar/animations/talk04.fbx differ diff --git a/interface/resources/avatar/animations/talk_righthand.fbx b/interface/resources/avatar/animations/talk_righthand.fbx new file mode 100644 index 0000000000..df8849b4b4 Binary files /dev/null and b/interface/resources/avatar/animations/talk_righthand.fbx differ diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 27e45daa7b..738975bb8c 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -903,52 +903,557 @@ "children": [ { "id": "idle", - "type": "stateMachine", + "type": "overlay", "data": { - "currentState": "idleStand", - "states": [ - { - "id": "idleStand", - "interpTarget": 6, - "interpDuration": 10, - "transitions": [ - { "var": "isTalking", "state": "idleTalk" } - ] - }, - { - "id": "idleTalk", - "interpTarget": 6, - "interpDuration": 10, - "transitions": [ - { "var": "notIsTalking", "state": "idleStand" } - ] - } - ] + "alpha": 1.0, + "alphaVar": "idleOverlayAlpha", + "boneSet": "upperBody" }, "children": [ + { + "id": "idleTalk", + "type": "randomSwitchStateMachine", + "data": { + "currentState": "idleTalk1", + "triggerRandomSwitch": "idleTalkSwitch", + "randomSwitchTimeMin": 5.0, + "randomSwitchTimeMax": 10.0, + "states": [ + { + "id": "idleTalk1", + "interpTarget": 6, + "interpDuration": 15, + "priority": 0.33, + "resume": true, + "transitions": [] + }, + { + "id": "idleTalk2", + "interpTarget": 6, + "interpDuration": 15, + "priority": 0.33, + "resume": true, + "transitions": [] + }, + { + "id": "idleTalk3", + "interpTarget": 6, + "interpDuration": 15, + "priority": 0.33, + "resume": true, + "transitions": [] + } + ] + }, + "children": [ + { + "id": "idleTalk1", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/talk.fbx", + "startFrame": 1.0, + "endFrame": 800.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "idleTalk2", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/talk_righthand.fbx", + "startFrame": 1.0, + "endFrame": 501.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "idleTalk3", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/talk04.fbx", + "startFrame": 1.0, + "endFrame": 499.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, { "id": "idleStand", - "type": "clip", + "type": "randomSwitchStateMachine", "data": { - "url": "qrc:///avatar/animations/idle.fbx", - "startFrame": 0.0, - "endFrame": 300.0, - "timeScale": 1.0, - "loopFlag": true + "currentState": "masterIdle", + "triggerTimeMin": 10.0, + "triggerTimeMax": 60.0, + "transitionVar": "timeToFidget", + "states": [ + { + "id": "masterIdle", + "interpTarget": 21, + "interpDuration": 20, + "priority": 1.0, + "resume": false, + "transitions": [ + { "var": "timeToFidget", "randomSwitchState": "fidget" } + ] + }, + { + "id": "fidget", + "interpTarget": 21, + "interpDuration": 20, + "priority": -1.0, + "resume": false, + "transitions": [ + { "var": "movement1OnDone", "randomSwitchState": "masterIdle" }, + { "var": "movement2OnDone", "randomSwitchState": "masterIdle" }, + { "var": "movement3OnDone", "randomSwitchState": "masterIdle" }, + { "var": "movement4OnDone", "randomSwitchState": "masterIdle" }, + { "var": "movement5OnDone", "randomSwitchState": "masterIdle" }, + { "var": "movement6OnDone", "randomSwitchState": "masterIdle" }, + { "var": "movement7OnDone", "randomSwitchState": "masterIdle" }, + { "var": "movement8OnDone", "randomSwitchState": "masterIdle" }, + { "var": "alt1ToMasterIdleOnDone", "randomSwitchState": "masterIdle" }, + { "var": "alt2ToMasterIdleOnDone", "randomSwitchState": "masterIdle" } + ] + } + ] }, - "children": [] - }, - { - "id": "idleTalk", - "type": "clip", - "data": { - "url": "qrc:///avatar/animations/talk.fbx", - "startFrame": 0.0, - "endFrame": 800.0, - "timeScale": 1.0, - "loopFlag": true - }, - "children": [] + "children": [ + { + "id": "masterIdle", + "type": "randomSwitchStateMachine", + "data": { + "currentState": "masterIdle1", + "triggerRandomSwitch": "masterIdleSwitch", + "randomSwitchTimeMin": 10.0, + "randomSwitchTimeMax": 60.0, + "states": [ + { + "id": "masterIdle1", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.33, + "resume": true, + "transitions": [] + }, + { + "id": "masterIdle2", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.33, + "resume": true, + "transitions": [] + }, + { + "id": "masterIdle3", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.33, + "resume": true, + "transitions": [] + } + ] + }, + "children": [ + { + "id": "masterIdle1", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 1.0, + "endFrame": 300.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "masterIdle2", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idleWS_all.fbx", + "startFrame": 1.0, + "endFrame": 1620.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "masterIdle3", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_lookaround01.fbx", + "startFrame": 1.0, + "endFrame": 901.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "fidget", + "type": "randomSwitchStateMachine", + "data": { + "currentState": "movement", + "states": [ + { + "id": "movement", + "interpTarget": 17, + "interpDuration": 15, + "priority": 0.8, + "resume": false, + "transitions": [] + }, + { + "id": "alternateIdle", + "interpTarget": 17, + "interpDuration": 15, + "priority": 0.2, + "resume": false, + "transitions": [] + } + ] + }, + "children": [ + { + "id": "movement", + "type": "randomSwitchStateMachine", + "data": { + "currentState": "movement1", + "states": [ + { + "id": "movement1", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.2, + "resume": false, + "transitions": [] + }, + { + "id": "movement2", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.2, + "resume": false, + "transitions": [] + }, + { + "id": "movement3", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.2, + "resume": false, + "transitions": [] + }, + { + "id": "movement4", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.2, + "resume": false, + "transitions": [] + }, + { + "id": "movement5", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.2, + "resume": false, + "transitions": [] + }, + { + "id": "movement6", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.2, + "resume": false, + "transitions": [] + }, + { + "id": "movement7", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.2, + "resume": false, + "transitions": [] + }, + { + "id": "movement8", + "interpTarget": 21, + "interpDuration": 20, + "priority": 0.2, + "resume": false, + "transitions": [] + } + ] + }, + "children": [ + { + "id": "movement1", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_once_slownod.fbx", + "startFrame": 1, + "endFrame": 91.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "movement2", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_once_headtilt.fbx", + "startFrame": 1, + "endFrame": 154, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "movement3", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_once_headtilt.fbx", + "startFrame": 1, + "endFrame": 154, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "movement4", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idleWS_all.fbx", + "startFrame": 1, + "endFrame": 1620, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "movement5", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_once_lookaround.fbx", + "startFrame": 1, + "endFrame": 324, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "movement6", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_once_neckstretch.fbx", + "startFrame": 1, + "endFrame": 169, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "movement7", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idleWS_all.fbx", + "startFrame": 1, + "endFrame": 1620, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "movement8", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_once_lookaround.fbx", + "startFrame": 1, + "endFrame": 324, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + }, + { + "id": "alternateIdle", + "type": "randomSwitchStateMachine", + "data": { + "currentState": "transitionToAltIdle1", + "triggerTimeMin": 10.0, + "triggerTimeMax": 60.0, + "transitionVar": "finishAltIdle2", + "states": [ + { + "id": "transitionToAltIdle1", + "interpTarget": 11, + "interpDuration": 10, + "priority": 0.5, + "resume": false, + "transitions": [ + { + "var": "transitionToAltIdle1OnDone", + "randomSwitchState": "altIdle1" + } + ] + }, + { + "id": "transitionToAltIdle2", + "interpTarget": 11, + "interpDuration": 10, + "priority": 0.5, + "resume": false, + "transitions": [ + { + "var": "transitionToAltIdle2OnDone", + "randomSwitchState": "altIdle2" + } + ] + }, + { + "id": "altIdle1", + "interpTarget": 11, + "interpDuration": 10, + "priority": -1.0, + "resume": false, + "transitions": [ + { + "var": "finishAltIdle2", + "randomSwitchState": "alt1ToMasterIdle" + } + ] + }, + { + "id": "altIdle2", + "interpTarget": 11, + "interpDuration": 10, + "priority": -1.0, + "resume": false, + "transitions": [ + { + "var": "finishAltIdle2", + "randomSwitchState": "alt2ToMasterIdle" + } + ] + }, + { + "id": "alt1ToMasterIdle", + "interpTarget": 11, + "interpDuration": 10, + "priority": -1.0, + "resume": false, + "transitions": [] + }, + { + "id": "alt2ToMasterIdle", + "interpTarget": 11, + "interpDuration": 10, + "priority": -1.0, + "resume": false, + "transitions": [] + } + ] + }, + "children": [ + { + "id": "transitionToAltIdle1", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_LFF_all.fbx", + "startFrame": 1, + "endFrame": 55, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "transitionToAltIdle2", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_RFF_all.fbx", + "startFrame": 1, + "endFrame": 56, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "altIdle1", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_LFF_all.fbx", + "startFrame": 55, + "endFrame": 389, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "altIdle2", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_RFF_all.fbx", + "startFrame": 56, + "endFrame": 390, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "alt1ToMasterIdle", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_LFF_all.fbx", + "startFrame": 389, + "endFrame": 472, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "alt2ToMasterIdle", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_RFF_all.fbx", + "startFrame": 390, + "endFrame": 453, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + } + + ] + } + + ] } ] }, @@ -1594,4 +2099,4 @@ } ] } -} +} \ No newline at end of file diff --git a/interface/resources/html/img/tablet-help-gamepad.jpg b/interface/resources/html/img/tablet-help-gamepad.jpg index bc6dcacee0..af44233537 100644 Binary files a/interface/resources/html/img/tablet-help-gamepad.jpg and b/interface/resources/html/img/tablet-help-gamepad.jpg differ diff --git a/interface/resources/html/img/tablet-help-keyboard.jpg b/interface/resources/html/img/tablet-help-keyboard.jpg index f19c399f3a..5c4a3e37ef 100644 Binary files a/interface/resources/html/img/tablet-help-keyboard.jpg and b/interface/resources/html/img/tablet-help-keyboard.jpg differ diff --git a/interface/resources/html/img/tablet-help-oculus.jpg b/interface/resources/html/img/tablet-help-oculus.jpg index 7e2062400a..1b2a4f4198 100644 Binary files a/interface/resources/html/img/tablet-help-oculus.jpg and b/interface/resources/html/img/tablet-help-oculus.jpg differ diff --git a/interface/resources/html/img/tablet-help-vive.jpg b/interface/resources/html/img/tablet-help-vive.jpg index 27b97d71bd..9e8cca69fe 100644 Binary files a/interface/resources/html/img/tablet-help-vive.jpg and b/interface/resources/html/img/tablet-help-vive.jpg differ diff --git a/interface/resources/html/img/tablet-help-windowsMR.jpg b/interface/resources/html/img/tablet-help-windowsMR.jpg index b9d0241bec..f35c88e35b 100644 Binary files a/interface/resources/html/img/tablet-help-windowsMR.jpg and b/interface/resources/html/img/tablet-help-windowsMR.jpg differ diff --git a/interface/resources/html/tabletHelp.html b/interface/resources/html/tabletHelp.html index dd7931d0ed..1140f045db 100644 --- a/interface/resources/html/tabletHelp.html +++ b/interface/resources/html/tabletHelp.html @@ -53,6 +53,16 @@ position: absolute; top: 0; left: 0; bottom: 0; right: 0; } + + #image_button { + position: absolute; + width: 463; + height: 410; + top: 155; + left: 8; + right: 8; + bottom: 146; + } #report_problem { position: fixed; @@ -67,17 +77,23 @@ var handControllerImageURL = null; var index = 0; var count = 3; + var handControllerRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#vr-controls"; + var keyboardRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/desktop.html#movement-controls"; + var gamepadRefURL = "https://docs.highfidelity.com/en/rc81/explore/get-started/vr-controls.html#gamepad"; function showKbm() { document.getElementById("main_image").setAttribute("src", "img/tablet-help-keyboard.jpg"); + document.getElementById("image_button").setAttribute("href", keyboardRefURL); } function showHandControllers() { document.getElementById("main_image").setAttribute("src", handControllerImageURL); + document.getElementById("image_button").setAttribute("href", handControllerRefURL); } function showGamepad() { document.getElementById("main_image").setAttribute("src", "img/tablet-help-gamepad.jpg"); + document.getElementById("image_button").setAttribute("href", gamepadRefURL); } function cycleRight() { @@ -171,6 +187,7 @@ + Report Problem diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml index f4e99f136c..b5a7ddb2d8 100644 --- a/interface/resources/qml/BubbleIcon.qml +++ b/interface/resources/qml/BubbleIcon.qml @@ -23,15 +23,15 @@ Rectangle { property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; function updateOpacity() { - if (ignoreRadiusEnabled) { - bubbleRect.opacity = 1.0; - } else { - bubbleRect.opacity = 0.7; - } + var rectOpacity = ignoreRadiusEnabled ? 1.0 : (mouseArea.containsMouse ? 1.0 : 0.7); + bubbleRect.opacity = rectOpacity; } Component.onCompleted: { updateOpacity(); + AvatarInputs.ignoreRadiusEnabledChanged.connect(function() { + ignoreRadiusEnabled = AvatarInputs.ignoreRadiusEnabled; + }); } onIgnoreRadiusEnabledChanged: { @@ -74,10 +74,10 @@ Rectangle { } drag.target: dragTarget; onContainsMouseChanged: { - var rectOpacity = (ignoreRadiusEnabled && containsMouse) ? 1.0 : (containsMouse ? 1.0 : 0.7); if (containsMouse) { Tablet.playSound(TabletEnums.ButtonHover); } + var rectOpacity = ignoreRadiusEnabled ? 1.0 : (mouseArea.containsMouse ? 1.0 : 0.7); bubbleRect.opacity = rectOpacity; } } diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index 17d6a7d3b3..f90a7d8561 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -398,7 +398,7 @@ Item { lineHeight: 1 lineHeightMode: Text.ProportionalHeight - onLinkActivated: loginDialog.openUrl(link); + onLinkActivated: Window.openUrl(link); Component.onCompleted: { if (termsTextMetrics.width > root.bannerWidth) { diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 4dd05f594d..04ffe72a57 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -363,7 +363,7 @@ Item { linkColor: hifi.colors.blueAccent onLinkActivated: { Tablet.playSound(TabletEnums.ButtonClick); - loginDialog.openUrl(link); + Window.openUrl(link); lightboxPopup.titleText = "Can't Access Account"; lightboxPopup.bodyText = lightboxPopup.cantAccessBodyText; lightboxPopup.button2text = "CLOSE"; diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index 69ac2f5a6c..f1e0cfe685 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -411,7 +411,7 @@ Item { lineHeight: 1 lineHeightMode: Text.ProportionalHeight - onLinkActivated: loginDialog.openUrl(link); + onLinkActivated: Window.openUrl(link); Component.onCompleted: { if (termsTextMetrics.width > root.bannerWidth) { diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index d450b1e7bc..d2fd1dfe35 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -234,7 +234,7 @@ Item { lineHeight: 1 lineHeightMode: Text.ProportionalHeight - onLinkActivated: loginDialog.openUrl(link); + onLinkActivated: Window.openUrl(link); Component.onCompleted: { if (termsTextMetrics.width > root.bannerWidth) { diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index e10f86a947..135633e403 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -57,6 +57,14 @@ Item { StatText { text: "Avatars: " + root.avatarCount } + StatText { + visible: true + text: "Refresh: " + root.refreshRateRegime + " - " + root.refreshRateTarget + } + StatText { + visible: root.expanded + text:" " + root.refreshRateMode + " - " + root.uxMode; + } StatText { text: "Game Rate: " + root.gameLoopRate } diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index f7e2494813..c603131b9c 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -254,7 +254,7 @@ Rectangle { switchWidth: root.switchWidth; anchors.top: parent.top anchors.left: parent.left - labelTextOn: qsTr("Warn when muted in HMD"); + labelTextOn: qsTr("HMD Mute Warning"); labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 619547ef43..65453ba21d 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -336,6 +336,8 @@ Rectangle { case Qt.Key_Return: case Qt.Key_Enter: event.accepted = true; + keypressTimer.stop(); + root.searchString = searchField.text; searchField.text = ""; getMarketplaceItems(); @@ -950,7 +952,7 @@ Rectangle { text: "LOG IN" onClicked: { - sendToScript({method: 'needsLogIn_loginClicked'}); + sendToScript({method: 'marketplace_loginClicked'}); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml index 0a1593161a..e17196b492 100644 --- a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml +++ b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml @@ -137,7 +137,7 @@ Item { width: parent.width/2 - anchors.leftMargin*2; text: "Cancel" onClicked: { - sendToScript({method: 'needsLogIn_cancelClicked'}); + sendToScript({method: 'passphrasePopup_cancelClicked'}); } } @@ -155,7 +155,7 @@ Item { width: parent.width/2 - anchors.rightMargin*2; text: "Log In" onClicked: { - sendToScript({method: 'needsLogIn_loginClicked'}); + sendToScript({method: 'marketplace_loginClicked'}); } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 762c619ffd..b232d49036 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -150,6 +150,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,7 @@ #include "scripting/WalletScriptingInterface.h" #include "scripting/TTSScriptingInterface.h" #include "scripting/KeyboardScriptingInterface.h" +#include "scripting/RefreshRateScriptingInterface.h" @@ -825,7 +827,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { audioDLLPath += "/audioWin7"; } QCoreApplication::addLibraryPath(audioDLLPath); -#endif +#endif DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); @@ -1456,6 +1458,34 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _overlays.init(); // do this before scripts load DependencyManager::set(); + auto offscreenUi = getOffscreenUI(); + connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() { + // Now that we've loaded the menu and thus switched to the previous display plugin + // we can unlock the desktop repositioning code, since all the positions will be + // relative to the desktop size for this plugin + auto offscreenUi = getOffscreenUI(); + auto desktop = offscreenUi->getDesktop(); + if (desktop) { + desktop->setProperty("repositionLocked", false); + } + }); + + connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + // Do not show login dialog if requested not to on the command line + QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); + int index = arguments().indexOf(hifiNoLoginCommandLineKey); + if (index != -1) { + resumeAfterLoginDialogActionTaken(); + return; + } + + showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif + }); + // Initialize the user interface and menu system // Needs to happen AFTER the render engine initialization to access its configuration initializeUi(); @@ -1790,6 +1820,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); + getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::STARTUP); + // Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); // if the _touchscreenDevice is not supported it will not be registered @@ -1810,34 +1842,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo updateVerboseLogging(); - // Now that we've loaded the menu and thus switched to the previous display plugin - // we can unlock the desktop repositioning code, since all the positions will be - // relative to the desktop size for this plugin - auto offscreenUi = getOffscreenUI(); - connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() { - auto offscreenUi = getOffscreenUI(); - auto desktop = offscreenUi->getDesktop(); - if (desktop) { - desktop->setProperty("repositionLocked", false); - } - }); - - connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) - // Do not show login dialog if requested not to on the command line - QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); - int index = arguments().indexOf(hifiNoLoginCommandLineKey); - if (index != -1) { - resumeAfterLoginDialogActionTaken(); - return; - } - - showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif - }); - // Make sure we don't time out during slow operations at startup updateHeartbeat(); QTimer* settingsTimer = new QTimer(); @@ -1866,12 +1870,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. - } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); } { @@ -2342,7 +2340,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo DependencyManager::get()->setPrecisionPicking(rayPickID, value); }); - EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { + EntityItem::setBillboardRotationOperator([](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { if (billboardMode == BillboardMode::YAW) { //rotate about vertical to face the camera glm::vec3 dPosition = frustumPos - position; @@ -2370,7 +2368,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo DependencyManager::get()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); }); - render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([=](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { bool isTablet = url == TabletScriptingInterface::QML; if (htmlContent) { webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); @@ -2410,7 +2408,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo const uint8_t TABLET_FPS = 90; webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); }); - render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { + render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([=](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { QQuickItem* rootItem = webSurface->getRootItem(); // Fix for crash in QtWebEngineCore when rapidly switching domains @@ -2448,6 +2446,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo DependencyManager::get()->preloadSounds(); DependencyManager::get()->createKeyboard(); + FileDialogHelper::setOpenDirectoryOperator([this](const QString& path) { openDirectory(path); }); + QDesktopServices::setUrlHandler("file", this, "showUrlHandler"); + QDesktopServices::setUrlHandler("", this, "showUrlHandler"); + auto drives = QDir::drives(); + for (auto drive : drives) { + QDesktopServices::setUrlHandler(QUrl(drive.absolutePath()).scheme(), this, "showUrlHandler"); + } + _pendingIdleEvent = false; _graphicsEngine.startup(); @@ -2624,6 +2630,8 @@ void Application::onAboutToQuit() { _aboutToQuit = true; cleanupBeforeQuit(); + + getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::SHUTDOWN); } void Application::cleanupBeforeQuit() { @@ -3233,6 +3241,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); + surfaceContext->setContextProperty("RefreshRate", new RefreshRateScriptingInterface()); _fileDownload = new FileScriptingInterface(engine); surfaceContext->setContextProperty("File", _fileDownload); connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip); @@ -3381,6 +3390,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + surfaceContext->setContextProperty("RefreshRate", new RefreshRateScriptingInterface()); surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED @@ -3986,6 +3996,15 @@ static void dumpEventQueue(QThread* thread) { } #endif // DEBUG_EVENT_QUEUE +bool Application::notify(QObject * object, QEvent * event) { + if (thread() == QThread::currentThread()) { + PROFILE_RANGE_IF_LONGER(app, "notify", 2) + return QApplication::notify(object, event); + } + + return QApplication::notify(object, event); +} + bool Application::event(QEvent* event) { if (_aboutToQuit) { @@ -4043,6 +4062,9 @@ bool Application::event(QEvent* event) { case QEvent::KeyRelease: keyReleaseEvent(static_cast(event)); return true; + case QEvent::FocusIn: + focusInEvent(static_cast(event)); + return true; case QEvent::FocusOut: focusOutEvent(static_cast(event)); return true; @@ -4104,6 +4126,12 @@ bool Application::eventFilter(QObject* object, QEvent* event) { } } + if (event->type() == QEvent::WindowStateChange) { + if (getWindow()->windowState() == Qt::WindowMinimized) { + getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::MINIMIZED); + } + } + return false; } @@ -4390,6 +4418,13 @@ void Application::keyReleaseEvent(QKeyEvent* event) { } +void Application::focusInEvent(QFocusEvent* event) { + if (!_aboutToQuit && _startUpFinished) { + getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::RUNNING); + } +} + + void Application::focusOutEvent(QFocusEvent* event) { auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); foreach(auto inputPlugin, inputPlugins) { @@ -4398,6 +4433,9 @@ void Application::focusOutEvent(QFocusEvent* event) { } } + if (!_aboutToQuit && _startUpFinished) { + getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::UNFOCUS); + } // FIXME spacemouse code still needs cleanup #if 0 //SpacemouseDevice::getInstance().focusOutEvent(); @@ -5477,6 +5515,13 @@ void Application::pauseUntilLoginDetermined() { // disconnect domain handler. nodeList->getDomainHandler().disconnect(); + // From now on, it's permissible to call resumeAfterLoginDialogActionTaken() + _resumeAfterLoginDialogActionTaken_SafeToRun = true; + + if (_resumeAfterLoginDialogActionTaken_WasPostponed) { + // resumeAfterLoginDialogActionTaken() was already called, but it aborted. Now it's safe to call it again. + resumeAfterLoginDialogActionTaken(); + } } void Application::resumeAfterLoginDialogActionTaken() { @@ -5485,6 +5530,11 @@ void Application::resumeAfterLoginDialogActionTaken() { return; } + if (!_resumeAfterLoginDialogActionTaken_SafeToRun) { + _resumeAfterLoginDialogActionTaken_WasPostponed = true; + return; + } + if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) { auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); toolbar->writeProperty("visible", true); @@ -5553,6 +5603,8 @@ void Application::resumeAfterLoginDialogActionTaken() { menu->getMenu("Developer")->setVisible(_developerMenuVisible); _myCamera.setMode(_previousCameraMode); cameraModeChanged(); + _startUpFinished = true; + getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::RUNNING); } void Application::loadAvatarScripts(const QVector& urls) { @@ -5741,9 +5793,6 @@ void Application::cycleCamera() { menu->setIsOptionChecked(MenuOption::ThirdPerson, false); menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); - } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { - // do nothing if in independent or camera entity modes - return; } cameraMenuChanged(); // handle the menu change } @@ -5759,12 +5808,6 @@ void Application::cameraModeChanged() { case CAMERA_MODE_MIRROR: Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true); break; - case CAMERA_MODE_INDEPENDENT: - Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true); - break; - case CAMERA_MODE_ENTITY: - Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true); - break; default: break; } @@ -5808,14 +5851,6 @@ void Application::cameraMenuChanged() { getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); } } - } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { - _myCamera.setMode(CAMERA_MODE_INDEPENDENT); - } - } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { - if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { - _myCamera.setMode(CAMERA_MODE_ENTITY); - } } } @@ -6728,11 +6763,11 @@ void Application::updateRenderArgs(float deltaTime) { // Configure the type of display / stereo appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); } + } - appRenderArgs._renderArgs._stencilMode = getActiveDisplayPlugin()->getStencilMaskMode(); - if (appRenderArgs._renderArgs._stencilMode == StencilMode::MESH) { - appRenderArgs._renderArgs._stencilMaskOperator = getActiveDisplayPlugin()->getStencilMaskMeshOperator(); - } + appRenderArgs._renderArgs._stencilMaskMode = getActiveDisplayPlugin()->getStencilMaskMode(); + if (appRenderArgs._renderArgs._stencilMaskMode == StencilMaskMode::MESH) { + appRenderArgs._renderArgs._stencilMaskOperator = getActiveDisplayPlugin()->getStencilMaskMeshOperator(); } { @@ -7352,6 +7387,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("RefreshRate", new RefreshRateScriptingInterface); scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); @@ -8269,19 +8305,6 @@ void Application::packageModel() { ModelPackager::package(); } -void Application::openUrl(const QUrl& url) const { - if (!url.isEmpty()) { - if (url.scheme() == URL_SCHEME_HIFI) { - DependencyManager::get()->handleLookupString(url.toString()); - } else if (url.scheme() == URL_SCHEME_HIFIAPP) { - DependencyManager::get()->openSystemApp(url.path()); - } else { - // address manager did not handle - ask QDesktopServices to handle - QDesktopServices::openUrl(url); - } - } -} - void Application::loadDialog() { ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), getPreviousScriptLocation(), @@ -8422,11 +8445,23 @@ void Application::loadAvatarBrowser() const { DependencyManager::get()->openTablet(); } +void Application::addSnapshotOperator(const SnapshotOperator& snapshotOperator) { + std::lock_guard lock(_snapshotMutex); + _snapshotOperators.push(snapshotOperator); + _hasPrimarySnapshot = _hasPrimarySnapshot || std::get<2>(snapshotOperator); +} + +bool Application::takeSnapshotOperators(std::queue& snapshotOperators) { + std::lock_guard lock(_snapshotMutex); + bool hasPrimarySnapshot = _hasPrimarySnapshot; + _hasPrimarySnapshot = false; + _snapshotOperators.swap(snapshotOperators); + return hasPrimarySnapshot; +} + void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { - postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { - // Get a screenshot and save it - QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); + addSnapshotOperator(std::make_tuple([notify, includeAnimated, aspectRatio, filename](const QImage& snapshot) { + QString path = DependencyManager::get()->saveSnapshot(snapshot, filename, TestScriptingInterface::getInstance()->getTestResultsLocation()); // If we're not doing an animated snapshot as well... if (!includeAnimated) { @@ -8435,19 +8470,20 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa emit DependencyManager::get()->stillSnapshotTaken(path, notify); } } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { - // Get an animated GIF snapshot and save it - SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); + qApp->postLambdaEvent([path, aspectRatio] { + // Get an animated GIF snapshot and save it + SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, DependencyManager::get()); + }); } - }); + }, aspectRatio, true)); } void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { - postLambdaEvent([notify, filename, this] { - QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); + addSnapshotOperator(std::make_tuple([notify, filename](const QImage& snapshot) { + QString snapshotPath = DependencyManager::get()->saveSnapshot(snapshot, filename, TestScriptingInterface::getInstance()->getTestResultsLocation()); emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); - }); + }, 0.0f, false)); } void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { @@ -8752,6 +8788,7 @@ void Application::updateDisplayMode() { auto displayPlugins = getDisplayPlugins(); // Default to the first item on the list, in case none of the menu items match + DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0); auto menu = getPrimaryMenu(); if (menu) { @@ -8841,6 +8878,14 @@ void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { if (desktop) { desktop->setProperty("repositionLocked", wasRepositionLocked); } + + RefreshRateManager& refreshRateManager = getRefreshRateManager(); + refreshRateManager.setRefreshRateOperator(OpenGLDisplayPlugin::getRefreshRateOperator()); + bool isHmd = newDisplayPlugin->isHmd(); + RefreshRateManager::UXMode uxMode = isHmd ? RefreshRateManager::UXMode::HMD : + RefreshRateManager::UXMode::DESKTOP; + + refreshRateManager.setUXMode(uxMode); } bool isHmd = _displayPlugin->isHmd(); @@ -9150,7 +9195,7 @@ void Application::readArgumentsFromLocalSocket() const { // If we received a message, try to open it as a URL if (message.length() > 0) { - qApp->openUrl(QString::fromUtf8(message)); + DependencyManager::get()->openUrl(QString::fromUtf8(message)); } } @@ -9267,6 +9312,44 @@ QString Application::getGraphicsCardType() { return GPUIdent::getInstance()->getName(); } +void Application::openDirectory(const QString& path) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "openDirectory", Q_ARG(const QString&, path)); + return; + } + + QString dirPath = path; + const QString FILE_SCHEME = "file:///"; + if (dirPath.startsWith(FILE_SCHEME)) { + dirPath.remove(0, FILE_SCHEME.length()); + } + QFileInfo fileInfo(dirPath); + if (fileInfo.isDir()) { + auto scheme = QUrl(path).scheme(); + QDesktopServices::unsetUrlHandler(scheme); + QDesktopServices::openUrl(path); + QDesktopServices::setUrlHandler(scheme, this, "showUrlHandler"); + } +} + +void Application::showUrlHandler(const QUrl& url) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "showUrlHandler", Q_ARG(const QUrl&, url)); + return; + } + + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Confirm openUrl", "Do you recognize this path or code and want to open or execute it: " + url.toDisplayString()); + QObject::connect(dlg, &ModalDialogListener::response, this, [=](QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + if (QMessageBox::Yes == static_cast(answer.toInt())) { + // Unset the handler, open the URL, and the reset the handler + QDesktopServices::unsetUrlHandler(url.scheme()); + QDesktopServices::openUrl(url); + QDesktopServices::setUrlHandler(url.scheme(), this, "showUrlHandler"); + } + }); +} + #if defined(Q_OS_ANDROID) void Application::beforeEnterBackground() { auto nodeList = DependencyManager::get(); @@ -9309,6 +9392,3 @@ void Application::toggleAwayMode(){ #endif - - -#include "Application.moc" diff --git a/interface/src/Application.h b/interface/src/Application.h index 99e57f1866..300769b349 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -1,4 +1,4 @@ -// +// // Application.h // interface/src // @@ -58,6 +58,7 @@ #include "gpu/Context.h" #include "LoginStateManager.h" #include "Menu.h" +#include "RefreshRateManager.h" #include "octree/OctreePacketProcessor.h" #include "render/Engine.h" #include "scripting/ControllerScriptingInterface.h" @@ -156,6 +157,7 @@ public: void updateCamera(RenderArgs& renderArgs, float deltaTime); void resizeGL(); + bool notify(QObject *, QEvent *) override; bool event(QEvent* event) override; bool eventFilter(QObject* object, QEvent* event) override; @@ -202,6 +204,7 @@ public: CompositorHelper& getApplicationCompositor() const; Overlays& getOverlays() { return _overlays; } + RefreshRateManager& getRefreshRateManager() { return _refreshRateManager; } size_t getRenderFrameCount() const { return _graphicsEngine.getRenderFrameCount(); } float getRenderLoopRate() const { return _graphicsEngine.getRenderLoopRate(); } @@ -344,6 +347,12 @@ public: void toggleAwayMode(); #endif + using SnapshotOperator = std::tuple, float, bool>; + void addSnapshotOperator(const SnapshotOperator& snapshotOperator); + bool takeSnapshotOperators(std::queue& snapshotOperators); + + void openDirectory(const QString& path); + signals: void svoImportRequested(const QString& url); @@ -403,8 +412,6 @@ public slots: static void packageModel(); - void openUrl(const QUrl& url) const; - void resetSensors(bool andReload = false); void setActiveFaceTracker() const; @@ -471,6 +478,9 @@ public slots: QString getGraphicsCardType(); + bool gpuTextureMemSizeStable(); + void showUrlHandler(const QUrl& url); + private slots: void onDesktopRootItemCreated(QQuickItem* qmlContext); void onDesktopRootContextCreated(QQmlContext* qmlContext); @@ -565,7 +575,6 @@ private: bool importFromZIP(const QString& filePath); bool importImage(const QString& urlString); - bool gpuTextureMemSizeStable(); int processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode); void trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket); @@ -716,6 +725,7 @@ private: QUuid _loginDialogID; QUuid _avatarInputsBarID; LoginStateManager _loginStateManager; + RefreshRateManager _refreshRateManager; quint64 _lastFaceTrackerUpdate; @@ -788,6 +798,9 @@ private: AudioInjectorPointer _snapshotSoundInjector; SharedSoundPointer _snapshotSound; SharedSoundPointer _sampleSound; + std::mutex _snapshotMutex; + std::queue _snapshotOperators; + bool _hasPrimarySnapshot { false }; DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin; QString _autoSwitchDisplayModeSupportedHMDPluginName; @@ -807,5 +820,9 @@ private: bool _showTrackedObjects { false }; bool _prevShowTrackedObjects { false }; + + bool _resumeAfterLoginDialogActionTaken_WasPostponed { false }; + bool _resumeAfterLoginDialogActionTaken_SafeToRun { false }; + bool _startUpFinished { false }; }; #endif // hifi_Application_h diff --git a/interface/src/FancyCamera.h b/interface/src/FancyCamera.h index 4ca073fb4f..aead54d0fd 100644 --- a/interface/src/FancyCamera.h +++ b/interface/src/FancyCamera.h @@ -19,14 +19,22 @@ class FancyCamera : public Camera { Q_OBJECT /**jsdoc - * @namespace - * @augments Camera - */ - - // FIXME: JSDoc 3.5.5 doesn't augment @property definitions. The following definition is repeated in Camera.h. - /**jsdoc - * @property {Uuid} cameraEntity The ID of the entity that the camera position and orientation follow when the camera is in - * entity mode. + * The Camera API provides access to the "camera" that defines your view in desktop and HMD display modes. + * + * @namespace Camera + * + * @hifi-interface + * @hifi-client-entity + * @hifi-avatar + * + * @property {Vec3} position - The position of the camera. You can set this value only when the camera is in independent + * mode. + * @property {Quat} orientation - The orientation of the camera. You can set this value only when the camera is in + * independent mode. + * @property {Camera.Mode} mode - The camera mode. + * @property {ViewFrustum} frustum - The camera frustum. + * @property {Uuid} cameraEntity - The ID of the entity that is used for the camera position and orientation when the + * camera is in entity mode. */ Q_PROPERTY(QUuid cameraEntity READ getCameraEntity WRITE setCameraEntity) @@ -38,25 +46,25 @@ public: public slots: - /**jsdoc - * Get the ID of the entity that the camera is set to use the position and orientation from when it's in entity mode. You can - * also get the entity ID using the Camera.cameraEntity property. - * @function Camera.getCameraEntity - * @returns {Uuid} The ID of the entity that the camera is set to follow when in entity mode; null if no camera - * entity has been set. - */ + /**jsdoc + * Gets the ID of the entity that the camera is set to follow (i.e., use the position and orientation from) when it's in + * entity mode. You can also get the entity ID using the {@link Camera|Camera.cameraEntity} property. + * @function Camera.getCameraEntity + * @returns {Uuid} The ID of the entity that the camera is set to follow when in entity mode; null if no + * camera entity has been set. + */ QUuid getCameraEntity() const; /**jsdoc - * Set the entity that the camera should use the position and orientation from when it's in entity mode. You can also set the - * entity using the Camera.cameraEntity property. - * @function Camera.setCameraEntity - * @param {Uuid} entityID The entity that the camera should follow when it's in entity mode. - * @example Move your camera to the position and orientation of the closest entity. - * Camera.setModeString("entity"); - * var entity = Entities.findClosestEntity(MyAvatar.position, 100.0); - * Camera.setCameraEntity(entity); - */ + * Sets the entity that the camera should follow (i.e., use the position and orientation from) when it's in entity mode. + * You can also set the entity using the {@link Camera|Camera.cameraEntity} property. + * @function Camera.setCameraEntity + * @param {Uuid} entityID - The entity that the camera should follow when it's in entity mode. + * @example Move your camera to the position and orientation of the closest entity. + * Camera.setModeString("entity"); + * var entity = Entities.findClosestEntity(MyAvatar.position, 100.0); + * Camera.setCameraEntity(entity); + */ void setCameraEntity(QUuid entityID); private: diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index e9aadea2b6..9341b2316c 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -194,20 +194,6 @@ Menu::Menu() { viewMirrorAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); - // View > Independent - auto viewIndependentAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::IndependentMode, 0, - false, qApp, SLOT(cameraMenuChanged()))); - - viewIndependentAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); - - // View > Entity Camera - auto viewEntityCameraAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::CameraEntityMode, 0, - false, qApp, SLOT(cameraMenuChanged()))); - - viewEntityCameraAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); - viewMenu->addSeparator(); // View > Center Player In View @@ -434,9 +420,21 @@ Menu::Menu() { MenuWrapper* resolutionMenu = renderOptionsMenu->addMenu(MenuOption::RenderResolution); QActionGroup* resolutionGroup = new QActionGroup(resolutionMenu); resolutionGroup->setExclusive(true); + +#if defined(Q_OS_MAC) + resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionOne, 0, false)); +#else resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionOne, 0, true)); +#endif + resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionTwoThird, 0, false)); + + #if defined(Q_OS_MAC) + resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionHalf, 0, true)); +#else resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionHalf, 0, false)); +#endif + resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionThird, 0, false)); resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionQuarter, 0, false)); @@ -627,6 +625,8 @@ Menu::Menu() { avatar.get(), SLOT(setEnableDebugDrawAnimPose(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawPosition, 0, false, avatar.get(), SLOT(setEnableDebugDrawPosition(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawOtherSkeletons, 0, false, + avatarManager.data(), SLOT(setEnableDebugDrawOtherSkeletons(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::MeshVisible, 0, true, avatar.get(), SLOT(setEnableMeshVisible(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::DisableEyelidAdjustment, 0, false); @@ -707,8 +707,7 @@ Menu::Menu() { // Developer > Timing >>> MenuWrapper* timingMenu = developerMenu->addMenu("Timing"); MenuWrapper* perfTimerMenu = timingMenu->addMenu("Performance Timer"); - addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayDebugTimingDetails, 0, false, - qApp, SLOT(enablePerfStats(bool))); + addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayDebugTimingDetails); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::OnlyDisplayTopTen, 0, true); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandUpdateTiming, 0, false); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandSimulationTiming, 0, false); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 3611faaf8f..eeede178c7 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -33,6 +33,7 @@ namespace MenuOption { const QString AnimDebugDrawBaseOfSupport = "Debug Draw Base of Support"; const QString AnimDebugDrawDefaultPose = "Debug Draw Default Pose"; const QString AnimDebugDrawPosition= "Debug Draw Position"; + const QString AnimDebugDrawOtherSkeletons = "Debug Draw Other Skeletons"; const QString AskToResetSettings = "Ask To Reset Settings on Start"; const QString AssetMigration = "ATP Asset Migration"; const QString AssetServer = "Asset Browser"; @@ -53,7 +54,6 @@ namespace MenuOption { const QString BookmarkAvatarEntities = "Bookmark Avatar Entities"; const QString BookmarkLocation = "Bookmark Location"; const QString CalibrateCamera = "Calibrate Camera"; - const QString CameraEntityMode = "Entity Mode"; const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; const QString ClearDiskCache = "Clear Disk Cache"; @@ -120,7 +120,6 @@ namespace MenuOption { const QString Help = "Help..."; const QString HomeLocation = "Home "; const QString IncreaseAvatarSize = "Increase Avatar Size"; - const QString IndependentMode = "Independent Mode"; const QString ActionMotorControl = "Enable Default Motor Control"; const QString LastLocation = "Last Location"; const QString LoadScript = "Open and Run Script File..."; diff --git a/interface/src/ModelSelector.cpp b/interface/src/ModelSelector.cpp index 3223e3ab9c..6da9327cac 100644 --- a/interface/src/ModelSelector.cpp +++ b/interface/src/ModelSelector.cpp @@ -18,9 +18,6 @@ #include #include -static const QString AVATAR_HEAD_AND_BODY_STRING = "Avatar Body with Head"; -static const QString ENTITY_MODEL_STRING = "Entity Model"; - ModelSelector::ModelSelector() { QFormLayout* form = new QFormLayout(this); diff --git a/interface/src/RefreshRateManager.cpp b/interface/src/RefreshRateManager.cpp new file mode 100644 index 0000000000..c4a35da1f6 --- /dev/null +++ b/interface/src/RefreshRateManager.cpp @@ -0,0 +1,149 @@ +// +// RefreshRateManager.cpp +// interface/src/ +// +// Created by Dante Ruiz on 2019-04-15. +// 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 "RefreshRateManager.h" + +#include +#include + + +#include + +#include + +static const int HMD_TARGET_RATE = 90; + +static const std::array REFRESH_RATE_PROFILE_TO_STRING = + { { "Eco", "Interactive", "Realtime" } }; + +static const std::array REFRESH_RATE_REGIME_TO_STRING = + { { "Running", "Unfocus", "Minimized", "StartUp", "ShutDown" } }; + +static const std::array UX_MODE_TO_STRING = + { { "Desktop", "HMD" } }; + +static const std::map REFRESH_RATE_PROFILE_FROM_STRING = + { { "Eco", RefreshRateManager::RefreshRateProfile::ECO }, + { "Interactive", RefreshRateManager::RefreshRateProfile::INTERACTIVE }, + { "Realtime", RefreshRateManager::RefreshRateProfile::REALTIME } }; + +static const std::array RUNNING_REGIME_PROFILES = + { { 5, 20, 60 } }; + +static const std::array UNFOCUS_REGIME_PROFILES = + { { 5, 5, 10 } }; + +static const std::array MINIMIZED_REGIME_PROFILE = + { { 2, 2, 2 } }; + +static const std::array START_AND_SHUTDOWN_REGIME_PROFILES = + { { 30, 30, 30 } }; + +static const std::array, RefreshRateManager::RefreshRateRegime::REGIME_NUM> REFRESH_RATE_REGIMES = + { { RUNNING_REGIME_PROFILES, UNFOCUS_REGIME_PROFILES, MINIMIZED_REGIME_PROFILE, + START_AND_SHUTDOWN_REGIME_PROFILES, START_AND_SHUTDOWN_REGIME_PROFILES } }; + + +std::string RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile refreshRateProfile) { + return REFRESH_RATE_PROFILE_TO_STRING.at(refreshRateProfile); +} + +RefreshRateManager::RefreshRateProfile RefreshRateManager::refreshRateProfileFromString(std::string refreshRateProfile) { + return REFRESH_RATE_PROFILE_FROM_STRING.at(refreshRateProfile); +} + +std::string RefreshRateManager::refreshRateRegimeToString(RefreshRateManager::RefreshRateRegime refreshRateRegime) { + return REFRESH_RATE_REGIME_TO_STRING.at(refreshRateRegime); +} + +std::string RefreshRateManager::uxModeToString(RefreshRateManager::RefreshRateManager::UXMode uxMode) { + return UX_MODE_TO_STRING.at(uxMode); +} + +RefreshRateManager::RefreshRateManager() { + _refreshRateProfile = (RefreshRateManager::RefreshRateProfile) _refreshRateMode.get(); +} + +void RefreshRateManager::setRefreshRateProfile(RefreshRateManager::RefreshRateProfile refreshRateProfile) { + if (_refreshRateProfile != refreshRateProfile) { + _refreshRateModeLock.withWriteLock([&] { + _refreshRateProfile = refreshRateProfile; + _refreshRateMode.set((int) refreshRateProfile); + }); + updateRefreshRateController(); + } +} + +RefreshRateManager::RefreshRateProfile RefreshRateManager::getRefreshRateProfile() const { + RefreshRateManager::RefreshRateProfile profile = RefreshRateManager::RefreshRateProfile::REALTIME; + + if (getUXMode() != RefreshRateManager::UXMode::HMD) { + profile =(RefreshRateManager::RefreshRateProfile) _refreshRateModeLock.resultWithReadLock([&] { + return _refreshRateMode.get(); + }); + } + + return profile; +} + +RefreshRateManager::RefreshRateRegime RefreshRateManager::getRefreshRateRegime() const { + return getUXMode() == RefreshRateManager::UXMode::HMD ? RefreshRateManager::RefreshRateRegime::RUNNING : + _refreshRateRegime; +} + +void RefreshRateManager::setRefreshRateRegime(RefreshRateManager::RefreshRateRegime refreshRateRegime) { + if (_refreshRateRegime != refreshRateRegime) { + _refreshRateRegime = refreshRateRegime; + updateRefreshRateController(); + } + +} + +void RefreshRateManager::setUXMode(RefreshRateManager::UXMode uxMode) { + if (_uxMode != uxMode) { + _uxMode = uxMode; + updateRefreshRateController(); + } +} + +void RefreshRateManager::updateRefreshRateController() const { + if (_refreshRateOperator) { + int targetRefreshRate; + if (_uxMode == RefreshRateManager::UXMode::DESKTOP) { + if (_refreshRateRegime == RefreshRateManager::RefreshRateRegime::RUNNING && + _refreshRateProfile == RefreshRateManager::RefreshRateProfile::INTERACTIVE) { + targetRefreshRate = getInteractiveRefreshRate(); + } else { + targetRefreshRate = REFRESH_RATE_REGIMES[_refreshRateRegime][_refreshRateProfile]; + } + } else { + targetRefreshRate = HMD_TARGET_RATE; + } + + _refreshRateOperator(targetRefreshRate); + _activeRefreshRate = targetRefreshRate; + } +} + +void RefreshRateManager::setInteractiveRefreshRate(int refreshRate) { + _refreshRateLock.withWriteLock([&] { + _interactiveRefreshRate.set(refreshRate); + }); + updateRefreshRateController(); +} + + +int RefreshRateManager::getInteractiveRefreshRate() const { + return _refreshRateLock.resultWithReadLock([&] { + return _interactiveRefreshRate.get(); + }); +} diff --git a/interface/src/RefreshRateManager.h b/interface/src/RefreshRateManager.h new file mode 100644 index 0000000000..ee7debe3ef --- /dev/null +++ b/interface/src/RefreshRateManager.h @@ -0,0 +1,83 @@ +// +// RefreshRateManager.h +// interface/src/ +// +// Created by Dante Ruiz on 2019-04-15. +// 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_RefreshRateManager_h +#define hifi_RefreshRateManager_h + +#include +#include + +#include +#include + +class RefreshRateManager { +public: + enum RefreshRateProfile { + ECO = 0, + INTERACTIVE, + REALTIME, + PROFILE_NUM + }; + + enum RefreshRateRegime { + RUNNING = 0, + UNFOCUS, + MINIMIZED, + STARTUP, + SHUTDOWN, + REGIME_NUM + }; + + enum UXMode { + DESKTOP = 0, + HMD, + UX_NUM + }; + + RefreshRateManager(); + ~RefreshRateManager() = default; + + void setRefreshRateProfile(RefreshRateProfile refreshRateProfile); + RefreshRateProfile getRefreshRateProfile() const; + + void setRefreshRateRegime(RefreshRateRegime refreshRateRegime); + RefreshRateRegime getRefreshRateRegime() const; + + void setUXMode(UXMode uxMode); + UXMode getUXMode() const { return _uxMode; } + + void setRefreshRateOperator(std::function refreshRateOperator) { _refreshRateOperator = refreshRateOperator; } + int getActiveRefreshRate() const { return _activeRefreshRate; } + void updateRefreshRateController() const; + void setInteractiveRefreshRate(int refreshRate); + int getInteractiveRefreshRate() const; + + static std::string refreshRateProfileToString(RefreshRateProfile refreshRateProfile); + static RefreshRateProfile refreshRateProfileFromString(std::string refreshRateProfile); + static std::string uxModeToString(UXMode uxMode); + static std::string refreshRateRegimeToString(RefreshRateRegime refreshRateRegime); + +private: + mutable ReadWriteLockable _refreshRateLock; + mutable ReadWriteLockable _refreshRateModeLock; + + mutable int _activeRefreshRate { 20 }; + RefreshRateProfile _refreshRateProfile { RefreshRateProfile::INTERACTIVE}; + RefreshRateRegime _refreshRateRegime { RefreshRateRegime::STARTUP }; + UXMode _uxMode; + + Setting::Handle _interactiveRefreshRate { "interactiveRefreshRate", 20}; + Setting::Handle _refreshRateMode { "refreshRateProfile", INTERACTIVE }; + + std::function _refreshRateOperator { nullptr }; +}; + +#endif diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index 12c9636746..da2874a3f4 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -152,10 +152,12 @@ public: _cachedArgsPointer->_viewport = args->_viewport; _cachedArgsPointer->_displayMode = args->_displayMode; _cachedArgsPointer->_renderMode = args->_renderMode; + _cachedArgsPointer->_stencilMaskMode = args->_stencilMaskMode; args->_blitFramebuffer = destFramebuffer; args->_viewport = glm::ivec4(0, 0, destFramebuffer->getWidth(), destFramebuffer->getHeight()); args->_displayMode = RenderArgs::MONO; args->_renderMode = RenderArgs::RenderMode::SECONDARY_CAMERA_RENDER_MODE; + args->_stencilMaskMode = StencilMaskMode::NONE; gpu::doInBatch("SecondaryCameraJob::run", args->_context, [&](gpu::Batch& batch) { batch.disableContextStereo(); @@ -255,10 +257,11 @@ public: void run(const render::RenderContextPointer& renderContext, const RenderArgsPointer& cachedArgs) { auto args = renderContext->args; if (cachedArgs) { - args->_blitFramebuffer = cachedArgs->_blitFramebuffer; - args->_viewport = cachedArgs->_viewport; - args->_displayMode = cachedArgs->_displayMode; - args->_renderMode = cachedArgs->_renderMode; + args->_blitFramebuffer = cachedArgs->_blitFramebuffer; + args->_viewport = cachedArgs->_viewport; + args->_displayMode = cachedArgs->_displayMode; + args->_renderMode = cachedArgs->_renderMode; + args->_stencilMaskMode = cachedArgs->_stencilMaskMode; } args->popViewFrustum(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 7a64edae11..2025d3cabc 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -120,6 +120,8 @@ void AvatarManager::init() { _myAvatar->addToScene(_myAvatar, scene, transaction); scene->enqueueTransaction(transaction); } + + setEnableDebugDrawOtherSkeletons(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawOtherSkeletons)); } void AvatarManager::setSpace(workload::SpacePointer& space ) { @@ -334,9 +336,14 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { if (avatar->getSkeletonModel()->isLoaded() && avatar->getWorkloadRegion() == workload::Region::R1) { _myAvatar->addAvatarHandsToFlow(avatar); } + if (_drawOtherAvatarSkeletons) { + avatar->debugJointData(); + } + avatar->setEnableMeshVisible(!_drawOtherAvatarSkeletons); avatar->updateRenderItem(renderTransaction); avatar->updateSpaceProxy(workloadTransaction); avatar->setLastRenderUpdateTime(startTime); + } else { // we've spent our time budget for this priority bucket // let's deal with the reminding avatars if this pass and BREAK from the for loop @@ -497,9 +504,11 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar // it might not fire until after we create a new instance for the same remote avatar, which creates a race // on the creation of entities for that avatar instance and the deletion of entities for this instance avatar->removeAvatarEntitiesFromTree(); - if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == KillAvatarReason::NoReason) { - emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID()); - emit DependencyManager::get()->enteredIgnoreRadius(); + if (removalReason != KillAvatarReason::AvatarDisconnected) { + if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { + emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID()); + emit DependencyManager::get()->enteredIgnoreRadius(); + } workload::Transaction workloadTransaction; workloadTransaction.remove(avatar->getSpaceIndex()); @@ -509,7 +518,7 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar render::Transaction transaction; avatar->removeFromScene(avatar, scene, transaction); scene->enqueueTransaction(transaction); - } else if (removalReason == KillAvatarReason::AvatarDisconnected) { + } else { // remove from node sets, if present DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); DependencyManager::get()->avatarDisconnected(avatar->getSessionUUID()); @@ -725,7 +734,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic boxHit._distance = FLT_MAX; for (size_t i = 0; i < hit._boundJoints.size(); i++) { - assert(hit._boundJoints[i] < multiSpheres.size()); + assert(hit._boundJoints[i] < (int)multiSpheres.size()); auto &mSphere = multiSpheres[hit._boundJoints[i]]; if (mSphere.isValid()) { float boundDistance = FLT_MAX; @@ -932,6 +941,19 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV } } +/**jsdoc + * PAL (People Access List) data for an avatar. + * @typedef {object} AvatarManager.PalData + * @property {Uuid} sessionUUID - The avatar's session ID. "" if the avatar is your own. + * @property {string} sessionDisplayName - The avatar's display name, sanitized and versioned, as defined by the avatar mixer. + * It is unique among all avatars present in the domain at the time. + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * domain. + * @property {boolean} isReplicated - Deprecated: This property is deprecated and will be + * removed. + * @property {Vec3} position - The position of the avatar. + * @property {number} palOrbOffset - The vertical offset from the avatar's position that an overlay orb should be displayed at. + */ QVariantMap AvatarManager::getPalData(const QStringList& specificAvatarIdentifiers) { QJsonArray palData; diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index f9b82da0c1..98deebe919 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -37,10 +37,11 @@ using SortedAvatar = std::pair>; /**jsdoc - * The AvatarManager API has properties and methods which manage Avatars within the same domain. + * The AvatarManager API provides information about avatars within the current domain. The avatars available are + * those that Interface has displayed and therefore knows about. * - *

Note: This API is also provided to Interface and client entity scripts as the synonym, - * AvatarList. For assignment client scripts, see the separate {@link AvatarList} API. + *

Warning: This API is also provided to Interface, client entity, and avatar scripts as the synonym, + * "AvatarList". For assignment client scripts, see the separate {@link AvatarList} API.

* * @namespace AvatarManager * @@ -48,8 +49,9 @@ using SortedAvatar = std::pair>; * @hifi-client-entity * @hifi-avatar * - * @borrows AvatarList.getAvatarIdentifiers as getAvatarIdentifiers - * @borrows AvatarList.getAvatarsInRange as getAvatarsInRange + * @borrows AvatarList.getAvatar as getAvatar + * @comment AvatarList.getAvatarIdentifiers as getAvatarIdentifiers - Don't borrow because behavior is slightly different. + * @comment AvatarList.getAvatarsInRange as getAvatarsInRange - Don't borrow because behavior is slightly different. * @borrows AvatarList.avatarAddedEvent as avatarAddedEvent * @borrows AvatarList.avatarRemovedEvent as avatarRemovedEvent * @borrows AvatarList.avatarSessionChangedEvent as avatarSessionChangedEvent @@ -67,6 +69,31 @@ class AvatarManager : public AvatarHashMap { public: + /**jsdoc + * Gets the IDs of all avatars known about in the domain. + * Your own avatar is included in the list as a null value. + * @function AvatarManager.getAvatarIdentifiers + * @returns {Uuid[]} The IDs of all known avatars in the domain. + * @example Report the IDS of all avatars within the domain. + * var avatars = AvatarManager.getAvatarIdentifiers(); + * print("Avatars in the domain: " + JSON.stringify(avatars)); + * // A null item is included for your avatar. + */ + + /**jsdoc + * Gets the IDs of all avatars known about within a specified distance from a point. + * Your own avatar's ID is included in the list if it is in range. + * @function AvatarManager.getAvatarsInRange + * @param {Vec3} position - The point about which the search is performed. + * @param {number} range - The search radius. + * @returns {Uuid[]} The IDs of all known avatars within the search distance from the position. + * @example Report the IDs of all avatars within 10m of your avatar. + * var RANGE = 10; + * var avatars = AvatarManager.getAvatarsInRange(MyAvatar.position, RANGE); + * print("Nearby avatars: " + JSON.stringify(avatars)); + * print("Own avatar: " + MyAvatar.sessionUUID); + */ + /// Registers the script types associated with the avatar manager. static void registerMetaTypes(QScriptEngine* engine); @@ -79,9 +106,7 @@ public: glm::vec3 getMyAvatarPosition() const { return _myAvatar->getWorldPosition(); } /**jsdoc - * @function AvatarManager.getAvatar - * @param {Uuid} avatarID - * @returns {AvatarData} + * @comment Uses the base class's JSDoc. */ // Null/Default-constructed QUuids will return MyAvatar Q_INVOKABLE virtual ScriptAvatarData* getAvatar(QUuid avatarID) override { return new ScriptAvatar(getAvatarBySessionID(avatarID)); } @@ -112,36 +137,53 @@ public: void handleCollisionEvents(const CollisionEvents& collisionEvents); /**jsdoc + * Gets the amount of avatar mixer data being generated by an avatar other than your own. * @function AvatarManager.getAvatarDataRate - * @param {Uuid} sessionID - * @param {string} [rateName=""] - * @returns {number} + * @param {Uuid} sessionID - The ID of the avatar whose data rate you're retrieving. + * @param {AvatarDataRate} [rateName=""] - The type of avatar mixer data to get the data rate of. + * @returns {number} The data rate in kbps; 0 if the avatar is your own. */ Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const; /**jsdoc + * Gets the update rate of avatar mixer data being generated by an avatar other than your own. * @function AvatarManager.getAvatarUpdateRate - * @param {Uuid} sessionID - * @param {string} [rateName=""] - * @returns {number} + * @param {Uuid} sessionID - The ID of the avatar whose update rate you're retrieving. + * @param {AvatarUpdateRate} [rateName=""] - The type of avatar mixer data to get the update rate of. + * @returns {number} The update rate in Hz; 0 if the avatar is your own. */ Q_INVOKABLE float getAvatarUpdateRate(const QUuid& sessionID, const QString& rateName = QString("")) const; /**jsdoc + * Gets the simulation rate of an avatar other than your own. * @function AvatarManager.getAvatarSimulationRate - * @param {Uuid} sessionID - * @param {string} [rateName=""] - * @returns {number} + * @param {Uuid} sessionID - The ID of the avatar whose simulation you're retrieving. + * @param {AvatarSimulationRate} [rateName=""] - The type of avatar data to get the simulation rate of. + * @returns {number} The simulation rate in Hz; 0 if the avatar is your own. */ Q_INVOKABLE float getAvatarSimulationRate(const QUuid& sessionID, const QString& rateName = QString("")) const; /**jsdoc + * Find the first avatar intersected by a {@link PickRay}. * @function AvatarManager.findRayIntersection - * @param {PickRay} ray - * @param {Uuid[]} [avatarsToInclude=[]] - * @param {Uuid[]} [avatarsToDiscard=[]] - * @param {boolean} pickAgainstMesh - * @returns {RayToAvatarIntersectionResult} + * @param {PickRay} ray - The ray to use for finding avatars. + * @param {Uuid[]} [avatarsToInclude=[]] - If not empty then search is restricted to these avatars. + * @param {Uuid[]} [avatarsToDiscard=[]] - Avatars to ignore in the search. + * @param {boolean} [pickAgainstMesh=true] - If true then the exact intersection with the avatar mesh is + * calculated, if false then the intersection is approximate. + * @returns {RayToAvatarIntersectionResult} The result of the search for the first intersected avatar. + * @example Find the first avatar directly in front of you. + * var pickRay = { + * origin: MyAvatar.position, + * direction: Quat.getFront(MyAvatar.orientation) + * }; + * + * var intersection = AvatarManager.findRayIntersection(pickRay); + * if (intersection.intersects) { + * print("Avatar found: " + JSON.stringify(intersection)); + * } else { + * print("No avatar found."); + * } */ Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersection(const PickRay& ray, const QScriptValue& avatarIdsToInclude = QScriptValue(), @@ -149,11 +191,12 @@ public: bool pickAgainstMesh = true); /**jsdoc * @function AvatarManager.findRayIntersectionVector - * @param {PickRay} ray - * @param {Uuid[]} avatarsToInclude - * @param {Uuid[]} avatarsToDiscard - * @param {boolean} pickAgainstMesh - * @returns {RayToAvatarIntersectionResult} + * @param {PickRay} ray - Ray. + * @param {Uuid[]} avatarsToInclude - Avatars to include. + * @param {Uuid[]} avatarsToDiscard - Avatars to discard. + * @param {boolean} pickAgainstMesh - Pick against mesh. + * @returns {RayToAvatarIntersectionResult} Intersection result. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersectionVector(const PickRay& ray, const QVector& avatarsToInclude, @@ -162,10 +205,11 @@ public: /**jsdoc * @function AvatarManager.findParabolaIntersectionVector - * @param {PickParabola} pick - * @param {Uuid[]} avatarsToInclude - * @param {Uuid[]} avatarsToDiscard - * @returns {ParabolaToAvatarIntersectionResult} + * @param {PickParabola} pick - Pick. + * @param {Uuid[]} avatarsToInclude - Avatars to include. + * @param {Uuid[]} avatarsToDiscard - Avatars to discard. + * @returns {ParabolaToAvatarIntersectionResult} Intersection result. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE ParabolaToAvatarIntersectionResult findParabolaIntersectionVector(const PickParabola& pick, const QVector& avatarsToInclude, @@ -173,27 +217,31 @@ public: /**jsdoc * @function AvatarManager.getAvatarSortCoefficient - * @param {string} name - * @returns {number} + * @param {string} name - Name. + * @returns {number} Value. + * @deprecated This function is deprecated and will be removed. */ // TODO: remove this HACK once we settle on optimal default sort coefficients Q_INVOKABLE float getAvatarSortCoefficient(const QString& name); /**jsdoc * @function AvatarManager.setAvatarSortCoefficient - * @param {string} name - * @param {number} value + * @param {string} name - Name + * @param {number} value - Value. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value); /**jsdoc - * Used in the PAL for getting PAL-related data about avatars nearby. Using this method is faster - * than iterating over each avatar and obtaining data about them in JavaScript, as that method - * locks and unlocks each avatar's data structure potentially hundreds of times per update tick. + * Gets PAL (People Access List) data for one or more avatars. Using this method is faster than iterating over each avatar + * and obtaining data about each individually. * @function AvatarManager.getPalData - * @param {string[]} [specificAvatarIdentifiers=[]] - The list of IDs of the avatars you want the PAL data for. - * If an empty list, the PAL data for all nearby avatars is returned. - * @returns {object[]} An array of objects, each object being the PAL data for an avatar. + * @param {string[]} [avatarIDs=[]] - The IDs of the avatars to get the PAL data for. If empty, then PAL data is obtained + * for all avatars. + * @returns {object<"data", AvatarManager.PalData[]>} An array of objects, each object being the PAL data for an avatar. + * @example Report the PAL data for an avatar nearby. + * var palData = AvatarManager.getPalData(); + * print("PAL data for one avatar: " + JSON.stringify(palData.data[0])); */ Q_INVOKABLE QVariantMap getPalData(const QStringList& specificAvatarIdentifiers = QStringList()); @@ -209,10 +257,20 @@ public: public slots: /**jsdoc * @function AvatarManager.updateAvatarRenderStatus - * @param {boolean} shouldRenderAvatars + * @param {boolean} shouldRenderAvatars - Should render avatars. + * @deprecated This function is deprecated and will be removed. */ void updateAvatarRenderStatus(bool shouldRenderAvatars); + /**jsdoc + * Displays other avatars skeletons debug graphics. + * @function AvatarManager.setEnableDebugDrawOtherSkeletons + * @param {boolean} enabled - true to show the debug graphics, false to hide. + */ + void setEnableDebugDrawOtherSkeletons(bool isEnabled) { + _drawOtherAvatarSkeletons = isEnabled; + } + protected: AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) override; @@ -250,6 +308,7 @@ private: workload::SpacePointer _space; AvatarTransit::TransitConfig _transitConfig; + bool _drawOtherAvatarSkeletons { false }; }; #endif // hifi_AvatarManager_h diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 568b492b46..e3fbbe12ad 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -168,6 +168,7 @@ MyAvatar::MyAvatar(QThread* thread) : _displayNameSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "displayName", ""), _collisionSoundURLSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "collisionSoundURL", QUrl(_collisionSoundURL)), _useSnapTurnSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "useSnapTurn", _useSnapTurn), + _hoverWhenUnsupportedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "hoverWhenUnsupported", _hoverWhenUnsupported), _userHeightSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "userHeight", DEFAULT_AVATAR_HEIGHT), _flyingHMDSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "flyingHMD", _flyingPrefHMD), _movementReferenceSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "movementReference", _movementReference), @@ -818,20 +819,8 @@ void MyAvatar::simulate(float deltaTime, bool inView) { if (_cauterizationNeedsUpdate) { _cauterizationNeedsUpdate = false; - // Redisplay cauterized entities that are no longer children of the avatar. - auto cauterizedChild = _cauterizedChildrenOfHead.begin(); - if (cauterizedChild != _cauterizedChildrenOfHead.end()) { - auto children = getChildren(); - while (cauterizedChild != _cauterizedChildrenOfHead.end()) { - if (!children.contains(*cauterizedChild)) { - updateChildCauterization(*cauterizedChild, false); - cauterizedChild = _cauterizedChildrenOfHead.erase(cauterizedChild); - } else { - ++cauterizedChild; - } - } - } - + auto objectsToUncauterize = _cauterizedChildrenOfHead; + _cauterizedChildrenOfHead.clear(); // Update cauterization of entities that are children of the avatar. auto headBoneSet = _skeletonModel->getCauterizeBoneSet(); forEachChild([&](SpatiallyNestablePointer object) { @@ -843,15 +832,19 @@ void MyAvatar::simulate(float deltaTime, bool inView) { updateChildCauterization(descendant, !_prevShouldDrawHead); }); _cauterizedChildrenOfHead.insert(object); - } else if (_cauterizedChildrenOfHead.find(object) != _cauterizedChildrenOfHead.end()) { - // Redisplay cauterized children that are not longer children of the head. - updateChildCauterization(object, false); + objectsToUncauterize.erase(object); + } else if (objectsToUncauterize.find(object) == objectsToUncauterize.end()) { + objectsToUncauterize.insert(object); object->forEachDescendant([&](SpatiallyNestablePointer descendant) { - updateChildCauterization(descendant, false); + objectsToUncauterize.insert(descendant); }); - _cauterizedChildrenOfHead.erase(object); } }); + + // Redisplay cauterized entities that are no longer children of the avatar. + for (auto cauterizedChild = objectsToUncauterize.begin(); cauterizedChild != objectsToUncauterize.end(); cauterizedChild++) { + updateChildCauterization(*cauterizedChild, false); + } } { @@ -956,6 +949,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) { bool collisionlessAllowed = zoneInteractionProperties.second; _characterController.setZoneFlyingAllowed(zoneAllowsFlying || !isPhysicsEnabled); _characterController.setComfortFlyingAllowed(_enableFlying); + _characterController.setHoverWhenUnsupported(_hoverWhenUnsupported); _characterController.setCollisionlessAllowed(collisionlessAllowed); } @@ -1049,11 +1043,15 @@ void MyAvatar::updateJointFromController(controller::Action poseKey, ThreadSafeV assert(QThread::currentThread() == thread()); auto userInputMapper = DependencyManager::get(); controller::Pose controllerPose = userInputMapper->getPoseState(poseKey); - Transform transform; - transform.setTranslation(controllerPose.getTranslation()); - transform.setRotation(controllerPose.getRotation()); - glm::mat4 controllerMatrix = transform.getMatrix(); - matrixCache.set(controllerMatrix); + if (controllerPose.isValid()) { + Transform transform; + transform.setTranslation(controllerPose.getTranslation()); + transform.setRotation(controllerPose.getRotation()); + glm::mat4 controllerMatrix = transform.getMatrix(); + matrixCache.set(controllerMatrix); + } else { + matrixCache.invalidate(); + } } // best called at end of main loop, after physics. @@ -1207,6 +1205,15 @@ void MyAvatar::overrideAnimation(const QString& url, float fps, bool loop, float _skeletonModel->getRig().overrideAnimation(url, fps, loop, firstFrame, lastFrame); } +void MyAvatar::overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "overrideHandAnimation", Q_ARG(bool, isLeft), Q_ARG(const QString&, url), Q_ARG(float, fps), + Q_ARG(bool, loop), Q_ARG(float, firstFrame), Q_ARG(float, lastFrame)); + return; + } + _skeletonModel->getRig().overrideHandAnimation(isLeft, url, fps, loop, firstFrame, lastFrame); +} + void MyAvatar::restoreAnimation() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "restoreAnimation"); @@ -1215,6 +1222,14 @@ void MyAvatar::restoreAnimation() { _skeletonModel->getRig().restoreAnimation(); } +void MyAvatar::restoreHandAnimation(bool isLeft) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "restoreHandAnimation", Q_ARG(bool, isLeft)); + return; + } + _skeletonModel->getRig().restoreHandAnimation(isLeft); +} + QStringList MyAvatar::getAnimationRoles() { if (QThread::currentThread() != thread()) { QStringList result; @@ -1296,6 +1311,7 @@ void MyAvatar::saveData() { _displayNameSetting.set(_displayName); _collisionSoundURLSetting.set(_collisionSoundURL); _useSnapTurnSetting.set(_useSnapTurn); + _hoverWhenUnsupportedSetting.set(_hoverWhenUnsupported); _userHeightSetting.set(getUserHeight()); _flyingHMDSetting.set(getFlyingHMDPref()); _movementReferenceSetting.set(getMovementReference()); @@ -1621,7 +1637,9 @@ void MyAvatar::handleChangedAvatarEntityData() { if (!skip) { sanitizeAvatarEntityProperties(properties); entityTree->withWriteLock([&] { - entityTree->updateEntity(id, properties); + if (entityTree->updateEntity(id, properties)) { + packetSender->queueEditAvatarEntityMessage(entityTree, id); + } }); } } @@ -1898,6 +1916,7 @@ void MyAvatar::loadData() { setDisplayName(_displayNameSetting.get()); setCollisionSoundURL(_collisionSoundURLSetting.get(QUrl(DEFAULT_AVATAR_COLLISION_SOUND_URL)).toString()); setSnapTurn(_useSnapTurnSetting.get()); + setHoverWhenUnsupported(_hoverWhenUnsupportedSetting.get()); setDominantHand(_dominantHandSetting.get(DOMINANT_RIGHT_HAND).toLower()); setStrafeEnabled(_strafeEnabledSetting.get(DEFAULT_STRAFE_ENABLED)); setHmdAvatarAlignmentType(_hmdAvatarAlignmentTypeSetting.get(DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE).toLower()); @@ -3180,17 +3199,40 @@ int MyAvatar::sendAvatarDataPacket(bool sendAll) { return bytesSent; } -const float RENDER_HEAD_CUTOFF_DISTANCE = 0.47f; - bool MyAvatar::cameraInsideHead(const glm::vec3& cameraPosition) const { + if (!_skeletonModel) { + return false; + } + + // transform cameraPosition into rig coordinates + AnimPose rigToWorld = AnimPose(getWorldOrientation() * Quaternions::Y_180, getWorldPosition()); + AnimPose worldToRig = rigToWorld.inverse(); + glm::vec3 rigCameraPosition = worldToRig * cameraPosition; + + // use head k-dop shape to determine if camera is inside head. + const Rig& rig = _skeletonModel->getRig(); + int headJointIndex = rig.indexOfJoint("Head"); + if (headJointIndex >= 0) { + const HFMModel& hfmModel = _skeletonModel->getHFMModel(); + AnimPose headPose; + if (rig.getAbsoluteJointPoseInRigFrame(headJointIndex, headPose)) { + glm::vec3 displacement; + const HFMJointShapeInfo& headShapeInfo = hfmModel.joints[headJointIndex].shapeInfo; + return findPointKDopDisplacement(rigCameraPosition, headPose, headShapeInfo, displacement); + } + } + + // fall back to simple distance check. + const float RENDER_HEAD_CUTOFF_DISTANCE = 0.47f; return glm::length(cameraPosition - getHeadPosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getModelScale()); } bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { bool defaultMode = renderArgs->_renderMode == RenderArgs::DEFAULT_RENDER_MODE; bool firstPerson = qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON; + bool overrideAnim = _skeletonModel ? _skeletonModel->getRig().isPlayingOverrideAnimation() : false; bool insideHead = cameraInsideHead(renderArgs->getViewFrustum().getPosition()); - return !defaultMode || !firstPerson || !insideHead; + return !defaultMode || (!firstPerson && !insideHead) || (overrideAnim && !insideHead); } void MyAvatar::setHasScriptedBlendshapes(bool hasScriptedBlendshapes) { @@ -3889,7 +3931,8 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette // See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders QVariantMap extraInfo; EntityItemID entityID = entityTree->evalRayIntersection(startPointIn, directionIn, include, ignore, - PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE) | PickFilter::getBitMask(PickFilter::FlagBit::PRECISE)), + PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::COLLIDABLE) | PickFilter::getBitMask(PickFilter::FlagBit::PRECISE) + | PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES) | PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES)), // exclude Local entities element, distance, face, normalOut, extraInfo, lockType, accurateResult); if (entityID.isNull()) { return false; @@ -4805,7 +4848,12 @@ bool MyAvatar::isReadyForPhysics() const { } void MyAvatar::setSprintMode(bool sprint) { - _walkSpeedScalar = sprint ? AVATAR_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR; + if (qApp->isHMDMode()) { + _walkSpeedScalar = sprint ? AVATAR_DESKTOP_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR; + } + else { + _walkSpeedScalar = sprint ? AVATAR_HMD_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR; + } } void MyAvatar::setIsInWalkingState(bool isWalking) { @@ -5773,12 +5821,19 @@ void MyAvatar::releaseGrab(const QUuid& grabID) { } void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "addAvatarHandsToFlow", + Q_ARG(const std::shared_ptr&, otherAvatar)); + return; + } auto &flow = _skeletonModel->getRig().getFlow(); - for (auto &handJointName : HAND_COLLISION_JOINTS) { - int jointIndex = otherAvatar->getJointIndex(handJointName); - if (jointIndex != -1) { - glm::vec3 position = otherAvatar->getJointPosition(jointIndex); - flow.setOthersCollision(otherAvatar->getID(), jointIndex, position); + if (otherAvatar != nullptr && flow.getActive()) { + for (auto &handJointName : HAND_COLLISION_JOINTS) { + int jointIndex = otherAvatar->getJointIndex(handJointName); + if (jointIndex != -1) { + glm::vec3 position = otherAvatar->getJointPosition(jointIndex); + flow.setOthersCollision(otherAvatar->getID(), jointIndex, position); + } } } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 804e2687e7..7179c31d91 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -116,8 +116,8 @@ class MyAvatar : public Avatar { * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. * @property {string} skeletonModelURL - The avatar's FST file. - * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
- * Deprecated: Use avatar entities instead. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments. + *

Deprecated: This property is deprecated and will be removed. Use avatar entities instead.

* @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the @@ -198,7 +198,7 @@ class MyAvatar : public Avatar { * @property {Pose} rightHandTipPose - The right hand's pose as determined by the hand controllers, relative to the avatar, * with the position adjusted by 0.3m along the direction of the palm. Read-only. * - * @property {number} energy - Deprecated: This property will be removed from the API. + * @property {number} energy - Deprecated: This property will be removed. * @property {boolean} isAway - true if your avatar is away (i.e., inactive), false if it is * active. * @@ -213,8 +213,9 @@ class MyAvatar : public Avatar { * was set false because the zone may disallow collisionless avatars. * @property {boolean} otherAvatarsCollisionsEnabled - Set to true to enable the avatar to collide with other * avatars, false to disable collisions with other avatars. - * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled.
- * Deprecated: Use collisionsEnabled instead. + * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled. + *

Deprecated: This property is deprecated and will be removed. Use collisionsEnabled + * instead.

* @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings > * Controls > Walking. Note: Setting the value has no effect unless Interface is restarted. * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Controls > Show room @@ -597,6 +598,26 @@ public: */ Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + /**jsdoc + * overrideHandAnimation() Gets the overrides the default hand poses that are triggered with controller buttons. + * use {@link MyAvatar.restoreHandAnimation}.

to restore the default poses. + * @function MyAvatar.overrideHandAnimation + * @param isLeft {boolean} Set true if using the left hand + * @param url {string} The URL to the animation file. Animation files need to be FBX format, but only need to contain the + * avatar skeleton and animation data. + * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. + * @param loop {boolean} Set to true if the animation should loop. + * @param firstFrame {number} The frame the animation should start at. + * @param lastFrame {number} The frame the animation should end at + * @example Override left hand animation for three seconds. + * // Override the left hand pose then restore the default pose. + * MyAvatar.overrideHandAnimation(isLeft, ANIM_URL, 30, true, 0, 53); + * Script.setTimeout(function () { + * MyAvatar.restoreHandAnimation(); + * }, 3000); + */ + Q_INVOKABLE void overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + /**jsdoc * Restores the default animations. *

The avatar animation system includes a set of default animations along with rules for how those animations are blended @@ -615,6 +636,24 @@ public: */ Q_INVOKABLE void restoreAnimation(); + /**jsdoc + * Restores the default hand animation state machine that is driven by the state machine in the avatar-animation json. + *

The avatar animation system includes a set of default animations along with rules for how those animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will + * override the default animations. restoreHandAnimation() is used to restore the default hand poses + * If you aren't currently playing an override hand + * animation, this function has no effect.

+ * @function MyAvatar.restoreHandAnimation + * @param isLeft {boolean} Set to true if using the left hand + * @example Override left hand animation for three seconds. + * // Override the left hand pose then restore the default pose. + * MyAvatar.overrideHandAnimation(isLeft, ANIM_URL, 30, true, 0, 53); + * Script.setTimeout(function () { + * MyAvatar.restoreHandAnimation(); + * }, 3000); + */ + Q_INVOKABLE void restoreHandAnimation(bool isLeft); + /**jsdoc * Gets the current animation roles. *

Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together @@ -759,6 +798,18 @@ public: * @param {number} index */ Q_INVOKABLE void setControlScheme(int index) { _controlSchemeIndex = (index >= 0 && index <= 2) ? index : 0; } + + /**jsdoc + * @function MyAvatar.hoverWhenUnsupported + * @returns {boolean} + */ + Q_INVOKABLE bool hoverWhenUnsupported() const { return _hoverWhenUnsupported; } + /**jsdoc + * @function MyAvatar.setHoverWhenUnsupported + * @param {boolean} on + */ + Q_INVOKABLE void setHoverWhenUnsupported(bool on) { _hoverWhenUnsupported = on; } + /**jsdoc * Sets the avatar's dominant hand. * @function MyAvatar.setDominantHand @@ -1519,14 +1570,14 @@ public: * @function MyAvatar.setCharacterControllerEnabled * @param {boolean} enabled - true to enable the avatar to collide with entities, false to * disable. - * @deprecated Use {@link MyAvatar.setCollisionsEnabled} instead. + * @deprecated This function is deprecated and will be removed. Use {@link MyAvatar.setCollisionsEnabled} instead. */ Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); // deprecated /**jsdoc * @function MyAvatar.getCharacterControllerEnabled * @returns {boolean} true if the avatar will currently collide with entities, false if it won't. - * @deprecated Use {@link MyAvatar.getCollisionsEnabled} instead. + * @deprecated This function is deprecated and will be removed. Use {@link MyAvatar.getCollisionsEnabled} instead. */ Q_INVOKABLE bool getCharacterControllerEnabled(); // deprecated @@ -1872,7 +1923,7 @@ public slots: /**jsdoc * @function MyAvatar.clearScaleRestriction - * @deprecated This function is deprecated and will be removed from the API. + * @deprecated This function is deprecated and will be removed. */ void clearScaleRestriction(); @@ -1881,7 +1932,8 @@ public slots: * Adds a thrust to your avatar's current thrust to be applied for a short while. * @function MyAvatar.addThrust * @param {Vec3} thrust - The thrust direction and magnitude. - * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. + * @deprecated This function is deprecated and will be removed. Use {@link MyAvatar|MyAvatar.motorVelocity} and related + * properties instead. */ // Set/Get update the thrust that will move the avatar around void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; @@ -1890,7 +1942,8 @@ public slots: * Gets the thrust currently being applied to your avatar. * @function MyAvatar.getThrust * @returns {Vec3} The thrust currently being applied to your avatar. - * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. + * @deprecated This function is deprecated and will be removed. Use {@link MyAvatar|MyAvatar.motorVelocity} and related + * properties instead. */ glm::vec3 getThrust() { return _thrust; }; @@ -1898,7 +1951,8 @@ public slots: * Sets the thrust to be applied to your avatar for a short while. * @function MyAvatar.setThrust * @param {Vec3} thrust - The thrust direction and magnitude. - * @deprecated Use {@link MyAvatar|MyAvatar.motorVelocity} and related properties instead. + * @deprecated This function is deprecated and will be removed. Use {@link MyAvatar|MyAvatar.motorVelocity} and related + * properties instead. */ void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } @@ -2243,7 +2297,7 @@ signals: * {@link MyAvatar.setAttachmentData|setAttachmentData}. * @function MyAvatar.attachmentsChanged * @returns {Signal} - * @deprecated Use avatar entities instead. + * @deprecated This signal is deprecated and will be removed. Use avatar entities instead. */ void attachmentsChanged(); @@ -2442,6 +2496,7 @@ private: ThreadSafeValueCache _prefOverrideAnimGraphUrl; QUrl _fstAnimGraphOverrideUrl; bool _useSnapTurn { true }; + bool _hoverWhenUnsupported{ true }; ThreadSafeValueCache _dominantHand { DOMINANT_RIGHT_HAND }; ThreadSafeValueCache _hmdAvatarAlignmentType { DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE }; ThreadSafeValueCache _strafeEnabled{ DEFAULT_STRAFE_ENABLED }; @@ -2637,6 +2692,7 @@ private: Setting::Handle _displayNameSetting; Setting::Handle _collisionSoundURLSetting; Setting::Handle _useSnapTurnSetting; + Setting::Handle _hoverWhenUnsupportedSetting; Setting::Handle _userHeightSetting; Setting::Handle _flyingHMDSetting; Setting::Handle _movementReferenceSetting; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 55c29b66c1..df46b428e7 100755 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -334,7 +334,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { eyeParams.leftEyeJointIndex = _rig.indexOfJoint("LeftEye"); eyeParams.rightEyeJointIndex = _rig.indexOfJoint("RightEye"); - _rig.updateFromEyeParameters(eyeParams); + if (_owningAvatar->getHasProceduralEyeFaceMovement()) { + _rig.updateFromEyeParameters(eyeParams); + } updateFingers(); } diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 2e775a20c3..d8cfe8f107 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -16,6 +16,7 @@ #include "Application.h" #include "AvatarMotionState.h" #include "DetailedMotionState.h" +#include "DebugDraw.h" const float DISPLAYNAME_FADE_TIME = 0.5f; const float DISPLAYNAME_FADE_FACTOR = pow(0.01f, 1.0f / DISPLAYNAME_FADE_TIME); @@ -358,6 +359,58 @@ void OtherAvatar::simulate(float deltaTime, bool inView) { } } +void OtherAvatar::debugJointData() const { + // Get a copy of the joint data + auto jointData = getJointData(); + auto skeletonData = getSkeletonData(); + if ((int)skeletonData.size() == jointData.size() && jointData.size() != 0) { + const vec4 RED(1.0f, 0.0f, 0.0f, 1.0f); + const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f); + const vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); + const vec4 LIGHT_RED(1.0f, 0.5f, 0.5f, 1.0f); + const vec4 LIGHT_GREEN(0.5f, 1.0f, 0.5f, 1.0f); + const vec4 LIGHT_BLUE(0.5f, 0.5f, 1.0f, 1.0f); + const vec4 GREY(0.3f, 0.3f, 0.3f, 1.0f); + const vec4 WHITE(1.0f, 1.0f, 1.0f, 1.0f); + const float AXIS_LENGTH = 0.1f; + + AnimPoseVec absoluteJointPoses; + AnimPose rigToAvatar = AnimPose(Quaternions::Y_180 * getWorldOrientation(), getWorldPosition()); + bool drawBones = false; + for (int i = 0; i < jointData.size(); i++) { + float jointScale = skeletonData[i].defaultScale * getTargetScale() * METERS_PER_CENTIMETER; + auto absoluteRotation = jointData[i].rotationIsDefaultPose ? skeletonData[i].defaultRotation : jointData[i].rotation; + auto localJointTranslation = jointScale * (jointData[i].translationIsDefaultPose ? skeletonData[i].defaultTranslation : jointData[i].translation); + bool isHips = skeletonData[i].jointName == "Hips"; + if (isHips) { + localJointTranslation = glm::vec3(0.0f); + drawBones = true; + } + AnimPose absoluteParentPose; + int parentIndex = skeletonData[i].parentIndex; + if (parentIndex != -1 && parentIndex < (int)absoluteJointPoses.size()) { + absoluteParentPose = absoluteJointPoses[parentIndex]; + } + AnimPose absoluteJointPose = AnimPose(absoluteRotation, absoluteParentPose.trans() + absoluteParentPose.rot() * localJointTranslation); + auto jointPose = rigToAvatar * absoluteJointPose; + auto parentPose = rigToAvatar * absoluteParentPose; + if (drawBones) { + glm::vec3 xAxis = jointPose.rot() * Vectors::UNIT_X; + glm::vec3 yAxis = jointPose.rot() * Vectors::UNIT_Y; + glm::vec3 zAxis = jointPose.rot() * Vectors::UNIT_Z; + + DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * xAxis, jointData[i].rotationIsDefaultPose ? LIGHT_RED : RED); + DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * yAxis, jointData[i].rotationIsDefaultPose ? LIGHT_GREEN : GREEN); + DebugDraw::getInstance().drawRay(jointPose.trans(), jointPose.trans() + AXIS_LENGTH * zAxis, jointData[i].rotationIsDefaultPose ? LIGHT_BLUE : BLUE); + if (!isHips) { + DebugDraw::getInstance().drawRay(jointPose.trans(), parentPose.trans(), jointData[i].translationIsDefaultPose ? WHITE : GREY); + } + } + absoluteJointPoses.push_back(absoluteJointPose); + } + } +} + void OtherAvatar::handleChangedAvatarEntityData() { PerformanceTimer perfTimer("attachments"); diff --git a/interface/src/avatar/OtherAvatar.h b/interface/src/avatar/OtherAvatar.h index 7669f44806..43bfd2a9ae 100644 --- a/interface/src/avatar/OtherAvatar.h +++ b/interface/src/avatar/OtherAvatar.h @@ -66,7 +66,7 @@ public: void setCollisionWithOtherAvatarsFlags() override; void simulate(float deltaTime, bool inView) override; - + void debugJointData() const; friend AvatarManager; protected: diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 37f28960e5..ea2de73db3 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -41,307 +41,246 @@ #include "ui/SecurityImageProvider.h" #include "scripting/HMDScriptingInterface.h" -static const char* KEY_FILE = "hifikey"; -static const char* INSTRUCTIONS_FILE = "backup_instructions.html"; -static const char* IMAGE_HEADER = "-----BEGIN SECURITY IMAGE-----\n"; -static const char* IMAGE_FOOTER = "-----END SECURITY IMAGE-----\n"; +namespace { + const char* KEY_FILE = "hifikey"; + const char* INSTRUCTIONS_FILE = "backup_instructions.html"; + const char* IMAGE_HEADER = "-----BEGIN SECURITY IMAGE-----\n"; + const char* IMAGE_FOOTER = "-----END SECURITY IMAGE-----\n"; -void initialize() { - static bool initialized = false; - if (!initialized) { - SSL_load_error_strings(); - SSL_library_init(); - OpenSSL_add_all_algorithms(); - initialized = true; - } -} - -QString keyFilePath() { - auto accountManager = DependencyManager::get(); - return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE)); -} -bool Wallet::copyKeyFileFrom(const QString& pathname) { - QString existing = getKeyFilePath(); - qCDebug(commerce) << "Old keyfile" << existing; - if (!existing.isEmpty()) { - QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1, - QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", "")); - qCDebug(commerce) << "Renaming old keyfile to" << backup; - if (!QFile::rename(existing, backup)) { - qCCritical(commerce) << "Unable to backup" << existing << "to" << backup; - return false; + void initialize() { + static bool initialized = false; + if (!initialized) { + SSL_load_error_strings(); + SSL_library_init(); + OpenSSL_add_all_algorithms(); + initialized = true; } } - QString destination = keyFilePath(); - bool result = QFile::copy(pathname, destination); - qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result; - return result; -} -// use the cached _passphrase if it exists, otherwise we need to prompt -int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) { - // just return a hardcoded pwd for now - auto wallet = DependencyManager::get(); - auto passphrase = wallet->getPassphrase(); - if (passphrase && !passphrase->isEmpty()) { - QString saltedPassphrase(*passphrase); - saltedPassphrase.append(wallet->getSalt()); - strcpy(password, saltedPassphrase.toUtf8().constData()); - return static_cast(passphrase->size()); - } else { - // this shouldn't happen - so lets log it to tell us we have - // a problem with the flow... - qCCritical(commerce) << "no cached passphrase while decrypting!"; - return 0; + QString keyFilePath() { + auto accountManager = DependencyManager::get(); + return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE)); } -} -EC_KEY* readKeys(const char* filename) { - FILE* fp; - EC_KEY *key = NULL; - if ((fp = fopen(filename, "rt"))) { - // file opened successfully - qCDebug(commerce) << "opened key file" << filename; + // use the cached _passphrase if it exists, otherwise we need to prompt + int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) { + // just return a hardcoded pwd for now + auto wallet = DependencyManager::get(); + auto passphrase = wallet->getPassphrase(); + if (passphrase && !passphrase->isEmpty()) { + QString saltedPassphrase(*passphrase); + saltedPassphrase.append(wallet->getSalt()); + strcpy(password, saltedPassphrase.toUtf8().constData()); + return static_cast(passphrase->size()); + } else { + // this shouldn't happen - so lets log it to tell us we have + // a problem with the flow... + qCCritical(commerce) << "no cached passphrase while decrypting!"; + return 0; + } + } - if ((key = PEM_read_EC_PUBKEY(fp, NULL, NULL, NULL))) { - // now read private key + EC_KEY* readKeys(QString filename) { + QFile file(filename); + EC_KEY* key = NULL; + if (file.open(QFile::ReadOnly)) { + // file opened successfully + qCDebug(commerce) << "opened key file" << filename; - qCDebug(commerce) << "read public key"; + QByteArray pemKeyBytes = file.readAll(); + BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); + if ((key = PEM_read_bio_EC_PUBKEY(bufio, NULL, NULL, NULL))) { + // now read private key - if ((key = PEM_read_ECPrivateKey(fp, &key, passwordCallback, NULL))) { - qCDebug(commerce) << "read private key"; - fclose(fp); - return key; + qCDebug(commerce) << "read public key"; + + if ((key = PEM_read_bio_ECPrivateKey(bufio, &key, passwordCallback, NULL))) { + qCDebug(commerce) << "read private key"; + } else { + qCDebug(commerce) << "failed to read private key"; + } + } else { + qCDebug(commerce) << "failed to read public key"; } - qCDebug(commerce) << "failed to read private key"; + BIO_free(bufio); + file.close(); } else { - qCDebug(commerce) << "failed to read public key"; + qCDebug(commerce) << "failed to open key file" << filename; } - fclose(fp); - } else { - qCDebug(commerce) << "failed to open key file" << filename; - } - return key; -} - -bool Wallet::writeBackupInstructions() { - QString inputFilename(PathUtils::resourcesPath() + "html/commerce/backup_instructions.html"); - QString outputFilename = PathUtils::getAppDataFilePath(INSTRUCTIONS_FILE); - QFile inputFile(inputFilename); - QFile outputFile(outputFilename); - bool retval = false; - - if (getKeyFilePath().isEmpty()) - { - return false; + return key; } - if (QFile::exists(inputFilename) && inputFile.open(QIODevice::ReadOnly)) { - if (outputFile.open(QIODevice::ReadWrite)) { - // Read the data from the original file, then close it - QByteArray fileData = inputFile.readAll(); - inputFile.close(); - - // Translate the data from the original file into a QString - QString text(fileData); - - // Replace the necessary string - text.replace(QString("HIFIKEY_PATH_REPLACEME"), keyFilePath()); - - // Write the new text back to the file - outputFile.write(text.toUtf8()); - - // Close the output file - outputFile.close(); - - retval = true; - qCDebug(commerce) << "wrote html file successfully"; - } else { - qCDebug(commerce) << "failed to open output html file" << outputFilename; - } - } else { - qCDebug(commerce) << "failed to open input html file" << inputFilename; - } - return retval; -} - -bool writeKeys(const char* filename, EC_KEY* keys) { - FILE* fp; - bool retval = false; - if ((fp = fopen(filename, "wt"))) { - if (!PEM_write_EC_PUBKEY(fp, keys)) { - fclose(fp); + bool writeKeys(QString filename, EC_KEY* keys) { + BIO* bio = BIO_new(BIO_s_mem()); + bool retval = false; + if (!PEM_write_bio_EC_PUBKEY(bio, keys)) { + BIO_free(bio); qCCritical(commerce) << "failed to write public key"; return retval; } - if (!PEM_write_ECPrivateKey(fp, keys, EVP_des_ede3_cbc(), NULL, 0, passwordCallback, NULL)) { - fclose(fp); + if (!PEM_write_bio_ECPrivateKey(bio, keys, EVP_des_ede3_cbc(), NULL, 0, passwordCallback, NULL)) { + BIO_free(bio); qCCritical(commerce) << "failed to write private key"; return retval; } - retval = true; - qCDebug(commerce) << "wrote keys successfully"; - fclose(fp); - } else { - qCDebug(commerce) << "failed to open key file" << filename; - } - return retval; -} + QFile file(filename); + if (file.open(QIODevice::WriteOnly)) { + const char* bio_data; + long bio_size = BIO_get_mem_data(bio, &bio_data); -bool Wallet::setWallet(const QByteArray& wallet) { - QFile file(keyFilePath()); - if (!file.open(QIODevice::WriteOnly)) { - qCCritical(commerce) << "Unable to open wallet for write in" << keyFilePath(); - return false; - } - if (file.write(wallet) != wallet.count()) { - qCCritical(commerce) << "Unable to write wallet in" << keyFilePath(); - return false; - } - file.close(); - return true; -} -QByteArray Wallet::getWallet() { - QFile file(keyFilePath()); - if (!file.open(QIODevice::ReadOnly)) { - qCInfo(commerce) << "No existing wallet in" << keyFilePath(); - return QByteArray(); - } - QByteArray wallet = file.readAll(); - file.close(); - return wallet; -} - -QPair generateECKeypair() { - - EC_KEY* keyPair = EC_KEY_new_by_curve_name(NID_secp256k1); - QPair retval{}; - - EC_KEY_set_asn1_flag(keyPair, OPENSSL_EC_NAMED_CURVE); - if (!EC_KEY_generate_key(keyPair)) { - qCDebug(commerce) << "Error generating EC Keypair -" << ERR_get_error(); + QByteArray keyBytes(bio_data, bio_size); + file.write(keyBytes); + retval = true; + qCDebug(commerce) << "wrote keys successfully"; + file.close(); + } else { + qCDebug(commerce) << "failed to open key file" << filename; + } + BIO_free(bio); return retval; } - // grab the public key and private key from the file - unsigned char* publicKeyDER = NULL; - int publicKeyLength = i2d_EC_PUBKEY(keyPair, &publicKeyDER); + QPair generateECKeypair() { + EC_KEY* keyPair = EC_KEY_new_by_curve_name(NID_secp256k1); + QPair retval {}; - unsigned char* privateKeyDER = NULL; - int privateKeyLength = i2d_ECPrivateKey(keyPair, &privateKeyDER); + EC_KEY_set_asn1_flag(keyPair, OPENSSL_EC_NAMED_CURVE); + if (!EC_KEY_generate_key(keyPair)) { + qCDebug(commerce) << "Error generating EC Keypair -" << ERR_get_error(); + return retval; + } - if (publicKeyLength <= 0 || privateKeyLength <= 0) { - qCDebug(commerce) << "Error getting DER public or private key from EC struct -" << ERR_get_error(); + // grab the public key and private key from the file + unsigned char* publicKeyDER = NULL; + int publicKeyLength = i2d_EC_PUBKEY(keyPair, &publicKeyDER); + unsigned char* privateKeyDER = NULL; + int privateKeyLength = i2d_ECPrivateKey(keyPair, &privateKeyDER); + + if (publicKeyLength <= 0 || privateKeyLength <= 0) { + qCDebug(commerce) << "Error getting DER public or private key from EC struct -" << ERR_get_error(); + + // cleanup the EC struct + EC_KEY_free(keyPair); + + // cleanup the public and private key DER data, if required + if (publicKeyLength > 0) { + OPENSSL_free(publicKeyDER); + } + + if (privateKeyLength > 0) { + OPENSSL_free(privateKeyDER); + } + + return retval; + } + + if (!writeKeys(keyFilePath(), keyPair)) { + qCDebug(commerce) << "couldn't save keys!"; + return retval; + } - // cleanup the EC struct EC_KEY_free(keyPair); - // cleanup the public and private key DER data, if required - if (publicKeyLength > 0) { - OPENSSL_free(publicKeyDER); - } - - if (privateKeyLength > 0) { - OPENSSL_free(privateKeyDER); - } + // prepare the return values. TODO: Fix this - we probably don't really even want the + // private key at all (better to read it when we need it?). Or maybe we do, when we have + // multiple keys? + retval.first = new QByteArray(reinterpret_cast(publicKeyDER), publicKeyLength); + retval.second = new QByteArray(reinterpret_cast(privateKeyDER), privateKeyLength); + // cleanup the publicKeyDER and publicKeyDER data + OPENSSL_free(publicKeyDER); + OPENSSL_free(privateKeyDER); return retval; } + // END copied code (which will soon change) + // the public key can just go into a byte array + QByteArray readPublicKey(QString filename) { + QByteArray retval; + QFile file(filename); + if (file.open(QIODevice::ReadOnly)) { + // file opened successfully + qCDebug(commerce) << "opened key file" << filename; - if (!writeKeys(keyFilePath().toStdString().c_str(), keyPair)) { - qCDebug(commerce) << "couldn't save keys!"; - return retval; - } + QByteArray pemKeyBytes = file.readAll(); + BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); - EC_KEY_free(keyPair); + EC_KEY* key = PEM_read_bio_EC_PUBKEY(bufio, NULL, NULL, NULL); + if (key) { + // file read successfully + unsigned char* publicKeyDER = NULL; + int publicKeyLength = i2d_EC_PUBKEY(key, &publicKeyDER); + // TODO: check for 0 length? - // prepare the return values. TODO: Fix this - we probably don't really even want the - // private key at all (better to read it when we need it?). Or maybe we do, when we have - // multiple keys? - retval.first = new QByteArray(reinterpret_cast(publicKeyDER), publicKeyLength); - retval.second = new QByteArray(reinterpret_cast(privateKeyDER), privateKeyLength); + // cleanup + EC_KEY_free(key); - // cleanup the publicKeyDER and publicKeyDER data - OPENSSL_free(publicKeyDER); - OPENSSL_free(privateKeyDER); - return retval; -} -// END copied code (which will soon change) + qCDebug(commerce) << "parsed public key file successfully"; -// the public key can just go into a byte array -QByteArray readPublicKey(const char* filename) { - FILE* fp; - EC_KEY* key = NULL; - if ((fp = fopen(filename, "r"))) { - // file opened successfully - qCDebug(commerce) << "opened key file" << filename; - if ((key = PEM_read_EC_PUBKEY(fp, NULL, NULL, NULL))) { - // file read successfully - unsigned char* publicKeyDER = NULL; - int publicKeyLength = i2d_EC_PUBKEY(key, &publicKeyDER); - // TODO: check for 0 length? - - // cleanup - EC_KEY_free(key); - fclose(fp); - - qCDebug(commerce) << "parsed public key file successfully"; - - QByteArray retval((char*)publicKeyDER, publicKeyLength); - OPENSSL_free(publicKeyDER); - return retval; + QByteArray retval((char*)publicKeyDER, publicKeyLength); + OPENSSL_free(publicKeyDER); + BIO_free(bufio); + file.close(); + return retval; + } else { + qCDebug(commerce) << "couldn't parse" << filename; + } + BIO_free(bufio); + file.close(); } else { - qCDebug(commerce) << "couldn't parse" << filename; + qCDebug(commerce) << "couldn't open" << filename; } - fclose(fp); - } else { - qCDebug(commerce) << "couldn't open" << filename; + return QByteArray(); } - return QByteArray(); -} -// the private key should be read/copied into heap memory. For now, we need the EC_KEY struct -// so I'll return that. -EC_KEY* readPrivateKey(const char* filename) { - FILE* fp; - EC_KEY* key = NULL; - if ((fp = fopen(filename, "r"))) { - // file opened successfully - qCDebug(commerce) << "opened key file" << filename; - if ((key = PEM_read_ECPrivateKey(fp, &key, passwordCallback, NULL))) { - qCDebug(commerce) << "parsed private key file successfully"; + // the private key should be read/copied into heap memory. For now, we need the EC_KEY struct + // so I'll return that. + EC_KEY* readPrivateKey(QString filename) { + QFile file(filename); + EC_KEY* key = NULL; + if (file.open(QIODevice::ReadOnly)) { + // file opened successfully + qCDebug(commerce) << "opened key file" << filename; + QByteArray pemKeyBytes = file.readAll(); + BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); + + if ((key = PEM_read_bio_ECPrivateKey(bufio, &key, passwordCallback, NULL))) { + qCDebug(commerce) << "parsed private key file successfully"; + + } else { + qCDebug(commerce) << "couldn't parse" << filename; + // if the passphrase is wrong, then let's not cache it + DependencyManager::get()->setPassphrase(""); + } + BIO_free(bufio); + file.close(); } else { - qCDebug(commerce) << "couldn't parse" << filename; - // if the passphrase is wrong, then let's not cache it - DependencyManager::get()->setPassphrase(""); + qCDebug(commerce) << "couldn't open" << filename; } - fclose(fp); - } else { - qCDebug(commerce) << "couldn't open" << filename; + return key; } - return key; -} -// QT's QByteArray will convert to Base64 without any embedded newlines. This just -// writes it with embedded newlines, which is more readable. -void outputBase64WithNewlines(QFile& file, const QByteArray& b64Array) { - for (int i = 0; i < b64Array.size(); i += 64) { - file.write(b64Array.mid(i, 64)); - file.write("\n"); + // QT's QByteArray will convert to Base64 without any embedded newlines. This just + // writes it with embedded newlines, which is more readable. + void outputBase64WithNewlines(QFile& file, const QByteArray& b64Array) { + for (int i = 0; i < b64Array.size(); i += 64) { + file.write(b64Array.mid(i, 64)); + file.write("\n"); + } } -} -void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArray& salt) { - // use the ones in the wallet - auto wallet = DependencyManager::get(); - memcpy(ivec, wallet->getIv(), 16); - memcpy(ckey, wallet->getCKey(), 32); -} + void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArray& salt) { + // use the ones in the wallet + auto wallet = DependencyManager::get(); + memcpy(ivec, wallet->getIv(), 16); + memcpy(ckey, wallet->getCKey(), 32); + } + +} // close unnamed namespace Wallet::Wallet() { auto nodeList = DependencyManager::get(); @@ -361,7 +300,7 @@ Wallet::Wallet() { if (wallet->getKeyFilePath().isEmpty() || !wallet->getSecurityImage()) { if (keyStatus == "preexisting") { status = (uint) WalletStatus::WALLET_STATUS_PREEXISTING; - } else{ + } else { status = (uint) WalletStatus::WALLET_STATUS_NOT_SET_UP; } } else if (!wallet->walletIsAuthenticatedWithPassphrase()) { @@ -371,7 +310,6 @@ Wallet::Wallet() { } else { status = (uint) WalletStatus::WALLET_STATUS_READY; } - walletScriptingInterface->setWalletStatus(status); }); @@ -405,6 +343,88 @@ Wallet::~Wallet() { } } +bool Wallet::setWallet(const QByteArray& wallet) { + QFile file(keyFilePath()); + if (!file.open(QIODevice::WriteOnly)) { + qCCritical(commerce) << "Unable to open wallet for write in" << keyFilePath(); + return false; + } + if (file.write(wallet) != wallet.count()) { + qCCritical(commerce) << "Unable to write wallet in" << keyFilePath(); + return false; + } + file.close(); + return true; +} +QByteArray Wallet::getWallet() { + QFile file(keyFilePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCInfo(commerce) << "No existing wallet in" << keyFilePath(); + return QByteArray(); + } + QByteArray wallet = file.readAll(); + file.close(); + return wallet; +} + +bool Wallet::copyKeyFileFrom(const QString& pathname) { + QString existing = getKeyFilePath(); + qCDebug(commerce) << "Old keyfile" << existing; + if (!existing.isEmpty()) { + QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1, + QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", "")); + qCDebug(commerce) << "Renaming old keyfile to" << backup; + if (!QFile::rename(existing, backup)) { + qCCritical(commerce) << "Unable to backup" << existing << "to" << backup; + return false; + } + } + QString destination = keyFilePath(); + bool result = QFile::copy(pathname, destination); + qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result; + return result; +} + +bool Wallet::writeBackupInstructions() { + QString inputFilename(PathUtils::resourcesPath() + "html/commerce/backup_instructions.html"); + QString outputFilename = PathUtils::getAppDataFilePath(INSTRUCTIONS_FILE); + QFile inputFile(inputFilename); + QFile outputFile(outputFilename); + bool retval = false; + + if (getKeyFilePath().isEmpty()) { + return false; + } + + if (QFile::exists(inputFilename) && inputFile.open(QIODevice::ReadOnly)) { + if (outputFile.open(QIODevice::ReadWrite)) { + // Read the data from the original file, then close it + QByteArray fileData = inputFile.readAll(); + inputFile.close(); + + // Translate the data from the original file into a QString + QString text(fileData); + + // Replace the necessary string + text.replace(QString("HIFIKEY_PATH_REPLACEME"), keyFilePath()); + + // Write the new text back to the file + outputFile.write(text.toUtf8()); + + // Close the output file + outputFile.close(); + + retval = true; + qCDebug(commerce) << "wrote html file successfully"; + } else { + qCDebug(commerce) << "failed to open output html file" << outputFilename; + } + } else { + qCDebug(commerce) << "failed to open input html file" << inputFilename; + } + return retval; +} + bool Wallet::setPassphrase(const QString& passphrase) { if (_passphrase) { delete _passphrase; @@ -569,10 +589,10 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() { } // otherwise, we have a passphrase but no keys, so we have to check - auto publicKey = readPublicKey(keyFilePath().toStdString().c_str()); + auto publicKey = readPublicKey(keyFilePath()); if (publicKey.size() > 0) { - if (auto key = readPrivateKey(keyFilePath().toStdString().c_str())) { + if (auto key = readPrivateKey(keyFilePath())) { EC_KEY_free(key); // be sure to add the public key so we don't do this over and over @@ -631,8 +651,7 @@ QStringList Wallet::listPublicKeys() { QString Wallet::signWithKey(const QByteArray& text, const QString& key) { EC_KEY* ecPrivateKey = NULL; - auto keyFilePathString = keyFilePath().toStdString(); - if ((ecPrivateKey = readPrivateKey(keyFilePath().toStdString().c_str()))) { + if ((ecPrivateKey = readPrivateKey(keyFilePath()))) { unsigned char* sig = new unsigned char[ECDSA_size(ecPrivateKey)]; unsigned int signatureBytes = 0; @@ -641,12 +660,8 @@ QString Wallet::signWithKey(const QByteArray& text, const QString& key) { QByteArray hashedPlaintext = QCryptographicHash::hash(text, QCryptographicHash::Sha256); - - int retrn = ECDSA_sign(0, - reinterpret_cast(hashedPlaintext.constData()), - hashedPlaintext.size(), - sig, - &signatureBytes, ecPrivateKey); + int retrn = ECDSA_sign(0, reinterpret_cast(hashedPlaintext.constData()), hashedPlaintext.size(), + sig, &signatureBytes, ecPrivateKey); EC_KEY_free(ecPrivateKey); QByteArray signature(reinterpret_cast(sig), signatureBytes); @@ -682,7 +697,6 @@ void Wallet::updateImageProvider() { } void Wallet::chooseSecurityImage(const QString& filename) { - if (_securityImage) { delete _securityImage; } @@ -754,7 +768,7 @@ QString Wallet::getKeyFilePath() { } bool Wallet::writeWallet(const QString& newPassphrase) { - EC_KEY* keys = readKeys(keyFilePath().toStdString().c_str()); + EC_KEY* keys = readKeys(keyFilePath()); auto ledger = DependencyManager::get(); // Remove any existing locker, because it will be out of date. if (!_publicKeys.isEmpty() && !ledger->receiveAt(_publicKeys.first(), _publicKeys.first(), QByteArray())) { @@ -768,7 +782,7 @@ bool Wallet::writeWallet(const QString& newPassphrase) { setPassphrase(newPassphrase); } - if (writeKeys(tempFileName.toStdString().c_str(), keys)) { + if (writeKeys(tempFileName, keys)) { if (writeSecurityImage(_securityImage, tempFileName)) { // ok, now move the temp file to the correct spot QFile(QString(keyFilePath())).remove(); @@ -834,10 +848,10 @@ void Wallet::handleChallengeOwnershipPacket(QSharedPointer pack challengingNodeUUID = packet->read(challengingNodeUUIDByteArraySize); } - EC_KEY* ec = readKeys(keyFilePath().toStdString().c_str()); + EC_KEY* ec = readKeys(keyFilePath()); QString sig; - if (ec) { + if (ec) { ERR_clear_error(); sig = signWithKey(text, ""); // base64 signature, QByteArray cast (on return) to QString FIXME should pass ec as string so we can tell which key to sign with status = 1; diff --git a/interface/src/graphics/GraphicsEngine.cpp b/interface/src/graphics/GraphicsEngine.cpp index c2137d3d97..267822baf2 100644 --- a/interface/src/graphics/GraphicsEngine.cpp +++ b/interface/src/graphics/GraphicsEngine.cpp @@ -244,6 +244,7 @@ void GraphicsEngine::render_performFrame() { finalFramebuffer = framebufferCache->getFramebuffer(); } + std::queue snapshotOperators; if (!_programsCompiled.load()) { gpu::doInBatch("splashFrame", _gpuContext, [&](gpu::Batch& batch) { batch.setFramebuffer(finalFramebuffer); @@ -271,6 +272,7 @@ void GraphicsEngine::render_performFrame() { PROFILE_RANGE(render, "/runRenderFrame"); renderArgs._hudOperator = displayPlugin->getHUDOperator(); renderArgs._hudTexture = qApp->getApplicationOverlay().getOverlayTexture(); + renderArgs._takingSnapshot = qApp->takeSnapshotOperators(snapshotOperators); renderArgs._blitFramebuffer = finalFramebuffer; render_runRenderFrame(&renderArgs); } @@ -285,6 +287,7 @@ void GraphicsEngine::render_performFrame() { frameBufferCache->releaseFramebuffer(framebuffer); } }; + frame->snapshotOperators = snapshotOperators; // deliver final scene rendering commands to the display plugin { PROFILE_RANGE(render, "/pluginOutput"); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 5af0a9371d..b2be010544 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -302,8 +302,11 @@ int main(int argc, const char* argv[]) { PROFILE_SYNC_BEGIN(startup, "app full ctor", ""); Application app(argcExtended, const_cast(argvExtended.data()), startupTime, runningMarkerExisted); PROFILE_SYNC_END(startup, "app full ctor", ""); - - + +#if defined(Q_OS_LINUX) + app.setWindowIcon(QIcon(PathUtils::resourcesPath() + "images/hifi-logo.svg")); +#endif + QTimer exitTimer; if (traceDuration > 0.0f) { exitTimer.setSingleShot(true); diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp index bd746c9090..12daae0351 100644 --- a/interface/src/raypick/LaserPointer.cpp +++ b/interface/src/raypick/LaserPointer.cpp @@ -233,16 +233,19 @@ PointerEvent LaserPointer::buildPointerEvent(const PickedObject& target, const P // If we just started triggering and we haven't moved too much, don't update intersection and pos2D TriggerState& state = hover ? _latestState : _states[button]; - float sensorToWorldScale = DependencyManager::get()->getMyAvatar()->getSensorToWorldScale(); - float deadspotSquared = TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED * sensorToWorldScale * sensorToWorldScale; - bool withinDeadspot = usecTimestampNow() - state.triggerStartTime < POINTER_MOVE_DELAY && glm::distance2(pos2D, state.triggerPos2D) < deadspotSquared; - if ((state.triggering || state.wasTriggering) && !state.deadspotExpired && withinDeadspot) { - pos2D = state.triggerPos2D; - intersection = state.intersection; - surfaceNormal = state.surfaceNormal; - } - if (!withinDeadspot) { - state.deadspotExpired = true; + auto avatar = DependencyManager::get()->getMyAvatar(); + if (avatar) { + float sensorToWorldScale = avatar->getSensorToWorldScale(); + float deadspotSquared = TOUCH_PRESS_TO_MOVE_DEADSPOT_SQUARED * sensorToWorldScale * sensorToWorldScale; + bool withinDeadspot = usecTimestampNow() - state.triggerStartTime < POINTER_MOVE_DELAY && glm::distance2(pos2D, state.triggerPos2D) < deadspotSquared; + if ((state.triggering || state.wasTriggering) && !state.deadspotExpired && withinDeadspot) { + pos2D = state.triggerPos2D; + intersection = state.intersection; + surfaceNormal = state.surfaceNormal; + } + if (!withinDeadspot) { + state.deadspotExpired = true; + } } return PointerEvent(pos2D, intersection, surfaceNormal, direction); diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index caae946116..b406c097e7 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -174,14 +174,10 @@ void Audio::setPTTDesktop(bool enabled) { _pttDesktop = enabled; } }); - if (!enabled) { - // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. - setMutedDesktop(true); - } else { - // Should be muted when not pushing to talk while PTT is enabled. + if (enabled || _settingsLoaded) { + // Set to default behavior (muted for Desktop) on Push-To-Talk disable or when enabled. Settings also need to be loaded. setMutedDesktop(true); } - if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkDesktopChanged(enabled); @@ -202,12 +198,9 @@ void Audio::setPTTHMD(bool enabled) { _pttHMD = enabled; } }); - if (!enabled) { - // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. - setMutedHMD(false); - } else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedHMD(true); + if (enabled || _settingsLoaded) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable or muted for when PTT is enabled. + setMutedHMD(enabled); } if (changed) { @@ -231,6 +224,7 @@ void Audio::loadData() { auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); + _settingsLoaded = true; } bool Audio::getPTTHMD() const { @@ -357,10 +351,12 @@ void Audio::onContextChanged() { changed = true; } }); - if (isHMD) { - setMuted(getMutedHMD()); - } else { - setMuted(getMutedDesktop()); + if (_settingsLoaded) { + bool isMuted = isHMD ? getMutedHMD() : getMutedDesktop(); + setMuted(isMuted); + // always set audio client muted state on context changed - sometimes setMuted does not catch it. + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); } if (changed) { emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 00da566b30..ed54dca5c6 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -40,25 +40,40 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { * @hifi-server-entity * @hifi-assignment-client * - * @property {boolean} muted - true if the audio input is muted, otherwise false. - * @property {boolean} mutedDesktop - true if the audio input is muted, otherwise false. + * @property {boolean} muted - true if the audio input is muted for the current user context (desktop or HMD), + * otherwise false. + * @property {boolean} mutedDesktop - true if desktop audio input is muted, otherwise false. + * @property {boolean} mutedHMD - true if the HMD input is muted, otherwise false. + * @property {boolean} warnWhenMuted - true if the "muted" warning is enabled, otherwise false. + * When enabled, if you speak while your microphone is muted, "muted" is displayed on the screen as a warning. * @property {boolean} noiseReduction - true if noise reduction is enabled, otherwise false. When * enabled, the input audio signal is blocked (fully attenuated) when it falls below an adaptive threshold set just * above the noise floor. + * @property {number} inputVolume - Adjusts the volume of the input audio, range 0.01.0. + * If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some + * devices, and others might only support values of 0.0 and 1.0. * @property {number} inputLevel - The loudness of the audio input, range 0.0 (no sound) – * 1.0 (the onset of clipping). Read-only. * @property {boolean} clipping - true if the audio input is clipping, otherwise false. - * @property {number} inputVolume - Adjusts the volume of the input audio; range 0.01.0. - * If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some - * devices, and others might only support values of 0.0 and 1.0. - * @property {boolean} isStereoInput - true if the input audio is being used in stereo, otherwise - * false. Some devices do not support stereo, in which case the value is always false. * @property {string} context - The current context of the audio: either "Desktop" or "HMD". * Read-only. - * @property {object} devices Read-only. Deprecated: This property is deprecated and will be - * removed. - * @property {boolean} isSoloing Read-only. true if any nodes are soloed. - * @property {Uuid[]} soloList Read-only. Get the list of currently soloed node UUIDs. + * @property {object} devices - Read-only. + *

Deprecated: This property is deprecated and will be removed. + * @property {boolean} pushToTalk - true if push-to-talk is enabled for the current user context (desktop or + * HMD), otherwise false. + * @property {boolean} pushToTalkDesktop - true if desktop push-to-talk is enabled, otherwise + * false. + * @property {boolean} pushToTalkHMD - true if HMD push-to-talk is enabled, otherwise false. + * @property {boolean} pushingToTalk - true if the user is currently pushing-to-talk, otherwise + * false. + * + * @comment The following properties are from AudioScriptingInterface.h. + * @property {boolean} isStereoInput - true if the input audio is being used in stereo, otherwise + * false. Some devices do not support stereo, in which case the value is always false. + * @property {boolean} isSoloing - true if currently audio soloing, i.e., playing audio from only specific + * avatars. Read-only. + * @property {Uuid[]} soloList - The list of currently soloed avatar IDs. Empty list if not currently audio soloing. + * Read-only. */ Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged) @@ -117,23 +132,23 @@ public: /**jsdoc * @function Audio.setInputDevice - * @param {object} device - * @param {boolean} isHMD + * @param {object} device - Device. + * @param {boolean} isHMD - Is HMD. * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device, bool isHMD); /**jsdoc * @function Audio.setOutputDevice - * @param {object} device - * @param {boolean} isHMD + * @param {object} device - Device. + * @param {boolean} isHMD - Is HMD. * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device, bool isHMD); /**jsdoc - * Enable or disable reverberation. Reverberation is done by the client, on the post-mix audio. The reverberation options - * come from either the domain's audio zone if used — configured on the server — or as scripted by + * Enables or disables reverberation. Reverberation is done by the client on the post-mix audio. The reverberation options + * come from either the domain's audio zone configured on the server or settings scripted by * {@link Audio.setReverbOptions|setReverbOptions}. * @function Audio.setReverb * @param {boolean} enable - true to enable reverberation, false to disable. @@ -165,69 +180,71 @@ public: Q_INVOKABLE void setReverb(bool enable); /**jsdoc - * Configure reverberation options. Use {@link Audio.setReverb|setReverb} to enable or disable reverberation. + * Configures reverberation options. Use {@link Audio.setReverb|setReverb} to enable or disable reverberation. * @function Audio.setReverbOptions * @param {AudioEffectOptions} options - The reverberation options. */ Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); /**jsdoc - * Sets the avatar gain at the server. - * Units are Decibels (dB) + * Sets the gain (relative volume) that avatars' voices are played at. This gain is used at the server. * @function Audio.setAvatarGain - * @param {number} gain (in dB) - */ + * @param {number} gain - Avatar gain (dB) at the server. + */ Q_INVOKABLE void setAvatarGain(float gain); /**jsdoc - * Gets the avatar gain at the server. + * Gets the gain (relative volume) that avatars' voices are played at. This gain is used at the server. * @function Audio.getAvatarGain - * @returns {number} gain (in dB) - */ + * @returns {number} Avatar gain (dB) at the server. + * @example Report current audio gain settings. + * // 0 value = normal volume; -ve value = quieter; +ve value = louder. + * print("Avatar gain: " + Audio.getAvatarGain()); + * print("Environment server gain: " + Audio.getInjectorGain()); + * print("Environment local gain: " + Audio.getLocalInjectorGain()); + * print("System gain: " + Audio.getSystemInjectorGain()); + */ Q_INVOKABLE float getAvatarGain(); /**jsdoc - * Sets the injector gain at the server. - * Units are Decibels (dB) + * Sets the gain (relative volume) that environment sounds from the server are played at. * @function Audio.setInjectorGain - * @param {number} gain (in dB) - */ + * @param {number} gain - Injector gain (dB) at the server. + */ Q_INVOKABLE void setInjectorGain(float gain); /**jsdoc - * Gets the injector gain at the server. + * Gets the gain (relative volume) that environment sounds from the server are played at. * @function Audio.getInjectorGain - * @returns {number} gain (in dB) - */ + * @returns {number} Injector gain (dB) at the server. + */ Q_INVOKABLE float getInjectorGain(); /**jsdoc - * Sets the local injector gain in the client. - * Units are Decibels (dB) + * Sets the gain (relative volume) that environment sounds from the client are played at. * @function Audio.setLocalInjectorGain - * @param {number} gain (in dB) - */ + * @param {number} gain - Injector gain (dB) in the client. + */ Q_INVOKABLE void setLocalInjectorGain(float gain); /**jsdoc - * Gets the local injector gain in the client. + * Gets the gain (relative volume) that environment sounds from the client are played at. * @function Audio.getLocalInjectorGain - * @returns {number} gain (in dB) - */ + * @returns {number} Injector gain (dB) in the client. + */ Q_INVOKABLE float getLocalInjectorGain(); /**jsdoc - * Sets the injector gain for system sounds. - * Units are Decibels (dB) + * Sets the gain (relative volume) that system sounds are played at. * @function Audio.setSystemInjectorGain - * @param {number} gain (in dB) - */ + * @param {number} gain - Injector gain (dB) in the client. + */ Q_INVOKABLE void setSystemInjectorGain(float gain); /**jsdoc - * Gets the injector gain for system sounds. + * Gets the gain (relative volume) that system sounds are played at. * @function Audio.getSystemInjectorGain - * @returns {number} gain (in dB) + * @returns {number} Injector gain (dB) in the client. */ Q_INVOKABLE float getSystemInjectorGain(); @@ -253,13 +270,13 @@ public: Q_INVOKABLE bool startRecording(const QString& filename); /**jsdoc - * Finish making an audio recording started with {@link Audio.startRecording|startRecording}. + * Finishes making an audio recording started with {@link Audio.startRecording|startRecording}. * @function Audio.stopRecording */ Q_INVOKABLE void stopRecording(); /**jsdoc - * Check whether an audio recording is currently being made. + * Checks whether an audio recording is currently being made. * @function Audio.getRecording * @returns {boolean} true if an audio recording is currently being made, otherwise false. */ @@ -275,9 +292,10 @@ signals: void nop(); /**jsdoc - * Triggered when the audio input is muted or unmuted. + * Triggered when the audio input is muted or unmuted for the current context (desktop or HMD). * @function Audio.mutedChanged - * @param {boolean} isMuted - true if the audio input is muted, otherwise false. + * @param {boolean} isMuted - true if the audio input is muted for the current context (desktop or HMD), + * otherwise false. * @returns {Signal} * @example Report when audio input is muted or unmuted * Audio.mutedChanged.connect(function (isMuted) { @@ -287,47 +305,55 @@ signals: void mutedChanged(bool isMuted); /**jsdoc - * Triggered when desktop audio input is muted or unmuted. - * @function Audio.mutedDesktopChanged - * @param {boolean} isMuted - true if the audio input is muted for desktop mode, otherwise false. - * @returns {Signal} - */ + * Triggered when desktop audio input is muted or unmuted. + * @function Audio.mutedDesektopChanged + * @param {boolean} isMuted - true if desktop audio input is muted, otherwise false. + * @returns {Signal} + * @example Report when desktop muting changes. + * Audio.mutedDesktopChanged.connect(function (isMuted) { + * print("Desktop muted: " + isMuted); + * }); + */ void mutedDesktopChanged(bool isMuted); /**jsdoc - * Triggered when HMD audio input is muted or unmuted. - * @function Audio.mutedHMDChanged - * @param {boolean} isMuted - true if the audio input is muted for HMD mode, otherwise false. - * @returns {Signal} - */ + * Triggered when HMD audio input is muted or unmuted. + * @function Audio.mutedHMDChanged + * @param {boolean} isMuted - true if HMD audio input is muted, otherwise false. + * @returns {Signal} + */ void mutedHMDChanged(bool isMuted); - /** - * Triggered when Push-to-Talk has been enabled or disabled. - * @function Audio.pushToTalkChanged - * @param {boolean} enabled - true if Push-to-Talk is enabled, otherwise false. - * @returns {Signal} - */ + /**jsdoc + * Triggered when push-to-talk is enabled or disabled for the current context (desktop or HMD). + * @function Audio.pushToTalkChanged + * @param {boolean} enabled - true if push-to-talk is enabled, otherwise false. + * @returns {Signal} + * @example Report when push-to-talk changes. + * Audio.pushToTalkChanged.connect(function (enabled) { + * print("Push to talk: " + (enabled ? "on" : "off")); + * }); + */ void pushToTalkChanged(bool enabled); - /** - * Triggered when Push-to-Talk has been enabled or disabled for desktop mode. - * @function Audio.pushToTalkDesktopChanged - * @param {boolean} enabled - true if Push-to-Talk is emabled for Desktop mode, otherwise false. - * @returns {Signal} - */ + /**jsdoc + * Triggered when push-to-talk is enabled or disabled for desktop mode. + * @function Audio.pushToTalkDesktopChanged + * @param {boolean} enabled - true if push-to-talk is enabled for desktop mode, otherwise false. + * @returns {Signal} + */ void pushToTalkDesktopChanged(bool enabled); - /** - * Triggered when Push-to-Talk has been enabled or disabled for HMD mode. - * @function Audio.pushToTalkHMDChanged - * @param {boolean} enabled - true if Push-to-Talk is emabled for HMD mode, otherwise false. - * @returns {Signal} - */ + /**jsdoc + * Triggered when push-to-talk is enabled or disabled for HMD mode. + * @function Audio.pushToTalkHMDChanged + * @param {boolean} enabled - true if push-to-talk is enabled for HMD mode, otherwise false. + * @returns {Signal} + */ void pushToTalkHMDChanged(bool enabled); /**jsdoc - * Triggered when the audio input noise reduction is enabled or disabled. + * Triggered when audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged * @param {boolean} isEnabled - true if audio input noise reduction is enabled, otherwise false. * @returns {Signal} @@ -346,8 +372,8 @@ signals: * Triggered when the input audio volume changes. * @function Audio.inputVolumeChanged * @param {number} volume - The requested volume to be applied to the audio input, range 0.0 – - * 1.0. The resulting value of Audio.inputVolume depends on the capabilities of the device: - * for example, the volume can't be changed on some devices, and others might only support values of 0.0 + * 1.0. The resulting value of Audio.inputVolume depends on the capabilities of the device. + * For example, the volume can't be changed on some devices, while others might only support values of 0.0 * and 1.0. * @returns {Signal} */ @@ -379,11 +405,11 @@ signals: void contextChanged(const QString& context); /**jsdoc - * Triggered when pushing to talk. - * @function Audio.pushingToTalkChanged - * @param {boolean} talking - true if broadcasting with PTT, false otherwise. - * @returns {Signal} - */ + * Triggered when the user starts or stops push-to-talk. + * @function Audio.pushingToTalkChanged + * @param {boolean} talking - true if started push-to-talk, false if stopped push-to-talk. + * @returns {Signal} + */ void pushingToTalkChanged(bool talking); public slots: @@ -409,6 +435,7 @@ protected: private: + bool _settingsLoaded { false }; float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; float _localInjectorGain { 0.0f }; // in dB diff --git a/interface/src/scripting/ControllerScriptingInterface.h b/interface/src/scripting/ControllerScriptingInterface.h index 8b7f62457e..3f3c2eec5d 100644 --- a/interface/src/scripting/ControllerScriptingInterface.h +++ b/interface/src/scripting/ControllerScriptingInterface.h @@ -286,7 +286,7 @@ public slots: * Disables default Interface actions for a joystick. * @function Controller.captureJoystick * @param {number} joystickID - The integer ID of the joystick. - * @deprecated This function no longer has any effect. + * @deprecated This function is deprecated and will be removed. It no longer has any effect. */ virtual void captureJoystick(int joystickIndex); @@ -295,7 +295,7 @@ public slots: * {@link Controller.captureJoystick|captureJoystick}. * @function Controller.releaseJoystick * @param {number} joystickID - The integer ID of the joystick. - * @deprecated This function no longer has any effect. + * @deprecated This function is deprecated and will be removed. It no longer has any effect. */ virtual void releaseJoystick(int joystickIndex); diff --git a/interface/src/scripting/RefreshRateScriptingInterface.h b/interface/src/scripting/RefreshRateScriptingInterface.h new file mode 100644 index 0000000000..697141583f --- /dev/null +++ b/interface/src/scripting/RefreshRateScriptingInterface.h @@ -0,0 +1,46 @@ +// +// RefreshRateScriptingInterface.h +// interface/src/scrfipting +// +// Created by Dante Ruiz on 2019-04-15. +// 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_RefreshRateScriptingInterface_h +#define hifi_RefreshRateScriptingInterface_h + +#include + +#include + +class RefreshRateScriptingInterface : public QObject { + Q_OBJECT +public: + RefreshRateScriptingInterface() = default; + ~RefreshRateScriptingInterface() = default; + +public: + Q_INVOKABLE QString getRefreshRateProfile() { + RefreshRateManager& refreshRateManager = qApp->getRefreshRateManager(); + return QString::fromStdString(RefreshRateManager::refreshRateProfileToString(refreshRateManager.getRefreshRateProfile())); + } + + Q_INVOKABLE QString getRefreshRateRegime() { + RefreshRateManager& refreshRateManager = qApp->getRefreshRateManager(); + return QString::fromStdString(RefreshRateManager::refreshRateRegimeToString(refreshRateManager.getRefreshRateRegime())); + } + + Q_INVOKABLE QString getUXMode() { + RefreshRateManager& refreshRateManager = qApp->getRefreshRateManager(); + return QString::fromStdString(RefreshRateManager::uxModeToString(refreshRateManager.getUXMode())); + } + + Q_INVOKABLE int getActiveRefreshRate() { + return qApp->getRefreshRateManager().getActiveRefreshRate(); + } +}; + +#endif diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index c3aeb2643b..6a694ce27b 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -199,3 +199,13 @@ void TestScriptingInterface::setOtherAvatarsReplicaCount(int count) { int TestScriptingInterface::getOtherAvatarsReplicaCount() { return qApp->getOtherAvatarsReplicaCount(); } + +void TestScriptingInterface::setMinimumGPUTextureMemStabilityCount(int count) { + QMetaObject::invokeMethod(qApp, "setMinimumGPUTextureMemStabilityCount", Qt::DirectConnection, Q_ARG(int, count)); +} + +bool TestScriptingInterface::isTextureLoadingComplete() { + bool result; + QMetaObject::invokeMethod(qApp, "gpuTextureMemSizeStable", Qt::DirectConnection, Q_RETURN_ARG(bool, result)); + return result; +} diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index 4a1d1a3eeb..897f955a74 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -163,6 +163,20 @@ public slots: */ Q_INVOKABLE int getOtherAvatarsReplicaCount(); + /**jsdoc + * Set number of cycles texture size is required to be stable + * @function Entities.setMinimumGPUTextureMemStabilityCount + * @param {number} count - Number of cycles to wait + */ + Q_INVOKABLE void setMinimumGPUTextureMemStabilityCount(int count); + + /**jsdoc + * Check whether all textures have been loaded. + * @function Entities.isTextureLoadingComplete + * @returns {boolean} true texture memory usage is not increasing + */ + Q_INVOKABLE bool isTextureLoadingComplete(); + private: bool waitForCondition(qint64 maxWaitMs, std::function condition); QString _testResultsLocation; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0f3d859093..2c1311924f 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -27,6 +27,7 @@ #include "MainWindow.h" #include "Menu.h" #include "OffscreenUi.h" +#include "commerce/QmlCommerce.h" static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation"; @@ -134,15 +135,17 @@ void WindowScriptingInterface::disconnectedFromDomain() { void WindowScriptingInterface::openUrl(const QUrl& url) { if (!url.isEmpty()) { - if (url.scheme() == URL_SCHEME_HIFI) { + auto scheme = url.scheme(); + if (scheme == URL_SCHEME_HIFI) { DependencyManager::get()->handleLookupString(url.toString()); + } else if (scheme == URL_SCHEME_HIFIAPP) { + DependencyManager::get()->openSystemApp(url.path()); } else { #if defined(Q_OS_ANDROID) QMap args; args["url"] = url.toString(); AndroidHelper::instance().requestActivity("WebView", true, args); #else - // address manager did not handle - ask QDesktopServices to handle QDesktopServices::openUrl(url); #endif } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index baff6444e1..77b586ec70 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -535,9 +535,10 @@ public slots: int openMessageBox(QString title, QString text, int buttons, int defaultButton); /**jsdoc - * Open a URL in the Interface window or other application, depending on the URL's scheme. If the URL starts with - * hifi:// then that URL is navigated to in Interface, otherwise the URL is opened in the application the OS - * associates with the URL's scheme (e.g., a Web browser for http://). + * Open a URL in the Interface window or other application, depending on the URL's scheme. The following schemes are supported: + * hifi (navigate to the URL in Interface), hifiapp (open a system app in Interface). Other schemes will either be handled by the OS + * (e.g. http, https, mailto) or will create a confirmation dialog asking the user to confirm that they want to try to open + * the URL. * @function Window.openUrl * @param {string} url - The URL to open. */ diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index a3a875ac40..e34b82e0a1 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -156,10 +156,10 @@ void DialogsManager::hmdTools(bool showTools) { } _hmdToolsDialog->show(); _hmdToolsDialog->raise(); + qApp->getWindow()->activateWindow(); } else { hmdToolsClosed(); } - qApp->getWindow()->activateWindow(); } void DialogsManager::hmdToolsClosed() { @@ -207,4 +207,4 @@ void DialogsManager::showDomainConnectionDialog() { _domainConnectionDialog->show(); _domainConnectionDialog->raise(); -} \ No newline at end of file +} diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp index 1cbe31f1eb..6262210620 100644 --- a/interface/src/ui/Keyboard.cpp +++ b/interface/src/ui/Keyboard.cpp @@ -910,6 +910,9 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) { }); _layerIndex = 0; addIncludeItemsToMallets(); + + auto myAvatar = DependencyManager::get()->getMyAvatar(); + scaleKeyboard(myAvatar->getSensorToWorldScale()); }); request->send(); diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h index 51e5e0571f..2b6829bf2b 100644 --- a/interface/src/ui/Keyboard.h +++ b/interface/src/ui/Keyboard.h @@ -98,6 +98,7 @@ public: bool isPassword() const; void setPassword(bool password); void enableRightMallet(); + void scaleKeyboard(float sensorToWorldScale); void enableLeftMallet(); void disableRightMallet(); void disableLeftMallet(); @@ -122,7 +123,6 @@ public slots: void handleTriggerContinue(const QUuid& id, const PointerEvent& event); void handleHoverBegin(const QUuid& id, const PointerEvent& event); void handleHoverEnd(const QUuid& id, const PointerEvent& event); - void scaleKeyboard(float sensorToWorldScale); private: struct Anchor { diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index b4f504822f..c0e96fe8bb 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -138,7 +138,7 @@ void LoginDialog::login(const QString& username, const QString& password) const void LoginDialog::loginThroughOculus() { qDebug() << "Attempting to login through Oculus"; if (auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { - oculusPlatformPlugin->requestNonceAndUserID([this] (QString nonce, QString oculusID) { + oculusPlatformPlugin->requestNonceAndUserID([] (QString nonce, QString oculusID) { DependencyManager::get()->requestAccessTokenWithOculus(nonce, oculusID); }); } @@ -279,10 +279,6 @@ void LoginDialog::createAccountFromSteam(QString username) { } } -void LoginDialog::openUrl(const QString& url) const { - QDesktopServices::openUrl(QUrl(url)); -} - void LoginDialog::linkCompleted(QNetworkReply* reply) { emit handleLinkCompleted(); } diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index e2fa8adc61..7c659a9320 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -80,8 +80,6 @@ protected slots: Q_INVOKABLE void signup(const QString& email, const QString& username, const QString& password); - Q_INVOKABLE void openUrl(const QString& url) const; - Q_INVOKABLE bool getLoginDialogPoppedUp() const; }; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 75279ef889..6a2516115d 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -82,6 +82,28 @@ void setupPreferences() { preferences->addPreference(new CheckPreference(GRAPHICS_QUALITY, "Show Shadows", getterShadow, setterShadow)); } + { + auto getter = []()->QString { + RefreshRateManager::RefreshRateProfile refreshRateProfile = qApp->getRefreshRateManager().getRefreshRateProfile(); + return QString::fromStdString(RefreshRateManager::refreshRateProfileToString(refreshRateProfile)); + }; + + auto setter = [](QString value) { + std::string profileName = value.toStdString(); + RefreshRateManager::RefreshRateProfile refreshRateProfile = RefreshRateManager::refreshRateProfileFromString(profileName); + qApp->getRefreshRateManager().setRefreshRateProfile(refreshRateProfile); + }; + + auto preference = new ComboBoxPreference(GRAPHICS_QUALITY, "Refresh Rate", getter, setter); + QStringList refreshRateProfiles + { QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::ECO)), + QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::INTERACTIVE)), + QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::REALTIME)) }; + + preference->setItems(refreshRateProfiles); + preferences->addPreference(preference); + } + // UI static const QString UI_CATEGORY { "User Interface" }; { @@ -278,6 +300,12 @@ void setupPreferences() { preference->setIndented(true); preferences->addPreference(preference); } + { + auto getter = [myAvatar]() -> bool { return myAvatar->hoverWhenUnsupported(); }; + auto setter = [myAvatar](bool value) { myAvatar->setHoverWhenUnsupported(value); }; + auto preference = new CheckPreference(VR_MOVEMENT, "Hover When Unsupported", getter, setter); + preferences->addPreference(preference); + } { auto getter = [myAvatar]()->int { return myAvatar->getMovementReference(); }; auto setter = [myAvatar](int value) { myAvatar->setMovementReference(value); }; diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 60c039ce1f..d97c401351 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -159,47 +159,57 @@ void Snapshot::save360Snapshot(const glm::vec3& cameraPosition, secondaryCameraRenderConfig->setOrientation(CAMERA_ORIENTATION_DOWN); _snapshotIndex = 0; + _taking360Snapshot = true; _snapshotTimer.start(SNAPSHOT_360_TIMER_INTERVAL); } void Snapshot::takeNextSnapshot() { - SecondaryCameraJobConfig* config = - static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); + if (_taking360Snapshot) { + if (!_waitingOnSnapshot) { + _waitingOnSnapshot = true; + qApp->addSnapshotOperator(std::make_tuple([this](const QImage& snapshot) { + // Order is: + // 0. Down + // 1. Front + // 2. Left + // 3. Back + // 4. Right + // 5. Up + if (_snapshotIndex < 6) { + _imageArray[_snapshotIndex] = snapshot; + } - // Order is: - // 0. Down - // 1. Front - // 2. Left - // 3. Back - // 4. Right - // 5. Up - if (_snapshotIndex < 6) { - _imageArray[_snapshotIndex] = qApp->getActiveDisplayPlugin()->getSecondaryCameraScreenshot(); - } + SecondaryCameraJobConfig* config = static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); + if (_snapshotIndex == 0) { + // Setup for Front Image capture + config->setOrientation(CAMERA_ORIENTATION_FRONT); + } else if (_snapshotIndex == 1) { + // Setup for Left Image capture + config->setOrientation(CAMERA_ORIENTATION_LEFT); + } else if (_snapshotIndex == 2) { + // Setup for Back Image capture + config->setOrientation(CAMERA_ORIENTATION_BACK); + } else if (_snapshotIndex == 3) { + // Setup for Right Image capture + config->setOrientation(CAMERA_ORIENTATION_RIGHT); + } else if (_snapshotIndex == 4) { + // Setup for Up Image capture + config->setOrientation(CAMERA_ORIENTATION_UP); + } else if (_snapshotIndex == 5) { + _taking360Snapshot = false; + } - if (_snapshotIndex == 0) { - // Setup for Front Image capture - config->setOrientation(CAMERA_ORIENTATION_FRONT); - } else if (_snapshotIndex == 1) { - // Setup for Left Image capture - config->setOrientation(CAMERA_ORIENTATION_LEFT); - } else if (_snapshotIndex == 2) { - // Setup for Back Image capture - config->setOrientation(CAMERA_ORIENTATION_BACK); - } else if (_snapshotIndex == 3) { - // Setup for Right Image capture - config->setOrientation(CAMERA_ORIENTATION_RIGHT); - } else if (_snapshotIndex == 4) { - // Setup for Up Image capture - config->setOrientation(CAMERA_ORIENTATION_UP); - } else if (_snapshotIndex > 5) { + _waitingOnSnapshot = false; + _snapshotIndex++; + }, 0.0f, false)); + } + } else { _snapshotTimer.stop(); // Reset secondary camera render config - static_cast( - qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping")) - ->setCurve(1); + SecondaryCameraJobConfig* config = static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); + static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping"))->setCurve(1); config->resetSizeSpectatorCamera(qApp->getWindow()->geometry().width(), qApp->getWindow()->geometry().height()); config->setProperty("attachedEntityId", _oldAttachedEntityId); config->setProperty("vFoV", _oldvFoV); @@ -217,8 +227,6 @@ void Snapshot::takeNextSnapshot() { QtConcurrent::run([this]() { convertToEquirectangular(); }); } } - - _snapshotIndex++; } void Snapshot::convertToCubemap() { diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 77bdfd4ac1..f13f4cd587 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -97,6 +97,8 @@ private: bool _cubemapOutputFormat; QTimer _snapshotTimer; qint16 _snapshotIndex; + bool _waitingOnSnapshot { false }; + bool _taking360Snapshot { false }; bool _oldEnabled; QVariant _oldAttachedEntityId; QVariant _oldOrientation; diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 9d58d89385..b8cffca8ab 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -27,7 +27,6 @@ QString SnapshotAnimated::snapshotAnimatedPath; QString SnapshotAnimated::snapshotStillPath; QVector SnapshotAnimated::snapshotAnimatedFrameVector; QVector SnapshotAnimated::snapshotAnimatedFrameDelayVector; -Application* SnapshotAnimated::app; float SnapshotAnimated::aspectRatio; QSharedPointer SnapshotAnimated::snapshotAnimatedDM; GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; @@ -36,12 +35,11 @@ GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; Setting::Handle SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true); Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS); -void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { +void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { SnapshotAnimated::snapshotAnimatedTimer = new QTimer(); SnapshotAnimated::aspectRatio = aspectRatio; - SnapshotAnimated::app = app; SnapshotAnimated::snapshotAnimatedDM = dm; // Define the output location of the still and animated snapshots. SnapshotAnimated::snapshotStillPath = pathStill; @@ -62,44 +60,45 @@ void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio void SnapshotAnimated::captureFrames() { if (SnapshotAnimated::snapshotAnimatedTimerRunning) { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(SnapshotAnimated::app->getActiveDisplayPlugin()->getScreenshot(SnapshotAnimated::aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); - SnapshotAnimated::snapshotAnimatedFrameVector.append(frame); + qApp->addSnapshotOperator(std::make_tuple([](const QImage& snapshot) { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame = snapshot.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + SnapshotAnimated::snapshotAnimatedFrameVector.append(frame); - // If that was the first frame... - if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - // Record the first frame timestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; - SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // If this is an intermediate or the final frame... - } else { - // Push the current frame delay onto the vector - SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10)); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // If that was the first frame... + if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // Record the first frame timestamp + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // If this is an intermediate or the final frame... + } else { + // Push the current frame delay onto the vector + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10)); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { - SnapshotAnimated::snapshotAnimatedTimerRunning = false; - - // Notify the user that we're processing the snapshot - // This also pops up the "Share" dialog. The unprocessed GIF will be visualized as a loading icon until processingGifCompleted() is called. - emit SnapshotAnimated::snapshotAnimatedDM->processingGifStarted(SnapshotAnimated::snapshotStillPath); - - // Kick off the thread that'll pack the frames into the GIF - QtConcurrent::run(processFrames); - // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE - // that the slot will not be called again in the future. - // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html - SnapshotAnimated::snapshotAnimatedTimer->stop(); - delete SnapshotAnimated::snapshotAnimatedTimer; + // If that was the last frame... + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { + SnapshotAnimated::snapshotAnimatedTimerRunning = false; + } } - } + }, SnapshotAnimated::aspectRatio, true)); + } else { + // Notify the user that we're processing the snapshot + // This also pops up the "Share" dialog. The unprocessed GIF will be visualized as a loading icon until processingGifCompleted() is called. + emit SnapshotAnimated::snapshotAnimatedDM->processingGifStarted(SnapshotAnimated::snapshotStillPath); + + // Kick off the thread that'll pack the frames into the GIF + QtConcurrent::run(processFrames); + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer->stop(); + delete SnapshotAnimated::snapshotAnimatedTimer; } } diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 87ce533fc3..15734f57c8 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -42,7 +42,6 @@ private: static QVector snapshotAnimatedFrameVector; static QVector snapshotAnimatedFrameDelayVector; static QSharedPointer snapshotAnimatedDM; - static Application* app; static float aspectRatio; static GifWriter snapshotAnimatedGifWriter; @@ -51,7 +50,7 @@ private: static void processFrames(); static void clearTempVariables(); public: - static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); + static void saveSnapshotAnimated(QString pathStill, float aspectRatio, QSharedPointer dm); static bool isAlreadyTakingSnapshotAnimated() { return snapshotAnimatedFirstFrameTimestamp != 0; }; static Setting::Handle alsoTakeAnimatedSnapshot; static Setting::Handle snapshotAnimatedDuration; diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 022b57c0d9..8f289812fa 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -132,6 +132,14 @@ void Stats::updateStats(bool force) { STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated()); STAT_UPDATE(serverCount, (int)nodeList->size()); STAT_UPDATE_FLOAT(renderrate, qApp->getRenderLoopRate(), 0.1f); + RefreshRateManager& refreshRateManager = qApp->getRefreshRateManager(); + std::string refreshRateMode = RefreshRateManager::refreshRateProfileToString(refreshRateManager.getRefreshRateProfile()); + std::string refreshRateRegime = RefreshRateManager::refreshRateRegimeToString(refreshRateManager.getRefreshRateRegime()); + std::string uxMode = RefreshRateManager::uxModeToString(refreshRateManager.getUXMode()); + STAT_UPDATE(refreshRateMode, QString::fromStdString(refreshRateMode)); + STAT_UPDATE(refreshRateRegime, QString::fromStdString(refreshRateRegime)); + STAT_UPDATE(uxMode, QString::fromStdString(uxMode)); + STAT_UPDATE(refreshRateTarget, refreshRateManager.getActiveRefreshRate()); if (qApp->getActiveDisplayPlugin()) { auto displayPlugin = qApp->getActiveDisplayPlugin(); auto stats = displayPlugin->getHardwareStats(); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 3134b223d6..7709f2d6dc 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -206,6 +206,10 @@ class Stats : public QQuickItem { STATS_PROPERTY(float, presentdroprate, 0) STATS_PROPERTY(int, gameLoopRate, 0) STATS_PROPERTY(int, avatarCount, 0) + STATS_PROPERTY(int, refreshRateTarget, 0) + STATS_PROPERTY(QString, refreshRateMode, QString()) + STATS_PROPERTY(QString, refreshRateRegime, QString()) + STATS_PROPERTY(QString, uxMode, QString()) STATS_PROPERTY(int, heroAvatarCount, 0) STATS_PROPERTY(int, physicsObjectCount, 0) STATS_PROPERTY(int, updatedAvatarCount, 0) @@ -1067,6 +1071,15 @@ signals: */ void decimatedTextureCountChanged(); + + void refreshRateTargetChanged(); + + void refreshRateModeChanged(); + + void refreshRateRegimeChanged(); + + void uxModeChanged(); + // QQuickItem signals. /**jsdoc diff --git a/libraries/animation/src/AnimContext.cpp b/libraries/animation/src/AnimContext.cpp index c8efd83318..186a88504f 100644 --- a/libraries/animation/src/AnimContext.cpp +++ b/libraries/animation/src/AnimContext.cpp @@ -11,11 +11,12 @@ #include "AnimContext.h" AnimContext::AnimContext(bool enableDebugDrawIKTargets, bool enableDebugDrawIKConstraints, bool enableDebugDrawIKChains, - const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix) : + const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix, int evaluationCount) : _enableDebugDrawIKTargets(enableDebugDrawIKTargets), _enableDebugDrawIKConstraints(enableDebugDrawIKConstraints), _enableDebugDrawIKChains(enableDebugDrawIKChains), _geometryToRigMatrix(geometryToRigMatrix), - _rigToWorldMatrix(rigToWorldMatrix) + _rigToWorldMatrix(rigToWorldMatrix), + _evaluationCount(evaluationCount) { } diff --git a/libraries/animation/src/AnimContext.h b/libraries/animation/src/AnimContext.h index e3ab5d9788..5f353fcae4 100644 --- a/libraries/animation/src/AnimContext.h +++ b/libraries/animation/src/AnimContext.h @@ -24,6 +24,7 @@ enum class AnimNodeType { BlendLinearMove, Overlay, StateMachine, + RandomSwitchStateMachine, Manipulator, InverseKinematics, DefaultPose, @@ -37,13 +38,14 @@ class AnimContext { public: AnimContext() {} AnimContext(bool enableDebugDrawIKTargets, bool enableDebugDrawIKConstraints, bool enableDebugDrawIKChains, - const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix); + const glm::mat4& geometryToRigMatrix, const glm::mat4& rigToWorldMatrix, int evaluationCount); bool getEnableDebugDrawIKTargets() const { return _enableDebugDrawIKTargets; } bool getEnableDebugDrawIKConstraints() const { return _enableDebugDrawIKConstraints; } bool getEnableDebugDrawIKChains() const { return _enableDebugDrawIKChains; } const glm::mat4& getGeometryToRigMatrix() const { return _geometryToRigMatrix; } const glm::mat4& getRigToWorldMatrix() const { return _rigToWorldMatrix; } + int getEvaluationCount() const { return _evaluationCount; } float getDebugAlpha(const QString& key) const { auto it = _debugAlphaMap.find(key); @@ -85,6 +87,7 @@ protected: bool _enableDebugDrawIKChains { false }; glm::mat4 _geometryToRigMatrix; glm::mat4 _rigToWorldMatrix; + int _evaluationCount{ 0 }; // used for debugging internal state of animation system. mutable DebugAlphaMap _debugAlphaMap; diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h index 1a12bb8ddb..31e10ca2d5 100644 --- a/libraries/animation/src/AnimNode.h +++ b/libraries/animation/src/AnimNode.h @@ -43,6 +43,7 @@ public: friend class AnimDebugDraw; friend void buildChildMap(std::map& map, Pointer node); friend class AnimStateMachine; + friend class AnimRandomSwitch; AnimNode(Type type, const QString& id) : _type(type), _id(id) {} virtual ~AnimNode() {} diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index b637d131f8..4131009324 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -22,6 +22,7 @@ #include "AnimationLogging.h" #include "AnimOverlay.h" #include "AnimStateMachine.h" +#include "AnimRandomSwitch.h" #include "AnimManipulator.h" #include "AnimInverseKinematics.h" #include "AnimDefaultPose.h" @@ -38,6 +39,7 @@ static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const Q static AnimNode::Pointer loadBlendLinearMoveNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadRandomSwitchStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadDefaultPoseNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); @@ -51,6 +53,7 @@ static const float ANIM_GRAPH_LOAD_PRIORITY = 10.0f; // returns node on success, nullptr on failure. static bool processDoNothing(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +bool processRandomSwitchStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static const char* animNodeTypeToString(AnimNode::Type type) { switch (type) { @@ -59,6 +62,7 @@ static const char* animNodeTypeToString(AnimNode::Type type) { case AnimNode::Type::BlendLinearMove: return "blendLinearMove"; case AnimNode::Type::Overlay: return "overlay"; case AnimNode::Type::StateMachine: return "stateMachine"; + case AnimNode::Type::RandomSwitchStateMachine: return "randomSwitchStateMachine"; case AnimNode::Type::Manipulator: return "manipulator"; case AnimNode::Type::InverseKinematics: return "inverseKinematics"; case AnimNode::Type::DefaultPose: return "defaultPose"; @@ -92,6 +96,16 @@ static AnimStateMachine::InterpType stringToInterpType(const QString& str) { } } +static AnimRandomSwitch::InterpType stringToRandomInterpType(const QString& str) { + if (str == "snapshotBoth") { + return AnimRandomSwitch::InterpType::SnapshotBoth; + } else if (str == "snapshotPrev") { + return AnimRandomSwitch::InterpType::SnapshotPrev; + } else { + return AnimRandomSwitch::InterpType::NumTypes; + } +} + static const char* animManipulatorJointVarTypeToString(AnimManipulator::JointVar::Type type) { switch (type) { case AnimManipulator::JointVar::Type::Absolute: return "absolute"; @@ -122,6 +136,7 @@ static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) { case AnimNode::Type::BlendLinearMove: return loadBlendLinearMoveNode; case AnimNode::Type::Overlay: return loadOverlayNode; case AnimNode::Type::StateMachine: return loadStateMachineNode; + case AnimNode::Type::RandomSwitchStateMachine: return loadRandomSwitchStateMachineNode; case AnimNode::Type::Manipulator: return loadManipulatorNode; case AnimNode::Type::InverseKinematics: return loadInverseKinematicsNode; case AnimNode::Type::DefaultPose: return loadDefaultPoseNode; @@ -140,6 +155,7 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) { case AnimNode::Type::BlendLinearMove: return processDoNothing; case AnimNode::Type::Overlay: return processDoNothing; case AnimNode::Type::StateMachine: return processStateMachineNode; + case AnimNode::Type::RandomSwitchStateMachine: return processRandomSwitchStateMachineNode; case AnimNode::Type::Manipulator: return processDoNothing; case AnimNode::Type::InverseKinematics: return processDoNothing; case AnimNode::Type::DefaultPose: return processDoNothing; @@ -463,6 +479,11 @@ static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const return node; } +static AnimNode::Pointer loadRandomSwitchStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + auto node = std::make_shared(id); + return node; +} + static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr); @@ -780,6 +801,141 @@ bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, return true; } +bool processRandomSwitchStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl) { + auto smNode = std::static_pointer_cast(node); + assert(smNode); + + READ_STRING(currentState, jsonObj, nodeId, jsonUrl, false); + READ_OPTIONAL_FLOAT(randomSwitchTimeMin, jsonObj, -1.0f); + READ_OPTIONAL_FLOAT(randomSwitchTimeMax, jsonObj, -1.0f); + READ_OPTIONAL_STRING(triggerRandomSwitch, jsonObj); + READ_OPTIONAL_FLOAT(triggerTimeMin, jsonObj, -1.0f); + READ_OPTIONAL_FLOAT(triggerTimeMax, jsonObj, -1.0f); + READ_OPTIONAL_STRING(transitionVar, jsonObj); + + + + auto statesValue = jsonObj.value("states"); + if (!statesValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad array \"states\" in random switch state Machine node, id =" << nodeId; + return false; + } + + // build a map for all children by name. + std::map childMap; + buildChildMap(childMap, node); + + // first pass parse all the states and build up the state and transition map. + using StringPair = std::pair; + using TransitionMap = std::multimap; + TransitionMap transitionMap; + + using RandomStateMap = std::map; + RandomStateMap randomStateMap; + + auto randomStatesArray = statesValue.toArray(); + for (const auto& randomStateValue : randomStatesArray) { + if (!randomStateValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad state object in \"random states\", id =" << nodeId; + return false; + } + auto stateObj = randomStateValue.toObject(); + + READ_STRING(id, stateObj, nodeId, jsonUrl, false); + READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl, false); + READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl, false); + READ_OPTIONAL_STRING(interpType, stateObj); + READ_FLOAT(priority, stateObj, nodeId, jsonUrl, false); + READ_BOOL(resume, stateObj, nodeId, jsonUrl, false); + + READ_OPTIONAL_STRING(interpTargetVar, stateObj); + READ_OPTIONAL_STRING(interpDurationVar, stateObj); + READ_OPTIONAL_STRING(interpTypeVar, stateObj); + + auto iter = childMap.find(id); + if (iter == childMap.end()) { + qCCritical(animation) << "AnimNodeLoader, could not find random stateMachine child (state) with nodeId =" << nodeId << "random stateId =" << id; + return false; + } + + AnimRandomSwitch::InterpType interpTypeEnum = AnimRandomSwitch::InterpType::SnapshotPrev; // default value + if (!interpType.isEmpty()) { + interpTypeEnum = stringToRandomInterpType(interpType); + if (interpTypeEnum == AnimRandomSwitch::InterpType::NumTypes) { + qCCritical(animation) << "AnimNodeLoader, bad interpType on random state Machine state, nodeId = " << nodeId << "random stateId =" << id; + return false; + } + } + + auto randomStatePtr = std::make_shared(id, iter->second, interpTarget, interpDuration, interpTypeEnum, priority, resume); + if (priority > 0.0f) { + smNode->addToPrioritySum(priority); + } + assert(randomStatePtr); + + if (!interpTargetVar.isEmpty()) { + randomStatePtr->setInterpTargetVar(interpTargetVar); + } + if (!interpDurationVar.isEmpty()) { + randomStatePtr->setInterpDurationVar(interpDurationVar); + } + if (!interpTypeVar.isEmpty()) { + randomStatePtr->setInterpTypeVar(interpTypeVar); + } + + smNode->addState(randomStatePtr); + randomStateMap.insert(RandomStateMap::value_type(randomStatePtr->getID(), randomStatePtr)); + + auto transitionsValue = stateObj.value("transitions"); + if (!transitionsValue.isArray()) { + qCritical(animation) << "AnimNodeLoader, bad array \"transitions\" in random state Machine node, stateId =" << id << "nodeId =" << nodeId; + return false; + } + + auto transitionsArray = transitionsValue.toArray(); + for (const auto& transitionValue : transitionsArray) { + if (!transitionValue.isObject()) { + qCritical(animation) << "AnimNodeLoader, bad transition object in \"transitions\", random stateId =" << id << "nodeId =" << nodeId; + return false; + } + auto transitionObj = transitionValue.toObject(); + + READ_STRING(var, transitionObj, nodeId, jsonUrl, false); + READ_STRING(randomSwitchState, transitionObj, nodeId, jsonUrl, false); + + transitionMap.insert(TransitionMap::value_type(randomStatePtr, StringPair(var, randomSwitchState))); + } + } + + // second pass: now iterate thru all transitions and add them to the appropriate states. + for (auto& transition : transitionMap) { + AnimRandomSwitch::RandomSwitchState::Pointer srcState = transition.first; + auto iter = randomStateMap.find(transition.second.second); + if (iter != randomStateMap.end()) { + srcState->addTransition(AnimRandomSwitch::RandomSwitchState::Transition(transition.second.first, iter->second)); + } else { + qCCritical(animation) << "AnimNodeLoader, bad random state machine transition from srcState =" << srcState->_id << "dstState =" << transition.second.second << "nodeId =" << nodeId; + return false; + } + } + + auto iter = randomStateMap.find(currentState); + if (iter == randomStateMap.end()) { + qCCritical(animation) << "AnimNodeLoader, bad currentState =" << currentState << "could not find child node" << "id =" << nodeId; + } + smNode->setCurrentState(iter->second); + smNode->setRandomSwitchTimeMin(randomSwitchTimeMin); + smNode->setRandomSwitchTimeMax(randomSwitchTimeMax); + smNode->setTriggerRandomSwitchVar(triggerRandomSwitch); + smNode->setTriggerTimeMin(triggerTimeMin); + smNode->setTriggerTimeMax(triggerTimeMax); + smNode->setTransitionVar(transitionVar); + + return true; +} + + + AnimNodeLoader::AnimNodeLoader(const QUrl& url) : _url(url) { diff --git a/libraries/animation/src/AnimRandomSwitch.cpp b/libraries/animation/src/AnimRandomSwitch.cpp new file mode 100644 index 0000000000..2549a50062 --- /dev/null +++ b/libraries/animation/src/AnimRandomSwitch.cpp @@ -0,0 +1,212 @@ +// +// AnimRandomSwitch.cpp +// +// Created by Angus Antley on 4/8/2019. +// Copyright (c) 2019 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimRandomSwitch.h" +#include "AnimUtil.h" +#include "AnimationLogging.h" + +AnimRandomSwitch::AnimRandomSwitch(const QString& id) : + AnimNode(AnimNode::Type::RandomSwitchStateMachine, id) { + +} + +AnimRandomSwitch::~AnimRandomSwitch() { + +} + +const AnimPoseVec& AnimRandomSwitch::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) { + float parentDebugAlpha = context.getDebugAlpha(_id); + + AnimRandomSwitch::RandomSwitchState::Pointer desiredState = _currentState; + if (abs(_randomSwitchEvaluationCount - context.getEvaluationCount()) > 1 || animVars.lookup(_triggerRandomSwitchVar, false)) { + + // get a random number and decide which motion to choose. + bool currentStateHasPriority = false; + float dice = randFloatInRange(0.0f, 1.0f); + float lowerBound = 0.0f; + for (const RandomSwitchState::Pointer& randState : _randomStates) { + if (randState->getPriority() > 0.0f) { + float upperBound = lowerBound + (randState->getPriority() / _totalPriorities); + if ((dice > lowerBound) && (dice < upperBound)) { + desiredState = randState; + } + lowerBound = upperBound; + + // this indicates if the curent state is one that can be selected randomly, or is one that was transitioned to by the random duration timer. + currentStateHasPriority = currentStateHasPriority || (_currentState == randState); + } + } + if (abs(_randomSwitchEvaluationCount - context.getEvaluationCount()) > 1) { + _duringInterp = false; + switchRandomState(animVars, context, desiredState, _duringInterp); + } else { + // firing a random switch, be sure that we aren't completing a previously triggered transition + if (currentStateHasPriority) { + if (desiredState->getID() != _currentState->getID()) { + _duringInterp = true; + switchRandomState(animVars, context, desiredState, _duringInterp); + } else { + _duringInterp = false; + } + } + } + _triggerTime = randFloatInRange(_triggerTimeMin, _triggerTimeMax); + _randomSwitchTime = randFloatInRange(_randomSwitchTimeMin, _randomSwitchTimeMax); + + } else { + + // here we are checking to see if we want a temporary movement + // evaluate currentState transitions + auto transitionState = evaluateTransitions(animVars); + if (transitionState != _currentState) { + _duringInterp = true; + switchRandomState(animVars, context, transitionState, _duringInterp); + _triggerTime = randFloatInRange(_triggerTimeMin, _triggerTimeMax); + _randomSwitchTime = randFloatInRange(_randomSwitchTimeMin, _randomSwitchTimeMax); + } + } + + _triggerTime -= dt; + if ((_triggerTime < 0.0f) && (_triggerTimeMin > 0.0f) && (_triggerTimeMax > 0.0f)) { + _triggerTime = randFloatInRange(_triggerTimeMin, _triggerTimeMax); + triggersOut.setTrigger(_transitionVar); + } + + _randomSwitchTime -= dt; + if ((_randomSwitchTime < 0.0f) && (_randomSwitchTimeMin > 0.0f) && (_randomSwitchTimeMax > 0.0f)) { + _randomSwitchTime = randFloatInRange(_randomSwitchTimeMin, _randomSwitchTimeMax); + // restart the trigger timer if it is also enabled + _triggerTime = randFloatInRange(_triggerTimeMin, _triggerTimeMax); + triggersOut.setTrigger(_triggerRandomSwitchVar); + } + + assert(_currentState); + auto currentStateNode = _children[_currentState->getChildIndex()]; + assert(currentStateNode); + + if (_duringInterp) { + _alpha += _alphaVel * dt; + if (_alpha < 1.0f) { + AnimPoseVec* nextPoses = nullptr; + AnimPoseVec* prevPoses = nullptr; + AnimPoseVec localNextPoses; + if (_interpType == InterpType::SnapshotBoth) { + // interp between both snapshots + prevPoses = &_prevPoses; + nextPoses = &_nextPoses; + } else if (_interpType == InterpType::SnapshotPrev) { + // interp between the prev snapshot and evaluated next target. + // this is useful for interping into a blend + localNextPoses = currentStateNode->evaluate(animVars, context, dt, triggersOut); + prevPoses = &_prevPoses; + nextPoses = &localNextPoses; + } else { + assert(false); + } + if (_poses.size() > 0 && nextPoses && prevPoses && nextPoses->size() > 0 && prevPoses->size() > 0) { + ::blend(_poses.size(), &(prevPoses->at(0)), &(nextPoses->at(0)), _alpha, &_poses[0]); + } + context.setDebugAlpha(_currentState->getID(), _alpha * parentDebugAlpha, _children[_currentState->getChildIndex()]->getType()); + } else { + _duringInterp = false; + _prevPoses.clear(); + _nextPoses.clear(); + } + } + + if (!_duringInterp){ + context.setDebugAlpha(_currentState->getID(), parentDebugAlpha, _children[_currentState->getChildIndex()]->getType()); + _poses = currentStateNode->evaluate(animVars, context, dt, triggersOut); + } + + _randomSwitchEvaluationCount = context.getEvaluationCount(); + processOutputJoints(triggersOut); + + context.addStateMachineInfo(_id, _currentState->getID(), _previousState->getID(), _duringInterp, _alpha); + if (_duringInterp) { + // hack: add previoius state to debug alpha map, with parens around it's name. + context.setDebugAlpha(QString("(%1)").arg(_previousState->getID()), 1.0f - _alpha, AnimNodeType::Clip); + } + + return _poses; +} + +void AnimRandomSwitch::setCurrentState(RandomSwitchState::Pointer randomState) { + _previousState = _currentState ? _currentState : randomState; + _currentState = randomState; +} + +void AnimRandomSwitch::addState(RandomSwitchState::Pointer randomState) { + _randomStates.push_back(randomState); +} + +void AnimRandomSwitch::switchRandomState(const AnimVariantMap& animVars, const AnimContext& context, RandomSwitchState::Pointer desiredState, bool shouldInterp) { + + auto nextStateNode = _children[desiredState->getChildIndex()]; + if (shouldInterp) { + + const float FRAMES_PER_SECOND = 30.0f; + + auto prevStateNode = _children[_currentState->getChildIndex()]; + + _alpha = 0.0f; + float duration = std::max(0.001f, animVars.lookup(desiredState->_interpDurationVar, desiredState->_interpDuration)); + _alphaVel = FRAMES_PER_SECOND / duration; + _interpType = (InterpType)animVars.lookup(desiredState->_interpTypeVar, (int)desiredState->_interpType); + + // because dt is 0, we should not encounter any triggers + const float dt = 0.0f; + AnimVariantMap triggers; + + if (_interpType == InterpType::SnapshotBoth) { + // snapshot previous pose. + _prevPoses = _poses; + // snapshot next pose at the target frame. + if (!desiredState->getResume()) { + nextStateNode->setCurrentFrame(desiredState->_interpTarget); + } + _nextPoses = nextStateNode->evaluate(animVars, context, dt, triggers); + } else if (_interpType == InterpType::SnapshotPrev) { + // snapshot previoius pose + _prevPoses = _poses; + // no need to evaluate _nextPoses we will do it dynamically during the interp, + // however we need to set the current frame. + if (!desiredState->getResume()) { + nextStateNode->setCurrentFrame(desiredState->_interpTarget - duration); + } + } else { + assert(false); + } + } else { + if (!desiredState->getResume()) { + nextStateNode->setCurrentFrame(desiredState->_interpTarget); + } + } + +#ifdef WANT_DEBUG + qCDebug(animation) << "AnimRandomSwitch::switchState:" << _currentState->getID() << "->" << desiredState->getID() << "duration =" << duration << "targetFrame =" << desiredState->_interpTarget << "interpType = " << (int)_interpType; +#endif + + setCurrentState(desiredState); +} + +AnimRandomSwitch::RandomSwitchState::Pointer AnimRandomSwitch::evaluateTransitions(const AnimVariantMap& animVars) const { + assert(_currentState); + for (auto& transition : _currentState->_transitions) { + if (animVars.lookup(transition._var, false)) { + return transition._randomSwitchState; + } + } + return _currentState; +} + +const AnimPoseVec& AnimRandomSwitch::getPosesInternal() const { + return _poses; +} diff --git a/libraries/animation/src/AnimRandomSwitch.h b/libraries/animation/src/AnimRandomSwitch.h new file mode 100644 index 0000000000..7a750cd89f --- /dev/null +++ b/libraries/animation/src/AnimRandomSwitch.h @@ -0,0 +1,184 @@ +// +// AnimRandomSwitch.h +// +// Created by Angus Antley on 4/8/19. +// Copyright (c) 2019 High Fidelity, Inc. All rights reserved. +// +// 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_AnimRandomSwitch_h +#define hifi_AnimRandomSwitch_h + +#include +#include +#include "AnimNode.h" + +// Random Switch State Machine for random transitioning between children AnimNodes +// +// This is mechanisim for choosing and playing a random animation and smoothly interpolating/fading +// between them. A RandomSwitch has a set of States, which typically reference +// child AnimNodes. Each Random Switch State has a list of Transitions, which are evaluated +// to determine when we should switch to a new State. Parameters for the smooth +// interpolation/fading are read from the Random Switch State that you are transitioning to. +// +// The currentState can be set directly via the setCurrentStateVar() and will override +// any State transitions. +// +// Each Random Switch State has two parameters that can be changed via AnimVars, +// * interpTarget - (frames) The destination frame of the interpolation. i.e. the first frame of the animation that will +// visible after interpolation is complete. +// * interpDuration - (frames) The total length of time it will take to interp between the current pose and the +// interpTarget frame. +// * interpType - How the interpolation is performed. +// * priority - this number represents how likely this Random Switch State will be chosen. +// the priority for each Random Switch State will be normalized, so their relative size is what is important +// * resume - if resume is false then if this state is chosen twice in a row it will remember what frame it was playing on. +// * SnapshotBoth: Stores two snapshots, the previous animation before interpolation begins and the target state at the +// interTarget frame. Then during the interpolation period the two snapshots are interpolated to produce smooth motion between them. +// * SnapshotPrev: Stores a snapshot of the previous animation before interpolation begins. However the target state is +// evaluated dynamically. During the interpolation period the previous snapshot is interpolated with the target pose +// to produce smooth motion between them. This mode is useful for interping into a blended animation where the actual +// blend factor is not known at the start of the interp or is might change dramatically during the interp. +// + +class AnimRandomSwitch : public AnimNode { +public: + friend class AnimNodeLoader; + friend bool processRandomSwitchStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl); + + enum class InterpType { + SnapshotBoth = 0, + SnapshotPrev, + NumTypes + }; + +protected: + + class RandomSwitchState { + public: + friend AnimRandomSwitch; + friend bool processRandomSwitchStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl); + + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; + + class Transition { + public: + friend AnimRandomSwitch; + Transition(const QString& var, RandomSwitchState::Pointer randomState) : _var(var), _randomSwitchState(randomState) {} + protected: + QString _var; + RandomSwitchState::Pointer _randomSwitchState; + }; + + RandomSwitchState(const QString& id, int childIndex, float interpTarget, float interpDuration, InterpType interpType, float priority, bool resume) : + _id(id), + _childIndex(childIndex), + _interpTarget(interpTarget), + _interpDuration(interpDuration), + _interpType(interpType), + _priority(priority), + _resume(resume){ + } + + void setInterpTargetVar(const QString& interpTargetVar) { _interpTargetVar = interpTargetVar; } + void setInterpDurationVar(const QString& interpDurationVar) { _interpDurationVar = interpDurationVar; } + void setInterpTypeVar(const QString& interpTypeVar) { _interpTypeVar = interpTypeVar; } + + int getChildIndex() const { return _childIndex; } + float getPriority() const { return _priority; } + bool getResume() const { return _resume; } + const QString& getID() const { return _id; } + + protected: + + void setInterpTarget(float interpTarget) { _interpTarget = interpTarget; } + void setInterpDuration(float interpDuration) { _interpDuration = interpDuration; } + void setPriority(float priority) { _priority = priority; } + void setResumeFlag(bool resume) { _resume = resume; } + + void addTransition(const Transition& transition) { _transitions.push_back(transition); } + + QString _id; + int _childIndex; + float _interpTarget; // frames + float _interpDuration; // frames + InterpType _interpType; + float _priority {0.0f}; + bool _resume {false}; + + QString _interpTargetVar; + QString _interpDurationVar; + QString _interpTypeVar; + + std::vector _transitions; + + private: + // no copies + RandomSwitchState(const RandomSwitchState&) = delete; + RandomSwitchState& operator=(const RandomSwitchState&) = delete; + }; + +public: + + explicit AnimRandomSwitch(const QString& id); + virtual ~AnimRandomSwitch() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) override; + + void setCurrentStateVar(QString& currentStateVar) { _currentStateVar = currentStateVar; } + +protected: + + void setCurrentState(RandomSwitchState::Pointer randomState); + void setTriggerRandomSwitchVar(const QString& triggerRandomSwitchVar) { _triggerRandomSwitchVar = triggerRandomSwitchVar; } + void setRandomSwitchTimeMin(float randomSwitchTimeMin) { _randomSwitchTimeMin = randomSwitchTimeMin; } + void setRandomSwitchTimeMax(float randomSwitchTimeMax) { _randomSwitchTimeMax = randomSwitchTimeMax; } + void setTransitionVar(const QString& transitionVar) { _transitionVar = transitionVar; } + void setTriggerTimeMin(float triggerTimeMin) { _triggerTimeMin = triggerTimeMin; } + void setTriggerTimeMax(float triggerTimeMax) { _triggerTimeMax = triggerTimeMax; } + void addToPrioritySum(float priority) { _totalPriorities += priority; } + + void addState(RandomSwitchState::Pointer randomState); + + void switchRandomState(const AnimVariantMap& animVars, const AnimContext& context, RandomSwitchState::Pointer desiredState, bool shouldInterp); + RandomSwitchState::Pointer evaluateTransitions(const AnimVariantMap& animVars) const; + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + + AnimPoseVec _poses; + + int _randomSwitchEvaluationCount { 0 }; + // interpolation state + bool _duringInterp = false; + InterpType _interpType{ InterpType::SnapshotPrev }; + float _alphaVel = 0.0f; + float _alpha = 0.0f; + AnimPoseVec _prevPoses; + AnimPoseVec _nextPoses; + float _totalPriorities { 0.0f }; + + RandomSwitchState::Pointer _currentState; + RandomSwitchState::Pointer _previousState; + std::vector _randomStates; + + QString _currentStateVar; + QString _triggerRandomSwitchVar; + QString _transitionVar; + float _triggerTimeMin { 10.0f }; + float _triggerTimeMax { 20.0f }; + float _triggerTime { 0.0f }; + float _randomSwitchTimeMin { 10.0f }; + float _randomSwitchTimeMax { 20.0f }; + float _randomSwitchTime { 0.0f }; + +private: + // no copies + AnimRandomSwitch(const AnimRandomSwitch&) = delete; + AnimRandomSwitch& operator=(const AnimRandomSwitch&) = delete; +}; + +#endif // hifi_AnimRandomSwitch_h diff --git a/libraries/animation/src/AnimTwoBoneIK.cpp b/libraries/animation/src/AnimTwoBoneIK.cpp index c91518d5db..b3686b4b57 100644 --- a/libraries/animation/src/AnimTwoBoneIK.cpp +++ b/libraries/animation/src/AnimTwoBoneIK.cpp @@ -128,7 +128,7 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const if (triggersOut.hasKey(endEffectorPositionVar)) { targetPose.trans() = triggersOut.lookupRigToGeometry(endEffectorPositionVar, tipPose.trans()); - } else if (animVars.hasKey(endEffectorRotationVar)) { + } else if (animVars.hasKey(endEffectorPositionVar)) { targetPose.trans() = animVars.lookupRigToGeometry(endEffectorPositionVar, tipPose.trans()); } @@ -147,9 +147,11 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const // http://mathworld.wolfram.com/Circle-CircleIntersection.html float midAngle = 0.0f; - if (d < r0 + r1) { + if ((d < r0 + r1) && (d > 0.0f) && (r0 > 0.0f) && (r1 > 0.0f)) { float y = sqrtf((-d + r1 - r0) * (-d - r1 + r0) * (-d + r1 + r0) * (d + r1 + r0)) / (2.0f * d); - midAngle = PI - (acosf(y / r0) + acosf(y / r1)); + float yR0Quotient = glm::clamp(y / r0, -1.0f, 1.0f); + float yR1Quotient = glm::clamp(y / r1, -1.0f, 1.0f); + midAngle = PI - (acosf(yR0Quotient) + acosf(yR1Quotient)); } // compute midJoint rotation diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index c23e228556..5fca2b4f88 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -142,3 +142,72 @@ glm::quat computeBodyFacingFromHead(const glm::quat& headRot, const glm::vec3& u return glmExtractRotation(bodyMat); } + + +const float INV_SQRT_3 = 1.0f / sqrtf(3.0f); +const int DOP14_COUNT = 14; +const glm::vec3 DOP14_NORMALS[DOP14_COUNT] = { + Vectors::UNIT_X, + -Vectors::UNIT_X, + Vectors::UNIT_Y, + -Vectors::UNIT_Y, + Vectors::UNIT_Z, + -Vectors::UNIT_Z, + glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), + -glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), + glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), + -glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), + glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), + -glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), + glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3), + -glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3) +}; + +// returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose. +// if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward +// such that it lies on the surface of the kdop. +bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const HFMJointShapeInfo& shapeInfo, glm::vec3& displacementOut) { + + // transform point into local space of jointShape. + glm::vec3 localPoint = shapePose.inverse().xformPoint(point); + + // Only works for 14-dop shape infos. + if (shapeInfo.dots.size() != DOP14_COUNT) { + return false; + } + + glm::vec3 minDisplacement(FLT_MAX); + float minDisplacementLen = FLT_MAX; + glm::vec3 p = localPoint - shapeInfo.avgPoint; + float pLen = glm::length(p); + if (pLen > 0.0f) { + int slabCount = 0; + for (int i = 0; i < DOP14_COUNT; i++) { + float dot = glm::dot(p, DOP14_NORMALS[i]); + if (dot > 0.0f && dot < shapeInfo.dots[i]) { + slabCount++; + float distToPlane = pLen * (shapeInfo.dots[i] / dot); + float displacementLen = distToPlane - pLen; + + // keep track of the smallest displacement + if (displacementLen < minDisplacementLen) { + minDisplacementLen = displacementLen; + minDisplacement = (p / pLen) * displacementLen; + } + } + } + if (slabCount == (DOP14_COUNT / 2) && minDisplacementLen != FLT_MAX) { + // we are within the k-dop so push the point along the minimum displacement found + displacementOut = shapePose.xformVectorFast(minDisplacement); + return true; + } else { + // point is outside of kdop + return false; + } + } else { + // point is directly on top of shapeInfo.avgPoint. + // push the point out along the x axis. + displacementOut = shapePose.xformVectorFast(shapeInfo.points[0]); + return true; + } +} diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index cf190e8dbf..c2925e31e8 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -128,4 +128,10 @@ protected: bool _snapshotValid { false }; }; + +// returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose. +// if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward +// such that it lies on the surface of the kdop. +bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const HFMJointShapeInfo& shapeInfo, glm::vec3& displacementOut); + #endif diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index eb9ebd33dd..a8bdb885e5 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -261,7 +261,7 @@ public: qCDebug(animation) << " " << pair.first << "=" << pair.second.getString(); break; default: - assert(("invalid AnimVariant::Type", false)); + assert(false); } } } diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index 1f9e72bf28..41db9d5606 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -499,12 +499,12 @@ void Flow::calculateConstraints(const std::shared_ptr& skeleton, bool toFloatSuccess; QStringRef(&name, (int)(name.size() - j), 1).toString().toFloat(&toFloatSuccess); if (!toFloatSuccess && (name.size() - j) > (int)simPrefix.size()) { - group = QStringRef(&name, (int)simPrefix.size(), (int)(name.size() - j + 1)).toString(); + group = QStringRef(&name, (int)simPrefix.size(), (int)(name.size() - j + 1) - (int)simPrefix.size()).toString(); break; } } if (group.isEmpty()) { - group = QStringRef(&name, (int)simPrefix.size(), name.size() - 1).toString(); + group = QStringRef(&name, (int)simPrefix.size(), name.size() - (int)simPrefix.size()).toString(); } qCDebug(animation) << "Sim joint added to flow: " << name; } else { diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 564dba7f05..331acedd4e 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -26,8 +26,9 @@ public: * 0RotationAndPositionAttempt to reach the rotation and position end * effector. * 1RotationOnlyAttempt to reach the end effector rotation only. - * 2HmdHeadDeprecated: A special mode of IK that would attempt - * to prevent unnecessary bending of the spine. + * 2HmdHeadA special mode of IK that would attempt to prevent unnecessary + * bending of the spine.
+ *

Deprecated: This target type is deprecated and will be removed.

* 3HipsRelativeRotationAndPositionAttempt to reach a rotation and position end * effector that is not in absolute rig coordinates but is offset by the avatar hips translation. * 4SplineUse a cubic Hermite spline to model the human spine. This prevents diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 43e94d23e8..633a505d14 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -370,6 +370,88 @@ void Rig::restoreAnimation() { } } +void Rig::overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { + HandAnimState::ClipNodeEnum clipNodeEnum; + if (isLeft) { + if (_leftHandAnimState.clipNodeEnum == HandAnimState::None || _leftHandAnimState.clipNodeEnum == HandAnimState::B) { + clipNodeEnum = HandAnimState::A; + } else { + clipNodeEnum = HandAnimState::B; + } + } else { + if (_rightHandAnimState.clipNodeEnum == HandAnimState::None || _rightHandAnimState.clipNodeEnum == HandAnimState::B) { + clipNodeEnum = HandAnimState::A; + } else { + clipNodeEnum = HandAnimState::B; + } + } + + if (_animNode) { + std::shared_ptr clip; + if (isLeft) { + if (clipNodeEnum == HandAnimState::A) { + clip = std::dynamic_pointer_cast(_animNode->findByName("leftHandAnimA")); + } else { + clip = std::dynamic_pointer_cast(_animNode->findByName("leftHandAnimB")); + } + } else { + if (clipNodeEnum == HandAnimState::A) { + clip = std::dynamic_pointer_cast(_animNode->findByName("rightHandAnimA")); + } else { + clip = std::dynamic_pointer_cast(_animNode->findByName("rightHandAnimB")); + } + } + + if (clip) { + // set parameters + clip->setLoopFlag(loop); + clip->setStartFrame(firstFrame); + clip->setEndFrame(lastFrame); + const float REFERENCE_FRAMES_PER_SECOND = 30.0f; + float timeScale = fps / REFERENCE_FRAMES_PER_SECOND; + clip->setTimeScale(timeScale); + clip->loadURL(url); + } + } + + // notify the handAnimStateMachine the desired state. + if (isLeft) { + // store current hand anim state. + _leftHandAnimState = { clipNodeEnum, url, fps, loop, firstFrame, lastFrame }; + _animVars.set("leftHandAnimNone", false); + _animVars.set("leftHandAnimA", clipNodeEnum == HandAnimState::A); + _animVars.set("leftHandAnimB", clipNodeEnum == HandAnimState::B); + } else { + // store current hand anim state. + _rightHandAnimState = { clipNodeEnum, url, fps, loop, firstFrame, lastFrame }; + _animVars.set("rightHandAnimNone", false); + _animVars.set("rightHandAnimA", clipNodeEnum == HandAnimState::A); + _animVars.set("rightHandAnimB", clipNodeEnum == HandAnimState::B); + } +} + +void Rig::restoreHandAnimation(bool isLeft) { + if (isLeft) { + if (_leftHandAnimState.clipNodeEnum != HandAnimState::None) { + _leftHandAnimState.clipNodeEnum = HandAnimState::None; + + // notify the handAnimStateMachine the desired state. + _animVars.set("leftHandAnimNone", true); + _animVars.set("leftHandAnimA", false); + _animVars.set("leftHandAnimB", false); + } + } else { + if (_rightHandAnimState.clipNodeEnum != HandAnimState::None) { + _rightHandAnimState.clipNodeEnum = HandAnimState::None; + + // notify the handAnimStateMachine the desired state. + _animVars.set("rightHandAnimNone", true); + _animVars.set("rightHandAnimA", false); + _animVars.set("rightHandAnimB", false); + } + } +} + void Rig::overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { NetworkAnimState::ClipNodeEnum clipNodeEnum = NetworkAnimState::None; @@ -1398,13 +1480,15 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons if (_animNode && _enabledAnimations) { DETAILED_PERFORMANCE_TIMER("handleTriggers"); + ++_evaluationCount; + updateAnimationStateHandlers(); _animVars.setRigToGeometryTransform(_rigToGeometryTransform); if (_networkNode) { _networkVars.setRigToGeometryTransform(_rigToGeometryTransform); } AnimContext context(_enableDebugDrawIKTargets, _enableDebugDrawIKConstraints, _enableDebugDrawIKChains, - getGeometryToRigTransform(), rigToWorldTransform); + getGeometryToRigTransform(), rigToWorldTransform, _evaluationCount); // evaluate the animation AnimVariantMap triggersOut; @@ -1521,74 +1605,6 @@ void Rig::updateHead(bool headEnabled, bool hipsEnabled, const AnimPose& headPos } } -const float INV_SQRT_3 = 1.0f / sqrtf(3.0f); -const int DOP14_COUNT = 14; -const glm::vec3 DOP14_NORMALS[DOP14_COUNT] = { - Vectors::UNIT_X, - -Vectors::UNIT_X, - Vectors::UNIT_Y, - -Vectors::UNIT_Y, - Vectors::UNIT_Z, - -Vectors::UNIT_Z, - glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), - -glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), - glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), - -glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), - glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), - -glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), - glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3), - -glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3) -}; - -// returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose. -// if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward -// such that it lies on the surface of the kdop. -static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const HFMJointShapeInfo& shapeInfo, glm::vec3& displacementOut) { - - // transform point into local space of jointShape. - glm::vec3 localPoint = shapePose.inverse().xformPoint(point); - - // Only works for 14-dop shape infos. - if (shapeInfo.dots.size() != DOP14_COUNT) { - return false; - } - - glm::vec3 minDisplacement(FLT_MAX); - float minDisplacementLen = FLT_MAX; - glm::vec3 p = localPoint - shapeInfo.avgPoint; - float pLen = glm::length(p); - if (pLen > 0.0f) { - int slabCount = 0; - for (int i = 0; i < DOP14_COUNT; i++) { - float dot = glm::dot(p, DOP14_NORMALS[i]); - if (dot > 0.0f && dot < shapeInfo.dots[i]) { - slabCount++; - float distToPlane = pLen * (shapeInfo.dots[i] / dot); - float displacementLen = distToPlane - pLen; - - // keep track of the smallest displacement - if (displacementLen < minDisplacementLen) { - minDisplacementLen = displacementLen; - minDisplacement = (p / pLen) * displacementLen; - } - } - } - if (slabCount == (DOP14_COUNT / 2) && minDisplacementLen != FLT_MAX) { - // we are within the k-dop so push the point along the minimum displacement found - displacementOut = shapePose.xformVectorFast(minDisplacement); - return true; - } else { - // point is outside of kdop - return false; - } - } else { - // point is directly on top of shapeInfo.avgPoint. - // push the point out along the x axis. - displacementOut = shapePose.xformVectorFast(shapeInfo.points[0]); - return true; - } -} - glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo, const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo) const { glm::vec3 position = handPosition; @@ -1995,8 +2011,35 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo return; } - _animVars.set("isTalking", params.isTalking); - _animVars.set("notIsTalking", !params.isTalking); + if (_previousIsTalking != params.isTalking) { + if (_talkIdleInterpTime < 1.0f) { + _talkIdleInterpTime = 1.0f - _talkIdleInterpTime; + } else { + _talkIdleInterpTime = 0.0f; + } + } + _previousIsTalking = params.isTalking; + + const float TOTAL_EASE_IN_TIME = 0.75f; + const float TOTAL_EASE_OUT_TIME = 1.5f; + if (params.isTalking) { + if (_talkIdleInterpTime < 1.0f) { + _talkIdleInterpTime += dt / TOTAL_EASE_IN_TIME; + float easeOutInValue = _talkIdleInterpTime < 0.5f ? 4.0f * powf(_talkIdleInterpTime, 3.0f) : 4.0f * powf((_talkIdleInterpTime - 1.0f), 3.0f) + 1.0f; + _animVars.set("idleOverlayAlpha", easeOutInValue); + } else { + _animVars.set("idleOverlayAlpha", 1.0f); + } + } else { + if (_talkIdleInterpTime < 1.0f) { + _talkIdleInterpTime += dt / TOTAL_EASE_OUT_TIME; + float easeOutInValue = _talkIdleInterpTime < 0.5f ? 4.0f * powf(_talkIdleInterpTime, 3.0f) : 4.0f * powf((_talkIdleInterpTime - 1.0f), 3.0f) + 1.0f; + float talkAlpha = 1.0f - easeOutInValue; + _animVars.set("idleOverlayAlpha", talkAlpha); + } else { + _animVars.set("idleOverlayAlpha", 0.0f); + } + } _headEnabled = params.primaryControllerFlags[PrimaryControllerType_Head] & (uint8_t)ControllerFlags::Enabled; bool leftHandEnabled = params.primaryControllerFlags[PrimaryControllerType_LeftHand] & (uint8_t)ControllerFlags::Enabled; @@ -2136,6 +2179,20 @@ void Rig::initAnimGraph(const QUrl& url) { overrideAnimation(origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); } + if (_rightHandAnimState.clipNodeEnum != HandAnimState::None) { + // restore the right hand animation we had before reset. + HandAnimState origState = _rightHandAnimState; + _rightHandAnimState = { HandAnimState::None, "", 30.0f, false, 0.0f, 0.0f }; + overrideHandAnimation(false, origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); + } + + if (_leftHandAnimState.clipNodeEnum != HandAnimState::None) { + // restore the left hand animation we had before reset. + HandAnimState origState = _leftHandAnimState; + _leftHandAnimState = { HandAnimState::None, "", 30.0f, false, 0.0f, 0.0f }; + overrideHandAnimation(true, origState.url, origState.fps, origState.loop, origState.firstFrame, origState.lastFrame); + } + // restore the role animations we had before reset. for (auto& roleAnimState : _roleAnimStates) { auto roleState = roleAnimState.second; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index df13ff5c2b..786d14200e 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -116,8 +116,12 @@ public: void destroyAnimGraph(); void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + bool isPlayingOverrideAnimation() const { return _userAnimState.clipNodeEnum != UserAnimState::None; }; void restoreAnimation(); + void overrideHandAnimation(bool isLeft, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + void restoreHandAnimation(bool isLeft); + void overrideNetworkAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void triggerNetworkRole(const QString& role); void restoreNetworkAnimation(); @@ -333,7 +337,7 @@ protected: RigRole _state { RigRole::Idle }; RigRole _desiredState { RigRole::Idle }; float _desiredStateAge { 0.0f }; - + struct NetworkAnimState { enum ClipNodeEnum { None = 0, @@ -356,6 +360,27 @@ protected: float blendTime; }; + struct HandAnimState { + enum ClipNodeEnum { + None = 0, + A, + B + }; + + HandAnimState() : clipNodeEnum(HandAnimState::None) {} + HandAnimState(ClipNodeEnum clipNodeEnumIn, const QString& urlIn, float fpsIn, bool loopIn, float firstFrameIn, float lastFrameIn) : + clipNodeEnum(clipNodeEnumIn), url(urlIn), fps(fpsIn), loop(loopIn), firstFrame(firstFrameIn), lastFrame(lastFrameIn) { + } + + + ClipNodeEnum clipNodeEnum; + QString url; + float fps; + bool loop; + float firstFrame; + float lastFrame; + }; + struct UserAnimState { enum ClipNodeEnum { None = 0, @@ -390,10 +415,15 @@ protected: UserAnimState _userAnimState; NetworkAnimState _networkAnimState; + HandAnimState _rightHandAnimState; + HandAnimState _leftHandAnimState; std::map _roleAnimStates; + int _evaluationCount{ 0 }; float _leftHandOverlayAlpha { 0.0f }; float _rightHandOverlayAlpha { 0.0f }; + float _talkIdleInterpTime { 0.0f }; + bool _previousIsTalking { false }; SimpleMovingAverage _averageForwardSpeed { 10 }; SimpleMovingAverage _averageLateralSpeed { 10 }; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index c537fea646..4d3311b065 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1397,7 +1397,6 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // spatialize into mixBuffer injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } else if (options.stereo) { if (options.positionSet) { @@ -1409,11 +1408,8 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } // direct mix into mixBuffer - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) { - mixBuffer[2*i+0] += convertToFloat(_localScratchBuffer[2*i+0]) * gain; - mixBuffer[2*i+1] += convertToFloat(_localScratchBuffer[2*i+1]) * gain; - } - + injector->getLocalHRTF().mixStereo(_localScratchBuffer, mixBuffer, gain, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } else { // injector is mono if (options.positionSet) { @@ -1431,11 +1427,8 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } else { // direct mix into mixBuffer - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) { - float sample = convertToFloat(_localScratchBuffer[i]) * gain; - mixBuffer[2*i+0] += sample; - mixBuffer[2*i+1] += sample; - } + injector->getLocalHRTF().mixMono(_localScratchBuffer, mixBuffer, gain, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } } diff --git a/libraries/audio/src/AudioFOA.cpp b/libraries/audio/src/AudioFOA.cpp index 30d29b72b7..0dd61fbd02 100644 --- a/libraries/audio/src/AudioFOA.cpp +++ b/libraries/audio/src/AudioFOA.cpp @@ -882,14 +882,16 @@ static void convertInput_ref(int16_t* src, float *dst[4], float gain, int numFra #endif -// in-place rotation of the soundfield -// crossfade between old and new rotation, to prevent artifacts -static void rotate_3x3_ref(float* buf[4], const float m0[3][3], const float m1[3][3], const float* win, int numFrames) { +// in-place rotation and scaling of the soundfield +// crossfade between old and new matrix, to prevent artifacts +static void rotate_4x4_ref(float* buf[4], const float m0[4][4], const float m1[4][4], const float* win, int numFrames) { - const float md[3][3] = { - { m0[0][0] - m1[0][0], m0[0][1] - m1[0][1], m0[0][2] - m1[0][2] }, - { m0[1][0] - m1[1][0], m0[1][1] - m1[1][1], m0[1][2] - m1[1][2] }, - { m0[2][0] - m1[2][0], m0[2][1] - m1[2][1], m0[2][2] - m1[2][2] }, + // matrix difference + const float md[4][4] = { + { m0[0][0] - m1[0][0], m0[0][1] - m1[0][1], m0[0][2] - m1[0][2], m0[0][3] - m1[0][3] }, + { m0[1][0] - m1[1][0], m0[1][1] - m1[1][1], m0[1][2] - m1[1][2], m0[1][3] - m1[1][3] }, + { m0[2][0] - m1[2][0], m0[2][1] - m1[2][1], m0[2][2] - m1[2][2], m0[2][3] - m1[2][3] }, + { m0[3][0] - m1[3][0], m0[3][1] - m1[3][1], m0[3][2] - m1[3][2], m0[3][3] - m1[3][3] }, }; for (int i = 0; i < numFrames; i++) { @@ -898,22 +900,27 @@ static void rotate_3x3_ref(float* buf[4], const float m0[3][3], const float m1[3 // interpolate the matrix float m00 = m1[0][0] + frac * md[0][0]; - float m10 = m1[1][0] + frac * md[1][0]; - float m20 = m1[2][0] + frac * md[2][0]; - float m01 = m1[0][1] + frac * md[0][1]; float m11 = m1[1][1] + frac * md[1][1]; float m21 = m1[2][1] + frac * md[2][1]; + float m31 = m1[3][1] + frac * md[3][1]; - float m02 = m1[0][2] + frac * md[0][2]; float m12 = m1[1][2] + frac * md[1][2]; float m22 = m1[2][2] + frac * md[2][2]; + float m32 = m1[3][2] + frac * md[3][2]; + + float m13 = m1[1][3] + frac * md[1][3]; + float m23 = m1[2][3] + frac * md[2][3]; + float m33 = m1[3][3] + frac * md[3][3]; // matrix multiply - float x = m00 * buf[1][i] + m01 * buf[2][i] + m02 * buf[3][i]; - float y = m10 * buf[1][i] + m11 * buf[2][i] + m12 * buf[3][i]; - float z = m20 * buf[1][i] + m21 * buf[2][i] + m22 * buf[3][i]; + float w = m00 * buf[0][i]; + float x = m11 * buf[1][i] + m12 * buf[2][i] + m13 * buf[3][i]; + float y = m21 * buf[1][i] + m22 * buf[2][i] + m23 * buf[3][i]; + float z = m31 * buf[1][i] + m32 * buf[2][i] + m33 * buf[3][i]; + + buf[0][i] = w; buf[1][i] = x; buf[2][i] = y; buf[3][i] = z; @@ -932,7 +939,7 @@ void rfft512_AVX2(float buf[512]); void rifft512_AVX2(float buf[512]); void rfft512_cmadd_1X2_AVX2(const float src[512], const float coef0[512], const float coef1[512], float dst0[512], float dst1[512]); void convertInput_AVX2(int16_t* src, float *dst[4], float gain, int numFrames); -void rotate_3x3_AVX2(float* buf[4], const float m0[3][3], const float m1[3][3], const float* win, int numFrames); +void rotate_4x4_AVX2(float* buf[4], const float m0[4][4], const float m1[4][4], const float* win, int numFrames); static void rfft512(float buf[512]) { static auto f = cpuSupportsAVX2() ? rfft512_AVX2 : rfft512_ref; @@ -954,8 +961,8 @@ static void convertInput(int16_t* src, float *dst[4], float gain, int numFrames) (*f)(src, dst, gain, numFrames); // dispatch } -static void rotate_3x3(float* buf[4], const float m0[3][3], const float m1[3][3], const float* win, int numFrames) { - static auto f = cpuSupportsAVX2() ? rotate_3x3_AVX2 : rotate_3x3_ref; +static void rotate_4x4(float* buf[4], const float m0[4][4], const float m1[4][4], const float* win, int numFrames) { + static auto f = cpuSupportsAVX2() ? rotate_4x4_AVX2 : rotate_4x4_ref; (*f)(buf, m0, m1, win, numFrames); // dispatch } @@ -965,7 +972,7 @@ static auto& rfft512 = rfft512_ref; static auto& rifft512 = rifft512_ref; static auto& rfft512_cmadd_1X2 = rfft512_cmadd_1X2_ref; static auto& convertInput = convertInput_ref; -static auto& rotate_3x3 = rotate_3x3_ref; +static auto& rotate_4x4 = rotate_4x4_ref; #endif @@ -1007,8 +1014,8 @@ ALIGN32 static const float crossfadeTable[FOA_BLOCK] = { 0.0020975362f, 0.0015413331f, 0.0010705384f, 0.0006852326f, 0.0003854819f, 0.0001713375f, 0.0000428362f, 0.0000000000f, }; -// convert quaternion to a column-major 3x3 rotation matrix -static void quatToMatrix_3x3(float w, float x, float y, float z, float m[3][3]) { +// convert quaternion to a column-major 4x4 rotation matrix +static void quatToMatrix_4x4(float w, float x, float y, float z, float m[4][4]) { float xx = x * (x + x); float xy = x * (y + y); @@ -1022,17 +1029,33 @@ static void quatToMatrix_3x3(float w, float x, float y, float z, float m[3][3]) float wy = w * (y + y); float wz = w * (z + z); - m[0][0] = 1.0f - (yy + zz); - m[0][1] = xy - wz; - m[0][2] = xz + wy; + m[0][0] = 1.0f; + m[0][1] = 0.0f; + m[0][2] = 0.0f; + m[0][3] = 0.0f; - m[1][0] = xy + wz; - m[1][1] = 1.0f - (xx + zz); - m[1][2] = yz - wx; + m[1][0] = 0.0f; + m[1][1] = 1.0f - (yy + zz); + m[1][2] = xy - wz; + m[1][3] = xz + wy; - m[2][0] = xz - wy; - m[2][1] = yz + wx; - m[2][2] = 1.0f - (xx + yy); + m[2][0] = 0.0f; + m[2][1] = xy + wz; + m[2][2] = 1.0f - (xx + zz); + m[2][3] = yz - wx; + + m[3][0] = 0.0f; + m[3][1] = xz - wy; + m[3][2] = yz + wx; + m[3][3] = 1.0f - (xx + yy); +} + +static void scaleMatrix_4x4(float scale, float m[4][4]) { + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + m[i][j] *= scale; + } + } } // Ambisonic to binaural render @@ -1047,18 +1070,26 @@ void AudioFOA::render(int16_t* input, float* output, int index, float qw, float ALIGN32 float inBuffer[4][FOA_BLOCK]; // deinterleaved input buffers float* in[4] = { inBuffer[0], inBuffer[1], inBuffer[2], inBuffer[3] }; - float rotation[3][3]; + float rotation[4][4]; // convert input to deinterleaved float - convertInput(input, in, FOA_GAIN * gain, FOA_BLOCK); + convertInput(input, in, FOA_GAIN, FOA_BLOCK); - // convert quaternion to 3x3 rotation - quatToMatrix_3x3(qw, qx, qy, qz, rotation); + // convert quaternion to 4x4 rotation + quatToMatrix_4x4(qw, qx, qy, qz, rotation); - // rotate the soundfield - rotate_3x3(in, _rotationState, rotation, crossfadeTable, FOA_BLOCK); + // apply gain as uniform scale + scaleMatrix_4x4(gain, rotation); - // rotation history update + // disable interpolation from reset state + if (_resetState) { + memcpy(_rotationState, rotation, sizeof(_rotationState)); + } + + // rotate and scale the soundfield + rotate_4x4(in, _rotationState, rotation, crossfadeTable, FOA_BLOCK); + + // new parameters become old memcpy(_rotationState, rotation, sizeof(_rotationState)); // @@ -1093,4 +1124,6 @@ void AudioFOA::render(int16_t* input, float* output, int index, float qw, float output[2*i+0] += accBuffer[0][i + FOA_OVERLAP]; output[2*i+1] += accBuffer[1][i + FOA_OVERLAP]; } + + _resetState = false; } diff --git a/libraries/audio/src/AudioFOA.h b/libraries/audio/src/AudioFOA.h index 9eccc35bce..e8cacc22ab 100644 --- a/libraries/audio/src/AudioFOA.h +++ b/libraries/audio/src/AudioFOA.h @@ -28,12 +28,7 @@ static_assert((FOA_BLOCK + FOA_OVERLAP) == FOA_NFFT, "FFT convolution requires L class AudioFOA { public: - AudioFOA() { - // identity matrix - _rotationState[0][0] = 1.0f; - _rotationState[1][1] = 1.0f; - _rotationState[2][2] = 1.0f; - }; + AudioFOA() {}; // // input: interleaved First-Order Ambisonic source @@ -55,8 +50,10 @@ private: // input history, for overlap-save float _fftState[4][FOA_OVERLAP] = {}; - // orientation history - float _rotationState[3][3] = {}; + // orientation and gain history + float _rotationState[4][4] = {}; + + bool _resetState = true; }; #endif // AudioFOA_h diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index 9de6440d67..e5e32781b0 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -750,6 +750,43 @@ static void interpolate(const float* src0, const float* src1, float* dst, float #endif +// apply gain crossfade with accumulation (interleaved) +static void gainfade_1x2(int16_t* src, float* dst, const float* win, float gain0, float gain1, int numFrames) { + + gain0 *= (1/32768.0f); // int16_t to float + gain1 *= (1/32768.0f); + + for (int i = 0; i < numFrames; i++) { + + float frac = win[i]; + float gain = gain1 + frac * (gain0 - gain1); + + float x0 = (float)src[i] * gain; + + dst[2*i+0] += x0; + dst[2*i+1] += x0; + } +} + +// apply gain crossfade with accumulation (interleaved) +static void gainfade_2x2(int16_t* src, float* dst, const float* win, float gain0, float gain1, int numFrames) { + + gain0 *= (1/32768.0f); // int16_t to float + gain1 *= (1/32768.0f); + + for (int i = 0; i < numFrames; i++) { + + float frac = win[i]; + float gain = gain1 + frac * (gain0 - gain1); + + float x0 = (float)src[2*i+0] * gain; + float x1 = (float)src[2*i+1] * gain; + + dst[2*i+0] += x0; + dst[2*i+1] += x1; + } +} + // design a 2nd order Thiran allpass static void ThiranBiquad(float f, float& b0, float& b1, float& b2, float& a1, float& a2) { @@ -1104,6 +1141,13 @@ void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, // apply global and local gain adjustment gain *= _gainAdjust; + // disable interpolation from reset state + if (_resetState) { + _azimuthState = azimuth; + _distanceState = distance; + _gainState = gain; + } + // to avoid polluting the cache, old filters are recomputed instead of stored setFilters(firCoef, bqCoef, delay, index, _azimuthState, _distanceState, _gainState, L0); @@ -1175,3 +1219,45 @@ void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, _resetState = false; } + +void AudioHRTF::mixMono(int16_t* input, float* output, float gain, int numFrames) { + + assert(numFrames == HRTF_BLOCK); + + // apply global and local gain adjustment + gain *= _gainAdjust; + + // disable interpolation from reset state + if (_resetState) { + _gainState = gain; + } + + // crossfade gain and accumulate + gainfade_1x2(input, output, crossfadeTable, _gainState, gain, HRTF_BLOCK); + + // new parameters become old + _gainState = gain; + + _resetState = false; +} + +void AudioHRTF::mixStereo(int16_t* input, float* output, float gain, int numFrames) { + + assert(numFrames == HRTF_BLOCK); + + // apply global and local gain adjustment + gain *= _gainAdjust; + + // disable interpolation from reset state + if (_resetState) { + _gainState = gain; + } + + // crossfade gain and accumulate + gainfade_2x2(input, output, crossfadeTable, _gainState, gain, HRTF_BLOCK); + + // new parameters become old + _gainState = gain; + + _resetState = false; +} diff --git a/libraries/audio/src/AudioHRTF.h b/libraries/audio/src/AudioHRTF.h index 7d23f4825a..436d6318a5 100644 --- a/libraries/audio/src/AudioHRTF.h +++ b/libraries/audio/src/AudioHRTF.h @@ -50,6 +50,12 @@ public: // void render(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames); + // + // Non-spatialized direct mix (accumulates into existing output) + // + void mixMono(int16_t* input, float* output, float gain, int numFrames); + void mixStereo(int16_t* input, float* output, float gain, int numFrames); + // // Fast path when input is known to be silent and state as been flushed // diff --git a/libraries/audio/src/AudioInjectorOptions.cpp b/libraries/audio/src/AudioInjectorOptions.cpp index 0946841fc6..cb7da4de33 100644 --- a/libraries/audio/src/AudioInjectorOptions.cpp +++ b/libraries/audio/src/AudioInjectorOptions.cpp @@ -48,21 +48,23 @@ QScriptValue injectorOptionsToScriptValue(QScriptEngine* engine, const AudioInje } /**jsdoc - * Configures how an audio injector plays its audio. + * Configures where and how an audio injector plays its audio. * @typedef {object} AudioInjector.AudioInjectorOptions * @property {Vec3} position=Vec3.ZERO - The position in the domain to play the sound. * @property {Quat} orientation=Quat.IDENTITY - The orientation in the domain to play the sound in. * @property {number} volume=1.0 - Playback volume, between 0.0 and 1.0. * @property {number} pitch=1.0 - Alter the pitch of the sound, within +/- 2 octaves. The value is the relative sample rate to - * resample the sound at, range 0.062516.0. A value of 0.0625 lowers the - * pitch by 2 octaves; 1.0 is no change in pitch; 16.0 raises the pitch by 2 octaves. + * resample the sound at, range 0.062516.0.
+ * A value of 0.0625 lowers the pitch by 2 octaves.
+ * A value of 1.0 means there is no change in pitch.
+ * A value of 16.0 raises the pitch by 2 octaves. * @property {boolean} loop=false - If true, the sound is played repeatedly until playback is stopped. * @property {number} secondOffset=0 - Starts playback from a specified time (seconds) within the sound file, ≥ * 0. - * @property {boolean} localOnly=false - IF true, the sound is played back locally on the client rather than to + * @property {boolean} localOnly=false - If true, the sound is played back locally on the client rather than to * others via the audio mixer. - * @property {boolean} ignorePenumbra=false - Deprecated: This property is deprecated and will be - * removed. + * @property {boolean} ignorePenumbra=false -

Deprecated: This property is deprecated and will be + * removed.

*/ void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOptions& injectorOptions) { if (!object.isObject()) { diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index e5ba322599..62fdb9dcdc 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -124,7 +124,7 @@ typedef QSharedPointer SharedSoundPointer; * An audio resource, created by {@link SoundCache.getSound}, to be played back using {@link Audio.playSound}. *

Supported formats:

*
    - *
  • WAV: 16-bit uncompressed WAV at any sample rate, with 1 (mono), 2 (stereo), or 4 (ambisonic) channels.
  • + *
  • WAV: 16-bit uncompressed at any sample rate, with 1 (mono), 2 (stereo), or 4 (ambisonic) channels.
  • *
  • MP3: Mono or stereo, at any sample rate.
  • *
  • RAW: 48khz 16-bit mono or stereo. File name must include ".stereo" to be interpreted as stereo.
  • *
diff --git a/libraries/audio/src/avx2/AudioFOA_avx2.cpp b/libraries/audio/src/avx2/AudioFOA_avx2.cpp index 70f9b0e5f6..15d37fcc3a 100644 --- a/libraries/audio/src/avx2/AudioFOA_avx2.cpp +++ b/libraries/audio/src/avx2/AudioFOA_avx2.cpp @@ -1289,14 +1289,16 @@ void convertInput_AVX2(int16_t* src, float *dst[4], float gain, int numFrames) { #endif -// in-place rotation of the soundfield -// crossfade between old and new rotation, to prevent artifacts -void rotate_3x3_AVX2(float* buf[4], const float m0[3][3], const float m1[3][3], const float* win, int numFrames) { +// in-place rotation and scaling of the soundfield +// crossfade between old and new matrix, to prevent artifacts +void rotate_4x4_AVX2(float* buf[4], const float m0[4][4], const float m1[4][4], const float* win, int numFrames) { - const float md[3][3] = { - { m0[0][0] - m1[0][0], m0[0][1] - m1[0][1], m0[0][2] - m1[0][2] }, - { m0[1][0] - m1[1][0], m0[1][1] - m1[1][1], m0[1][2] - m1[1][2] }, - { m0[2][0] - m1[2][0], m0[2][1] - m1[2][1], m0[2][2] - m1[2][2] }, + // matrix difference + const float md[4][4] = { + { m0[0][0] - m1[0][0], m0[0][1] - m1[0][1], m0[0][2] - m1[0][2], m0[0][3] - m1[0][3] }, + { m0[1][0] - m1[1][0], m0[1][1] - m1[1][1], m0[1][2] - m1[1][2], m0[1][3] - m1[1][3] }, + { m0[2][0] - m1[2][0], m0[2][1] - m1[2][1], m0[2][2] - m1[2][2], m0[2][3] - m1[2][3] }, + { m0[3][0] - m1[3][0], m0[3][1] - m1[3][1], m0[3][2] - m1[3][2], m0[3][3] - m1[3][3] }, }; assert(numFrames % 8 == 0); @@ -1307,30 +1309,35 @@ void rotate_3x3_AVX2(float* buf[4], const float m0[3][3], const float m1[3][3], // interpolate the matrix __m256 m00 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[0][0]), _mm256_broadcast_ss(&m1[0][0])); - __m256 m10 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[1][0]), _mm256_broadcast_ss(&m1[1][0])); - __m256 m20 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[2][0]), _mm256_broadcast_ss(&m1[2][0])); - __m256 m01 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[0][1]), _mm256_broadcast_ss(&m1[0][1])); __m256 m11 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[1][1]), _mm256_broadcast_ss(&m1[1][1])); __m256 m21 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[2][1]), _mm256_broadcast_ss(&m1[2][1])); + __m256 m31 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[3][1]), _mm256_broadcast_ss(&m1[3][1])); - __m256 m02 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[0][2]), _mm256_broadcast_ss(&m1[0][2])); __m256 m12 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[1][2]), _mm256_broadcast_ss(&m1[1][2])); __m256 m22 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[2][2]), _mm256_broadcast_ss(&m1[2][2])); + __m256 m32 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[3][2]), _mm256_broadcast_ss(&m1[3][2])); + + __m256 m13 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[1][3]), _mm256_broadcast_ss(&m1[1][3])); + __m256 m23 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[2][3]), _mm256_broadcast_ss(&m1[2][3])); + __m256 m33 = _mm256_fmadd_ps(frac, _mm256_broadcast_ss(&md[3][3]), _mm256_broadcast_ss(&m1[3][3])); // matrix multiply - __m256 x = _mm256_mul_ps(m00, _mm256_loadu_ps(&buf[1][i])); - __m256 y = _mm256_mul_ps(m10, _mm256_loadu_ps(&buf[1][i])); - __m256 z = _mm256_mul_ps(m20, _mm256_loadu_ps(&buf[1][i])); + __m256 w = _mm256_mul_ps(m00, _mm256_loadu_ps(&buf[0][i])); - x = _mm256_fmadd_ps(m01, _mm256_loadu_ps(&buf[2][i]), x); - y = _mm256_fmadd_ps(m11, _mm256_loadu_ps(&buf[2][i]), y); - z = _mm256_fmadd_ps(m21, _mm256_loadu_ps(&buf[2][i]), z); + __m256 x = _mm256_mul_ps(m11, _mm256_loadu_ps(&buf[1][i])); + __m256 y = _mm256_mul_ps(m21, _mm256_loadu_ps(&buf[1][i])); + __m256 z = _mm256_mul_ps(m31, _mm256_loadu_ps(&buf[1][i])); - x = _mm256_fmadd_ps(m02, _mm256_loadu_ps(&buf[3][i]), x); - y = _mm256_fmadd_ps(m12, _mm256_loadu_ps(&buf[3][i]), y); - z = _mm256_fmadd_ps(m22, _mm256_loadu_ps(&buf[3][i]), z); + x = _mm256_fmadd_ps(m12, _mm256_loadu_ps(&buf[2][i]), x); + y = _mm256_fmadd_ps(m22, _mm256_loadu_ps(&buf[2][i]), y); + z = _mm256_fmadd_ps(m32, _mm256_loadu_ps(&buf[2][i]), z); + x = _mm256_fmadd_ps(m13, _mm256_loadu_ps(&buf[3][i]), x); + y = _mm256_fmadd_ps(m23, _mm256_loadu_ps(&buf[3][i]), y); + z = _mm256_fmadd_ps(m33, _mm256_loadu_ps(&buf[3][i]), z); + + _mm256_storeu_ps(&buf[0][i], w); _mm256_storeu_ps(&buf[1][i], x); _mm256_storeu_ps(&buf[2][i], y); _mm256_storeu_ps(&buf[3][i], z); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b6c5c6d235..204ed79660 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -509,6 +509,26 @@ void Avatar::relayJointDataToChildren() { _reconstructSoftEntitiesJointMap = false; } +/**jsdoc + * An avatar has different types of data simulated at different rates, in Hz. + * + * + * + * + * + * + * + * + * + * + * + * + *
Rate NameDescription
"avatar" or ""The rate at which the avatar is updated even if not in view.
"avatarInView"The rate at which the avatar is updated if in view.
"skeletonModel"The rate at which the skeleton model is being updated, even if there are no + * joint data available.
"jointData"The rate at which joint data are being updated.
""When no rate name is specified, the "avatar" update rate is + * provided.
+ * + * @typedef {string} AvatarSimulationRate + */ float Avatar::getSimulationRate(const QString& rateName) const { if (rateName == "") { return _simulationRate.rate(); @@ -1449,6 +1469,37 @@ QStringList Avatar::getJointNames() const { return result; } +std::vector Avatar::getSkeletonDefaultData() { + std::vector defaultSkeletonData; + if (_skeletonModel->isLoaded()) { + auto& model = _skeletonModel->getHFMModel(); + auto& rig = _skeletonModel->getRig(); + float geometryToRigScale = extractScale(rig.getGeometryToRigTransform())[0]; + QStringList jointNames = getJointNames(); + int sizeCount = 0; + for (int i = 0; i < jointNames.size(); i++) { + AvatarSkeletonTrait::UnpackedJointData jointData; + jointData.jointIndex = i; + jointData.parentIndex = rig.getJointParentIndex(i); + if (jointData.parentIndex == -1) { + jointData.boneType = model.joints[i].isSkeletonJoint ? AvatarSkeletonTrait::BoneType::SkeletonRoot : AvatarSkeletonTrait::BoneType::NonSkeletonRoot; + } else { + jointData.boneType = model.joints[i].isSkeletonJoint ? AvatarSkeletonTrait::BoneType::SkeletonChild : AvatarSkeletonTrait::BoneType::NonSkeletonChild; + } + jointData.defaultRotation = rig.getAbsoluteDefaultPose(i).rot(); + jointData.defaultTranslation = getDefaultJointTranslation(i); + float jointLocalScale = extractScale(model.joints[i].transform)[0]; + jointData.defaultScale = jointLocalScale / geometryToRigScale; + jointData.jointName = jointNames[i]; + jointData.stringLength = jointNames[i].size(); + jointData.stringStart = sizeCount; + sizeCount += jointNames[i].size(); + defaultSkeletonData.push_back(jointData); + } + } + return defaultSkeletonData; +} + glm::vec3 Avatar::getJointPosition(int index) const { glm::vec3 position; _skeletonModel->getJointPositionInWorldFrame(index, position); @@ -1515,6 +1566,8 @@ void Avatar::rigReady() { buildSpine2SplineRatioCache(); computeMultiSphereShapes(); buildSpine2SplineRatioCache(); + setSkeletonData(getSkeletonDefaultData()); + sendSkeletonData(); } // rig has been reset. diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index aef5ac09e9..a196c018d2 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -199,6 +199,8 @@ public: virtual int getJointIndex(const QString& name) const override; virtual QStringList getJointNames() const override; + std::vector getSkeletonDefaultData(); + /**jsdoc * Gets the default rotation of a joint (in the current avatar) relative to its parent. *

For information on the joint hierarchy used, see @@ -501,8 +503,8 @@ public: /**jsdoc * @function MyAvatar.getSimulationRate - * @param {string} [rateName=""] - Rate name. - * @returns {number} Simulation rate. + * @param {AvatarSimulationRate} [rateName=""] - Rate name. + * @returns {number} Simulation rate in Hz. * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE float getSimulationRate(const QString& rateName = QString("")) const; diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index fbcf36a8c9..295a0e9f52 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -270,28 +270,19 @@ bool SkeletonModel::getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& getJointPosition(_rig.indexOfJoint("RightEye"), secondEyePosition)) { return true; } - // no eye joints; try to estimate based on head/neck joints - glm::vec3 neckPosition, headPosition; - if (getJointPosition(_rig.indexOfJoint("Neck"), neckPosition) && - getJointPosition(_rig.indexOfJoint("Head"), headPosition)) { - const float EYE_PROPORTION = 0.6f; - glm::vec3 baseEyePosition = glm::mix(neckPosition, headPosition, EYE_PROPORTION); + + int headJointIndex = _rig.indexOfJoint("Head"); + glm::vec3 headPosition; + if (getJointPosition(headJointIndex, headPosition)) { + + // get head joint rotation. glm::quat headRotation; - getJointRotation(_rig.indexOfJoint("Head"), headRotation); - const float EYES_FORWARD = 0.25f; - const float EYE_SEPARATION = 0.1f; - float headHeight = glm::distance(neckPosition, headPosition); - firstEyePosition = baseEyePosition + headRotation * glm::vec3(EYE_SEPARATION, 0.0f, EYES_FORWARD) * headHeight; - secondEyePosition = baseEyePosition + headRotation * glm::vec3(-EYE_SEPARATION, 0.0f, EYES_FORWARD) * headHeight; - return true; - } else if (getJointPosition(_rig.indexOfJoint("Head"), headPosition)) { - glm::vec3 baseEyePosition = headPosition; - glm::quat headRotation; - getJointRotation(_rig.indexOfJoint("Head"), headRotation); - const float EYES_FORWARD_HEAD_ONLY = 0.30f; - const float EYE_SEPARATION = 0.1f; - firstEyePosition = baseEyePosition + headRotation * glm::vec3(EYE_SEPARATION, 0.0f, EYES_FORWARD_HEAD_ONLY); - secondEyePosition = baseEyePosition + headRotation * glm::vec3(-EYE_SEPARATION, 0.0f, EYES_FORWARD_HEAD_ONLY); + getJointRotation(headJointIndex, headRotation); + + float heightRatio = _rig.getUnscaledEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT; + glm::vec3 ipdOffset = glm::vec3(DEFAULT_AVATAR_IPD / 2.0f, 0.0f, 0.0f); + firstEyePosition = headPosition + headRotation * heightRatio * (DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET + ipdOffset); + secondEyePosition = headPosition + headRotation * heightRatio * (DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET - ipdOffset); return true; } return false; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index a2b0b808ba..aea214efd7 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -55,7 +55,7 @@ using namespace std; const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData"; static const int TRANSLATION_COMPRESSION_RADIX = 14; -static const int FAUX_JOINT_COMPRESSION_RADIX = 12; +static const int HAND_CONTROLLER_COMPRESSION_RADIX = 12; static const int SENSOR_TO_WORLD_SCALE_RADIX = 10; static const float AUDIO_LOUDNESS_SCALE = 1024.0f; static const float DEFAULT_AVATAR_DENSITY = 1000.0f; // density of water @@ -66,7 +66,7 @@ size_t AvatarDataPacket::maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients return FACE_TRACKER_INFO_SIZE + numBlendshapeCoefficients * sizeof(float); } -size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) { +size_t AvatarDataPacket::maxJointDataSize(size_t numJoints) { const size_t validityBitsSize = calcBitVectorSize((int)numJoints); size_t totalSize = sizeof(uint8_t); // numJoints @@ -76,14 +76,6 @@ size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) totalSize += validityBitsSize; // Translations mask totalSize += sizeof(float); // maxTranslationDimension totalSize += numJoints * sizeof(SixByteTrans); // Translations - - size_t NUM_FAUX_JOINT = 2; - totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints - - if (hasGrabJoints) { - totalSize += sizeof(AvatarDataPacket::FarGrabJoints); - } - return totalSize; } @@ -98,9 +90,6 @@ size_t AvatarDataPacket::minJointDataSize(size_t numJoints) { totalSize += sizeof(float); // maxTranslationDimension // assume no valid translations - size_t NUM_FAUX_JOINT = 2; - totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints - return totalSize; } @@ -329,6 +318,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // separately bool hasParentInfo = false; bool hasAvatarLocalPosition = false; + bool hasHandControllers = false; bool hasFaceTrackerInfo = false; @@ -346,7 +336,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent hasAvatarLocalPosition = hasParent() && (sendAll || tranlationChangedSince(lastSentTime) || parentInfoChangedSince(lastSentTime)); - + hasHandControllers = _controllerLeftHandMatrixCache.isValid() || _controllerRightHandMatrixCache.isValid(); hasFaceTrackerInfo = !dropFaceTracking && (hasFaceTracker() || getHasScriptedBlendshapes()) && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); hasJointData = !sendMinimum; @@ -364,6 +354,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent | (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0) | (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0) | (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0) + | (hasHandControllers ? AvatarDataPacket::PACKET_HAS_HAND_CONTROLLERS : 0) | (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0) | (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0) | (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0) @@ -406,7 +397,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + NUM_BYTES_RFC4122_UUID + AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) + - AvatarDataPacket::maxJointDataSize(_jointData.size(), true) + + AvatarDataPacket::maxJointDataSize(_jointData.size()) + AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size()); if (maxDataSize == 0) { @@ -592,7 +583,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } - IF_AVATAR_SPACE(PACKET_HAS_AVATAR_LOCAL_POSITION, sizeof(getLocalPosition()) ) { + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_LOCAL_POSITION, AvatarDataPacket::AVATAR_LOCAL_POSITION_SIZE) { auto startSection = destinationBuffer; const auto localPosition = getLocalPosition(); AVATAR_MEMCPY(localPosition); @@ -603,6 +594,23 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } + IF_AVATAR_SPACE(PACKET_HAS_HAND_CONTROLLERS, AvatarDataPacket::HAND_CONTROLLERS_SIZE) { + auto startSection = destinationBuffer; + + Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix()); + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerLeftHandTransform.getRotation()); + destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerLeftHandTransform.getTranslation(), HAND_CONTROLLER_COMPRESSION_RADIX); + + Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix()); + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerRightHandTransform.getRotation()); + destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(), HAND_CONTROLLER_COMPRESSION_RADIX); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->handControllersRate.increment(numBytes); + } + } + const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients(); // If it is connected, pack up the data IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, sizeof(AvatarDataPacket::FaceTrackerInfo) + (size_t)blendshapeCoefficients.size() * sizeof(float)) { @@ -638,9 +646,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // include jointData if there is room for the most minimal section. i.e. no translations or rotations. IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) { // Minimum space required for another rotation joint - - // size of joint + following translation bit-vector + translation scale + faux joints: - const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize + - sizeof(float) + AvatarDataPacket::FAUX_JOINTS_SIZE; + // size of joint + following translation bit-vector + translation scale: + const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize + sizeof(float); auto startSection = destinationBuffer; @@ -759,17 +766,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } sendStatus.translationsSent = i; - // faux joints - Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix()); - destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerLeftHandTransform.getRotation()); - destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerLeftHandTransform.getTranslation(), - FAUX_JOINT_COMPRESSION_RADIX); - - Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix()); - destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerRightHandTransform.getRotation()); - destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(), - FAUX_JOINT_COMPRESSION_RADIX); - IF_AVATAR_SPACE(PACKET_HAS_GRAB_JOINTS, sizeof (AvatarDataPacket::FarGrabJoints)) { // the far-grab joints may range further than 3 meters, so we can't use packFloatVec3ToSignedTwoByteFixed etc auto startSection = destinationBuffer; @@ -902,12 +898,12 @@ bool AvatarData::shouldLogError(const quint64& now) { } -const unsigned char* unpackFauxJoint(const unsigned char* sourceBuffer, ThreadSafeValueCache& matrixCache) { +const unsigned char* unpackHandController(const unsigned char* sourceBuffer, ThreadSafeValueCache& matrixCache) { glm::quat orientation; glm::vec3 position; Transform transform; sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, orientation); - sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, position, FAUX_JOINT_COMPRESSION_RADIX); + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, position, HAND_CONTROLLER_COMPRESSION_RADIX); transform.setTranslation(position); transform.setRotation(orientation); matrixCache.set(transform.getMatrix()); @@ -952,6 +948,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { bool hasAdditionalFlags = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS); bool hasParentInfo = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_PARENT_INFO); bool hasAvatarLocalPosition = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION); + bool hasHandControllers = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_HAND_CONTROLLERS); bool hasFaceTrackerInfo = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO); bool hasJointData = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_JOINT_DATA); bool hasJointDefaultPoseFlags = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS); @@ -1240,6 +1237,20 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _localPositionUpdateRate.increment(); } + if (hasHandControllers) { + auto startSection = sourceBuffer; + + sourceBuffer = unpackHandController(sourceBuffer, _controllerLeftHandMatrixCache); + sourceBuffer = unpackHandController(sourceBuffer, _controllerRightHandMatrixCache); + + int numBytesRead = sourceBuffer - startSection; + _handControllersRate.increment(numBytesRead); + _handControllersUpdateRate.increment(); + } else { + _controllerLeftHandMatrixCache.invalidate(); + _controllerRightHandMatrixCache.invalidate(); + } + if (hasFaceTrackerInfo) { auto startSection = sourceBuffer; @@ -1351,10 +1362,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { << "size:" << (int)(sourceBuffer - startPosition); } #endif - // faux joints - sourceBuffer = unpackFauxJoint(sourceBuffer, _controllerLeftHandMatrixCache); - sourceBuffer = unpackFauxJoint(sourceBuffer, _controllerRightHandMatrixCache); - int numBytesRead = sourceBuffer - startSection; _jointDataRate.increment(numBytesRead); _jointDataUpdateRate.increment(); @@ -1445,6 +1452,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { * * "globalPosition"Incoming global position. * "localPosition"Incoming local position. + * "handControllers"Incoming hand controllers. * "avatarBoundingBox"Incoming avatar bounding box. * "avatarOrientation"Incoming avatar orientation. * "avatarScale"Incoming avatar scale. @@ -1483,6 +1491,8 @@ float AvatarData::getDataRate(const QString& rateName) const { return _globalPositionRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "localPosition") { return _localPositionRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "handControllers") { + return _handControllersRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "avatarBoundingBox") { return _avatarBoundingBoxRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "avatarOrientation") { @@ -1545,9 +1555,9 @@ float AvatarData::getDataRate(const QString& rateName) const { * Rate NameDescription * * - * "globalPosition"Global position. * "localPosition"Local position. + * "handControllers"Hand controller positions and orientations. * "avatarBoundingBox"Avatar bounding box. * "avatarOrientation"Avatar orientation. * "avatarScale"Avatar scale. @@ -1559,7 +1569,6 @@ float AvatarData::getDataRate(const QString& rateName) const { * "faceTracker"Face tracker data. * "jointData"Joint data. * "farGrabJointData"Far grab joint data. - * ""When no rate name is specified, the overall update rate is provided. * * @@ -1573,6 +1582,8 @@ float AvatarData::getUpdateRate(const QString& rateName) const { return _globalPositionUpdateRate.rate(); } else if (rateName == "localPosition") { return _localPositionUpdateRate.rate(); + } else if (rateName == "handControllers") { + return _handControllersUpdateRate.rate(); } else if (rateName == "avatarBoundingBox") { return _avatarBoundingBoxUpdateRate.rate(); } else if (rateName == "avatarOrientation") { @@ -1635,6 +1646,13 @@ void AvatarData::setJointData(int index, const glm::quat& rotation, const glm::v data.translationIsDefaultPose = false; } +QVector AvatarData::getJointData() const { + QVector jointData; + QReadLocker readLock(&_jointDataLock); + jointData = _jointData; + return jointData; +} + void AvatarData::clearJointData(int index) { if (index < 0 || index >= LOWEST_PSEUDO_JOINT_INDEX) { return; @@ -1721,7 +1739,6 @@ glm::vec3 AvatarData::getJointTranslation(const QString& name) const { // on another thread in between the call to getJointIndex and getJointTranslation // return getJointTranslation(getJointIndex(name)); return readLockWithNamedJointIndex(name, [this](int index) { - return _jointData.at(index).translation; return getJointTranslation(index); }); } @@ -1809,8 +1826,8 @@ glm::quat AvatarData::getJointRotation(const QString& name) const { // Can't do this, not thread safe // return getJointRotation(getJointIndex(name)); - return readLockWithNamedJointIndex(name, [&](int index) { - return _jointData.at(index).rotation; + return readLockWithNamedJointIndex(name, [this](int index) { + return getJointRotation(index); }); } @@ -1990,11 +2007,98 @@ QUrl AvatarData::getWireSafeSkeletonModelURL() const { return QUrl(); } } +QByteArray AvatarData::packSkeletonData() const { + // Send an avatar trait packet with the skeleton data before the mesh is loaded + int avatarDataSize = 0; + QByteArray avatarDataByteArray; + _avatarSkeletonDataLock.withReadLock([&] { + // Add header + AvatarSkeletonTrait::Header header; + header.maxScaleDimension = 0.0f; + header.maxTranslationDimension = 0.0f; + header.numJoints = (uint8_t)_avatarSkeletonData.size(); + header.stringTableLength = 0; + + for (size_t i = 0; i < _avatarSkeletonData.size(); i++) { + header.stringTableLength += (uint16_t)_avatarSkeletonData[i].jointName.size(); + auto& translation = _avatarSkeletonData[i].defaultTranslation; + header.maxTranslationDimension = std::max(header.maxTranslationDimension, std::max(std::max(translation.x, translation.y), translation.z)); + header.maxScaleDimension = std::max(header.maxScaleDimension, _avatarSkeletonData[i].defaultScale); + } + + const int byteArraySize = (int)sizeof(AvatarSkeletonTrait::Header) + (int)(header.numJoints * sizeof(AvatarSkeletonTrait::JointData)) + header.stringTableLength; + avatarDataByteArray = QByteArray(byteArraySize, 0); + unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); + const unsigned char* const startPosition = destinationBuffer; + + memcpy(destinationBuffer, &header, sizeof(header)); + destinationBuffer += sizeof(AvatarSkeletonTrait::Header); + + QString stringTable = ""; + for (size_t i = 0; i < _avatarSkeletonData.size(); i++) { + AvatarSkeletonTrait::JointData jdata; + jdata.boneType = _avatarSkeletonData[i].boneType; + jdata.parentIndex = _avatarSkeletonData[i].parentIndex; + packFloatRatioToTwoByte((uint8_t*)(&jdata.defaultScale), _avatarSkeletonData[i].defaultScale / header.maxScaleDimension); + packOrientationQuatToSixBytes(jdata.defaultRotation, _avatarSkeletonData[i].defaultRotation); + packFloatVec3ToSignedTwoByteFixed(jdata.defaultTranslation, _avatarSkeletonData[i].defaultTranslation / header.maxTranslationDimension, TRANSLATION_COMPRESSION_RADIX); + jdata.jointIndex = (uint16_t)i; + jdata.stringStart = (uint16_t)_avatarSkeletonData[i].stringStart; + jdata.stringLength = (uint8_t)_avatarSkeletonData[i].stringLength; + stringTable += _avatarSkeletonData[i].jointName; + memcpy(destinationBuffer, &jdata, sizeof(AvatarSkeletonTrait::JointData)); + destinationBuffer += sizeof(AvatarSkeletonTrait::JointData); + } + + memcpy(destinationBuffer, stringTable.toUtf8(), header.stringTableLength); + destinationBuffer += header.stringTableLength; + + avatarDataSize = destinationBuffer - startPosition; + }); + return avatarDataByteArray.left(avatarDataSize); +} QByteArray AvatarData::packSkeletonModelURL() const { return getWireSafeSkeletonModelURL().toEncoded(); } +void AvatarData::unpackSkeletonData(const QByteArray& data) { + + const unsigned char* startPosition = reinterpret_cast(data.data()); + const unsigned char* sourceBuffer = startPosition; + + auto header = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(const AvatarSkeletonTrait::Header); + + std::vector joints; + for (uint8_t i = 0; i < header->numJoints; i++) { + auto jointData = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(const AvatarSkeletonTrait::JointData); + AvatarSkeletonTrait::UnpackedJointData uJointData; + uJointData.boneType = (int)jointData->boneType; + uJointData.jointIndex = (int)i; + uJointData.stringLength = (int)jointData->stringLength; + uJointData.stringStart = (int)jointData->stringStart; + uJointData.parentIndex = ((uJointData.boneType == AvatarSkeletonTrait::BoneType::SkeletonRoot) || + (uJointData.boneType == AvatarSkeletonTrait::BoneType::NonSkeletonRoot)) ? -1 : (int)jointData->parentIndex; + unpackOrientationQuatFromSixBytes(reinterpret_cast(&jointData->defaultRotation), uJointData.defaultRotation); + unpackFloatVec3FromSignedTwoByteFixed(reinterpret_cast(&jointData->defaultTranslation), uJointData.defaultTranslation, TRANSLATION_COMPRESSION_RADIX); + unpackFloatRatioFromTwoByte(reinterpret_cast(&jointData->defaultScale), uJointData.defaultScale); + uJointData.defaultTranslation *= header->maxTranslationDimension; + uJointData.defaultScale *= header->maxScaleDimension; + joints.push_back(uJointData); + } + QString table = QString::fromUtf8(reinterpret_cast(sourceBuffer), (int)header->stringTableLength); + for (size_t i = 0; i < joints.size(); i++) { + QStringRef subString(&table, joints[i].stringStart, joints[i].stringLength); + joints[i].jointName = subString.toString(); + } + if (_clientTraitsHandler) { + _clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonData); + } + setSkeletonData(joints); +} + void AvatarData::unpackSkeletonModelURL(const QByteArray& data) { auto skeletonModelURL = QUrl::fromEncoded(data); setSkeletonModelURL(skeletonModelURL); @@ -2030,6 +2134,8 @@ QByteArray AvatarData::packTrait(AvatarTraits::TraitType traitType) const { // Call packer function if (traitType == AvatarTraits::SkeletonModelURL) { traitBinaryData = packSkeletonModelURL(); + } else if (traitType == AvatarTraits::SkeletonData) { + traitBinaryData = packSkeletonData(); } return traitBinaryData; @@ -2051,6 +2157,8 @@ QByteArray AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, Avat void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) { if (traitType == AvatarTraits::SkeletonModelURL) { unpackSkeletonModelURL(traitBinaryData); + } else if (traitType == AvatarTraits::SkeletonData) { + unpackSkeletonData(traitBinaryData); } } @@ -2113,7 +2221,6 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { } _skeletonModelURL = expanded; - if (_clientTraitsHandler) { _clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonModelURL); } @@ -2905,6 +3012,20 @@ glm::mat4 AvatarData::getControllerRightHandMatrix() const { return _controllerRightHandMatrixCache.get(); } +/**jsdoc + * Information about a ray-to-avatar intersection. + * @typedef {object} RayToAvatarIntersectionResult + * @property {boolean} intersects - true if an avatar is intersected, false if it isn't. + * @property {string} avatarID - The ID of the avatar that is intersected. + * @property {number} distance - The distance from the ray origin to the intersection. + * @property {string} face - The name of the box face that is intersected; "UNKNOWN_FACE" if mesh was picked + * against. + * @property {Vec3} intersection - The ray intersection point in world coordinates. + * @property {Vec3} surfaceNormal - The surface normal at the intersection point. + * @property {number} jointIndex - The index of the joint intersected. + * @property {SubmeshIntersection} extraInfo - Extra information on the mesh intersected if mesh was picked against, + * {} if it wasn't. + */ QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, const RayToAvatarIntersectionResult& value) { QScriptValue obj = engine->newObject(); obj.setProperty("intersects", value.intersects); @@ -2997,6 +3118,26 @@ AABox AvatarData::computeBubbleBox(float bubbleScale) const { return box; } +void AvatarData::setSkeletonData(const std::vector& skeletonData) { + _avatarSkeletonDataLock.withWriteLock([&] { + _avatarSkeletonData = skeletonData; + }); +} + +std::vector AvatarData::getSkeletonData() const { + std::vector skeletonData; + _avatarSkeletonDataLock.withReadLock([&] { + skeletonData = _avatarSkeletonData; + }); + return skeletonData; +} + +void AvatarData::sendSkeletonData() const{ + if (_clientTraitsHandler) { + _clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonData); + } +} + AABox AvatarData::getDefaultBubbleBox() const { AABox bubbleBox(_defaultBubbleBox); bubbleBox.translate(_globalPosition); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 1c4b0cfc53..76fa9e0a34 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -145,6 +145,45 @@ const char AVATARDATA_FLAGS_MINIMUM = 0; using SmallFloat = uint16_t; // a compressed float with less precision, user defined radix +namespace AvatarSkeletonTrait { + enum BoneType { + SkeletonRoot = 0, + SkeletonChild, + NonSkeletonRoot, + NonSkeletonChild + }; + + PACKED_BEGIN struct Header { + float maxTranslationDimension; + float maxScaleDimension; + uint8_t numJoints; + uint16_t stringTableLength; + } PACKED_END; + + PACKED_BEGIN struct JointData { + uint16_t stringStart; + uint8_t stringLength; + uint8_t boneType; + uint8_t defaultTranslation[6]; + uint8_t defaultRotation[6]; + uint16_t defaultScale; + uint16_t jointIndex; + uint16_t parentIndex; + } PACKED_END; + + struct UnpackedJointData { + int stringStart; + int stringLength; + int boneType; + glm::vec3 defaultTranslation; + glm::quat defaultRotation; + float defaultScale; + int jointIndex; + int parentIndex; + QString jointName; + }; +} + namespace AvatarDataPacket { // NOTE: every time AvatarData is sent from mixer to client, it also includes the GUIID for the session @@ -164,10 +203,11 @@ namespace AvatarDataPacket { const HasFlags PACKET_HAS_ADDITIONAL_FLAGS = 1U << 7; const HasFlags PACKET_HAS_PARENT_INFO = 1U << 8; const HasFlags PACKET_HAS_AVATAR_LOCAL_POSITION = 1U << 9; - const HasFlags PACKET_HAS_FACE_TRACKER_INFO = 1U << 10; - const HasFlags PACKET_HAS_JOINT_DATA = 1U << 11; - const HasFlags PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS = 1U << 12; - const HasFlags PACKET_HAS_GRAB_JOINTS = 1U << 13; + const HasFlags PACKET_HAS_HAND_CONTROLLERS = 1U << 10; + const HasFlags PACKET_HAS_FACE_TRACKER_INFO = 1U << 11; + const HasFlags PACKET_HAS_JOINT_DATA = 1U << 12; + const HasFlags PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS = 1U << 13; + const HasFlags PACKET_HAS_GRAB_JOINTS = 1U << 14; const size_t AVATAR_HAS_FLAGS_SIZE = 2; using SixByteQuat = uint8_t[6]; @@ -230,7 +270,7 @@ namespace AvatarDataPacket { // // POTENTIAL SAVINGS - 20 bytes - SixByteQuat sensorToWorldQuat; // 6 byte compressed quaternion part of sensor to world matrix + SixByteQuat sensorToWorldQuat; // 6 byte compressed quaternion part of sensor to world matrix uint16_t sensorToWorldScale; // uniform scale of sensor to world matrix float sensorToWorldTrans[3]; // fourth column of sensor to world matrix // FIXME - sensorToWorldTrans might be able to be better compressed if it was @@ -258,6 +298,7 @@ namespace AvatarDataPacket { PACKED_BEGIN struct AvatarLocalPosition { float localPosition[3]; // parent frame translation of the avatar } PACKED_END; + const size_t AVATAR_LOCAL_POSITION_SIZE = 12; static_assert(sizeof(AvatarLocalPosition) == AVATAR_LOCAL_POSITION_SIZE, "AvatarDataPacket::AvatarLocalPosition size doesn't match."); @@ -273,6 +314,15 @@ namespace AvatarDataPacket { PARENT_INFO_SIZE + AVATAR_LOCAL_POSITION_SIZE; + PACKED_BEGIN struct HandControllers { + SixByteQuat leftHandRotation; + SixByteTrans leftHandTranslation; + SixByteQuat rightHandRotation; + SixByteTrans rightHandTranslation; + } PACKED_END; + static const size_t HAND_CONTROLLERS_SIZE = 24; + static_assert(sizeof(HandControllers) == HAND_CONTROLLERS_SIZE, "AvatarDataPacket::HandControllers size doesn't match."); + // variable length structure follows @@ -303,7 +353,7 @@ namespace AvatarDataPacket { SixByteTrans rightHandControllerTranslation; }; */ - size_t maxJointDataSize(size_t numJoints, bool hasGrabJoints); + size_t maxJointDataSize(size_t numJoints); size_t minJointDataSize(size_t numJoints); /* @@ -327,7 +377,6 @@ namespace AvatarDataPacket { static_assert(sizeof(FarGrabJoints) == FAR_GRAB_JOINTS_SIZE, "AvatarDataPacket::FarGrabJoints size doesn't match."); static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE; - static const size_t FAUX_JOINTS_SIZE = 2 * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); struct SendStatus { HasFlags itemFlags { 0 }; @@ -404,6 +453,7 @@ class AvatarDataRate { public: RateCounter<> globalPositionRate; RateCounter<> localPositionRate; + RateCounter<> handControllersRate; RateCounter<> avatarBoundingBoxRate; RateCounter<> avatarOrientationRate; RateCounter<> avatarScaleRate; @@ -467,8 +517,8 @@ class AvatarData : public QObject, public SpatiallyNestable { * @property {boolean} lookAtSnappingEnabled=true - true if the avatar's eyes snap to look at another avatar's * eyes when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. * @property {string} skeletonModelURL - The avatar's FST file. - * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments.
- * Deprecated: Use avatar entities instead. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments. + *

Deprecated: This property is deprecated and will be removed. Use avatar entities instead.

* @property {string[]} jointNames - The list of joints in the current avatar model. Read-only. * @property {Uuid} sessionUUID - Unique ID of the avatar in the domain. Read-only. * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the @@ -479,7 +529,8 @@ class AvatarData : public QObject, public SpatiallyNestable { * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. - * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. + * @property {boolean} hasPriority - true if the avatar is in a "hero" zone, false if it isn't. + * Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) @@ -1075,7 +1126,7 @@ public: * Gets information about the models currently attached to your avatar. * @function Avatar.getAttachmentsVariant * @returns {AttachmentData[]} Information about all models attached to your avatar. - * @deprecated Use avatar entities instead. + * @deprecated This function is deprecated and will be removed. Use avatar entities instead. */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual QVariantList getAttachmentsVariant() const; @@ -1086,7 +1137,7 @@ public: * update your avatar's attachments per the changed data. * @function Avatar.setAttachmentsVariant * @param {AttachmentData[]} variant - The attachment data defining the models to have attached to your avatar. - * @deprecated Use avatar entities instead. + * @deprecated This function is deprecated and will be removed. Use avatar entities instead. */ // FIXME: Can this name be improved? Can it be deprecated? Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant); @@ -1167,7 +1218,7 @@ public: * Gets information about the models currently attached to your avatar. * @function Avatar.getAttachmentData * @returns {AttachmentData[]} Information about all models attached to your avatar. - * @deprecated Use avatar entities instead. + * @deprecated This function is deprecated and will be removed. Use avatar entities instead. * @example Report the URLs of all current attachments. * var attachments = MyAvatar.getaAttachmentData(); * for (var i = 0; i < attachments.length; i++) { @@ -1185,7 +1236,7 @@ public: * @function Avatar.setAttachmentData * @param {AttachmentData[]} attachmentData - The attachment data defining the models to have attached to your avatar. Use * null to remove all attachments. - * @deprecated Use avatar entities instead. + * @deprecated This function is deprecated and will be removed. Use avatar entities instead. * @example Remove a hat attachment if your avatar is wearing it. * var hatURL = "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx"; * var attachments = MyAvatar.getAttachmentData(); @@ -1222,7 +1273,7 @@ public: * @param {boolean} [allowDuplicates=false] - If true then more than one copy of any particular model may be * attached to the same joint; if false then the same model cannot be attached to the same joint. * @param {boolean} [useSaved=true] - Not used. - * @deprecated Use avatar entities instead. + * @deprecated This function is deprecated and will be removed. Use avatar entities instead. * @example Attach a cowboy hat to your avatar's head. * var attachment = { * modelURL: "https://s3.amazonaws.com/hifi-public/tony/cowboy-hat.fbx", @@ -1253,7 +1304,7 @@ public: * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the most * recently attached model is removed from which ever joint it was attached to. - * @deprecated Use avatar entities instead. + * @deprecated This function is deprecated and will be removed. Use avatar entities instead. */ Q_INVOKABLE virtual void detachOne(const QString& modelURL, const QString& jointName = QString()); @@ -1263,7 +1314,7 @@ public: * @param {string} modelURL - The URL of the model to detach. * @param {string} [jointName=""] - The name of the joint to detach the model from. If "", then the model is * detached from all joints. - * @deprecated Use avatar entities instead. + * @deprecated This function is deprecated and will be removed. Use avatar entities instead. */ Q_INVOKABLE virtual void detachAll(const QString& modelURL, const QString& jointName = QString()); @@ -1419,6 +1470,10 @@ public: void setIsNewAvatar(bool isNewAvatar) { _isNewAvatar = isNewAvatar; } bool getIsNewAvatar() { return _isNewAvatar; } void setIsClientAvatar(bool isClientAvatar) { _isClientAvatar = isClientAvatar; } + void setSkeletonData(const std::vector& skeletonData); + std::vector getSkeletonData() const; + void sendSkeletonData() const; + QVector getJointData() const; signals: @@ -1597,12 +1652,13 @@ protected: bool hasParent() const { return !getParentID().isNull(); } bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; } + QByteArray packSkeletonData() const; QByteArray packSkeletonModelURL() const; QByteArray packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); QByteArray packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); void unpackSkeletonModelURL(const QByteArray& data); - + void unpackSkeletonData(const QByteArray& data); // isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master" // Audio Mixer that the replicated avatar is connected to. @@ -1670,6 +1726,7 @@ protected: RateCounter<> _parseBufferRate; RateCounter<> _globalPositionRate; RateCounter<> _localPositionRate; + RateCounter<> _handControllersRate; RateCounter<> _avatarBoundingBoxRate; RateCounter<> _avatarOrientationRate; RateCounter<> _avatarScaleRate; @@ -1687,6 +1744,7 @@ protected: RateCounter<> _parseBufferUpdateRate; RateCounter<> _globalPositionUpdateRate; RateCounter<> _localPositionUpdateRate; + RateCounter<> _handControllersUpdateRate; RateCounter<> _avatarBoundingBoxUpdateRate; RateCounter<> _avatarOrientationUpdateRate; RateCounter<> _avatarScaleUpdateRate; @@ -1719,6 +1777,9 @@ protected: AvatarGrabDataMap _avatarGrabData; bool _avatarGrabDataChanged { false }; // by network + mutable ReadWriteLockable _avatarSkeletonDataLock; + std::vector _avatarSkeletonData; + // used to transform any sensor into world space, including the _hmdSensorMat, or hand controllers. ThreadSafeValueCache _sensorToWorldMatrixCache { glm::mat4() }; ThreadSafeValueCache _controllerLeftHandMatrixCache { glm::mat4() }; @@ -1751,14 +1812,11 @@ protected: template T readLockWithNamedJointIndex(const QString& name, const T& defaultValue, F f) const { - int index = getFauxJointIndex(name); QReadLocker readLock(&_jointDataLock); - - // The first conditional is superfluous, but illustrative - if (index == -1 || index < _jointData.size()) { + int index = getJointIndex(name); + if (index == -1) { return defaultValue; } - return f(index); } @@ -1769,8 +1827,8 @@ protected: template void writeLockWithNamedJointIndex(const QString& name, F f) { - int index = getFauxJointIndex(name); QWriteLocker writeLock(&_jointDataLock); + int index = getJointIndex(name); if (index == -1) { return; } diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 3abd352778..29a40c5b6b 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -229,8 +229,9 @@ AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, AvatarSharedPointer AvatarHashMap::findAvatar(const QUuid& sessionUUID) const { QReadLocker locker(&_hashLock); - if (_avatarHash.contains(sessionUUID)) { - return _avatarHash.value(sessionUUID); + auto avatarIter = _avatarHash.find(sessionUUID); + if (avatarIter != _avatarHash.end()) { + return avatarIter.value(); } return nullptr; } @@ -331,6 +332,12 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer void AvatarHashMap::processBulkAvatarTraits(QSharedPointer message, SharedNodePointer sendingNode) { AvatarTraits::TraitMessageSequence seq; + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < (qint64)sizeof(AvatarTraits::TraitMessageSequence)) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + message->readPrimitive(&seq); auto traitsAckPacket = NLPacket::create(PacketType::BulkAvatarTraitsAck, sizeof(AvatarTraits::TraitMessageSequence), true); @@ -343,7 +350,14 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer mess nodeList->sendPacket(std::move(traitsAckPacket), *avatarMixer); } - while (message->getBytesLeftToRead()) { + while (message->getBytesLeftToRead() > 0) { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(NUM_BYTES_RFC4122_UUID + + sizeof(AvatarTraits::TraitType))) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + // read the avatar ID to figure out which avatar this is for auto avatarID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); @@ -359,6 +373,12 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer mess auto& lastProcessedVersions = _processedTraitVersions[avatarID]; while (traitType != AvatarTraits::NullTrait && message->getBytesLeftToRead() > 0) { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitVersion))) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + AvatarTraits::TraitVersion packetTraitVersion; message->readPrimitive(&packetTraitVersion); @@ -366,8 +386,20 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer mess bool skipBinaryTrait = false; if (AvatarTraits::isSimpleTrait(traitType)) { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitWireSize))) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + message->readPrimitive(&traitBinarySize); + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < traitBinarySize) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + // check if this trait version is newer than what we already have for this avatar if (packetTraitVersion > lastProcessedVersions[traitType]) { auto traitData = message->read(traitBinarySize); @@ -378,11 +410,24 @@ void AvatarHashMap::processBulkAvatarTraits(QSharedPointer mess skipBinaryTrait = true; } } else { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(NUM_BYTES_RFC4122_UUID + + sizeof(AvatarTraits::TraitWireSize))) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + AvatarTraits::TraitInstanceID traitInstanceID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); message->readPrimitive(&traitBinarySize); + // Trying to read more bytes than available, bail + if (traitBinarySize < -1 || message->getBytesLeftToRead() < traitBinarySize) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + auto& processedInstanceVersion = lastProcessedVersions.getInstanceValueRef(traitType, traitInstanceID); if (packetTraitVersion > processedInstanceVersion) { if (traitBinarySize == AvatarTraits::DELETED_TRAIT_SIZE) { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 8395651d6b..17a3d28eb0 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -36,8 +36,10 @@ const int CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 50; const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND; /**jsdoc - * Note: An AvatarList API is also provided for Interface and client entity scripts: it is a - * synonym for the {@link AvatarManager} API. + * The AvatarList API provides information about avatars within the current domain. + * + *

Warning: An API named "AvatarList" is also provided for Interface, client entity, and avatar + * scripts, however, it is a synonym for the {@link AvatarManager} API.

* * @namespace AvatarList * @@ -78,23 +80,37 @@ public: // Currently, your own avatar will be included as the null avatar id. /**jsdoc + * Gets the IDs of all avatars in the domain. + *

Warning: If the AC script is acting as an avatar (i.e., Agent.isAvatar == true) the + * avatar's ID is NOT included in results.

* @function AvatarList.getAvatarIdentifiers - * @returns {Uuid[]} + * @returns {Uuid[]} The IDs of all avatars in the domain (excluding AC script's avatar). + * @example Report the IDS of all avatars within the domain. + * var avatars = AvatarList.getAvatarIdentifiers(); + * print("Avatars in the domain: " + JSON.stringify(avatars)); */ Q_INVOKABLE QVector getAvatarIdentifiers(); /**jsdoc + * Gets the IDs of all avatars within a specified distance from a point. + *

Warning: If the AC script is acting as an avatar (i.e., Agent.isAvatar == true) the + * avatar's ID is NOT included in results.

* @function AvatarList.getAvatarsInRange - * @param {Vec3} position - * @param {number} range - * @returns {Uuid[]} + * @param {Vec3} position - The point about which the search is performed. + * @param {number} range - The search radius. + * @returns {Uuid[]} The IDs of all avatars within the search distance from the position (excluding AC script's avatar). + * @example Report the IDs of all avatars within 10m of the origin. + * var RANGE = 10; + * var avatars = AvatarList.getAvatarsInRange(Vec3.ZERO, RANGE); + * print("Avatars near the origin: " + JSON.stringify(avatars)); */ Q_INVOKABLE QVector getAvatarsInRange(const glm::vec3& position, float rangeMeters) const; /**jsdoc + * Gets information about an avatar. * @function AvatarList.getAvatar - * @param {Uuid} avatarID - * @returns {AvatarData} + * @param {Uuid} avatarID - The ID of the avatar. + * @returns {AvatarData} Information about the avatar. */ // Null/Default-constructed QUuids will return MyAvatar Q_INVOKABLE virtual ScriptAvatarData* getAvatar(QUuid avatarID) { return new ScriptAvatarData(getAvatarBySessionID(avatarID)); } @@ -110,34 +126,57 @@ public: signals: /**jsdoc + * Triggered when an avatar arrives in the domain. * @function AvatarList.avatarAddedEvent - * @param {Uuid} sessionUUID + * @param {Uuid} sessionUUID - The ID of the avatar that arrived in the domain. * @returns {Signal} + * @example Report when an avatar arrives in the domain. + * AvatarManager.avatarAddedEvent.connect(function (sessionID) { + * print("Avatar arrived: " + sessionID); + * }); + * + * // Note: If using from the AvatarList API, replace "AvatarManager" with "AvatarList". */ void avatarAddedEvent(const QUuid& sessionUUID); /**jsdoc + * Triggered when an avatar leaves the domain. * @function AvatarList.avatarRemovedEvent - * @param {Uuid} sessionUUID + * @param {Uuid} sessionUUID - The ID of the avatar that left the domain. * @returns {Signal} + * @example Report when an avatar leaves the domain. + * AvatarManager.avatarRemovedEvent.connect(function (sessionID) { + * print("Avatar left: " + sessionID); + * }); + * + * // Note: If using from the AvatarList API, replace "AvatarManager" with "AvatarList". */ void avatarRemovedEvent(const QUuid& sessionUUID); /**jsdoc + * Triggered when an avatar's session ID changes. * @function AvatarList.avatarSessionChangedEvent - * @param {Uuid} sessionUUID - * @param {Uuid} oldSessionUUID + * @param {Uuid} newSessionUUID - The new session ID. + * @param {Uuid} oldSessionUUID - The old session ID. * @returns {Signal} + * @example Report when an avatar's session ID changes. + * AvatarManager.avatarSessionChangedEvent.connect(function (newSessionID, oldSessionID) { + * print("Avatar session ID changed from " + oldSessionID + " to " + newSessionID); + * }); + * + * // Note: If using from the AvatarList API, replace "AvatarManager" with "AvatarList". */ void avatarSessionChangedEvent(const QUuid& sessionUUID,const QUuid& oldUUID); public slots: /**jsdoc + * Checks whether there is an avatar within a specified distance from a point. * @function AvatarList.isAvatarInRange - * @param {string} position - * @param {string} range - * @returns {boolean} + * @param {string} position - The test position. + * @param {string} range - The test distance. + * @returns {boolean} true if there's an avatar within the specified distance of the point, false + * if not. */ bool isAvatarInRange(const glm::vec3 & position, const float range); @@ -145,36 +184,41 @@ protected slots: /**jsdoc * @function AvatarList.sessionUUIDChanged - * @param {Uuid} sessionUUID - * @param {Uuid} oldSessionUUID + * @param {Uuid} sessionUUID - New session ID. + * @param {Uuid} oldSessionUUID - Old session ID. + * @deprecated This function is deprecated and will be removed. */ void sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID); /**jsdoc * @function AvatarList.processAvatarDataPacket - * @param {} message - * @param {} sendingNode + * @param {object} message - Message. + * @param {object} sendingNode - Sending node. + * @deprecated This function is deprecated and will be removed. */ void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); /**jsdoc * @function AvatarList.processAvatarIdentityPacket - * @param {} message - * @param {} sendingNode + * @param {object} message - Message. + * @param {object} sendingNode - Sending node. + * @deprecated This function is deprecated and will be removed. */ void processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode); /**jsdoc * @function AvatarList.processBulkAvatarTraits - * @param {} message - * @param {} sendingNode + * @param {object} message - Message. + * @param {object} sendingNode - Sending node. + * @deprecated This function is deprecated and will be removed. */ void processBulkAvatarTraits(QSharedPointer message, SharedNodePointer sendingNode); /**jsdoc * @function AvatarList.processKillAvatar - * @param {} message - * @param {} sendingNode + * @param {object} message - Message. + * @param {object} sendingNode - Sending node. + * @deprecated This function is deprecated and will be removed. */ void processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode); diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index 13d64ec225..007b0418a9 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -29,7 +29,7 @@ namespace AvatarTraits { // Simple traits SkeletonModelURL = 0, - + SkeletonData, // Instanced traits FirstInstancedTrait, AvatarEntity = FirstInstancedTrait, diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index f6bd66e89a..e133f178df 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -107,8 +107,7 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { if (initialSend || *simpleIt == Updated) { bytesWritten += AvatarTraits::packTrait(traitType, *traitsPacketList, *_owningAvatar); - - + if (traitType == AvatarTraits::SkeletonModelURL) { // keep track of our skeleton version in case we get an override back _currentSkeletonVersion = _currentTraitVersion; @@ -147,7 +146,16 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { void ClientTraitsHandler::processTraitOverride(QSharedPointer message, SharedNodePointer sendingNode) { if (sendingNode->getType() == NodeType::AvatarMixer) { Lock lock(_traitLock); - while (message->getBytesLeftToRead()) { + + while (message->getBytesLeftToRead() > 0) { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitType) + + sizeof(AvatarTraits::TraitVersion) + + sizeof(AvatarTraits::TraitWireSize))) { + qWarning() << "Malformed trait override packet, bailling"; + return; + } + AvatarTraits::TraitType traitType; message->readPrimitive(&traitType); @@ -157,6 +165,12 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer m AvatarTraits::TraitWireSize traitBinarySize; message->readPrimitive(&traitBinarySize); + // Trying to read more bytes than available, bail + if (traitBinarySize < -1 || message->getBytesLeftToRead() < traitBinarySize) { + qWarning() << "Malformed trait override packet, bailling"; + return; + } + // only accept an override if this is for a trait type we override // and the version matches what we last sent for skeleton if (traitType == AvatarTraits::SkeletonModelURL diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 01f7ff360a..9c5c2c6918 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -16,6 +16,53 @@ #include "AvatarData.h" +/**jsdoc + * Information about an avatar. + * @typedef {object} AvatarData + * @property {Vec3} position - The avatar's position. + * @property {number} scale - The target scale of the avatar without any restrictions on permissible values imposed by the + * domain. + * @property {Vec3} handPosition - A user-defined hand position, in world coordinates. The position moves with the avatar but + * is otherwise not used or changed by Interface. + * @property {number} bodyPitch - The pitch of the avatar's body, in degrees. + * @property {number} bodyYaw - The yaw of the avatar's body, in degrees. + * @property {number} bodyRoll - The roll of the avatar's body, in degrees. + * @property {Quat} orientation - The orientation of the avatar's body. + * @property {Quat} headOrientation - The orientation of the avatar's head. + * @property {number} headPitch - The pitch of the avatar's head relative to the body, in degrees. + * @property {number} headYaw - The yaw of the avatar's head relative to the body, in degrees. + * @property {number} headRoll - The roll of the avatar's head relative to the body, in degrees. + * + * @property {Vec3} velocity - The linear velocity of the avatar. + * @property {Vec3} angularVelocity - The angular velocity of the avatar. + * + * @property {Uuid} sessionUUID - The avatar's session ID. + * @property {string} displayName - The avatar's display name. + * @property {string} sessionDisplayName - The avatar's display name, sanitized and versioned, as defined by the avatar mixer. + * It is unique among all avatars present in the domain at the time. + * @property {boolean} isReplicated - Deprecated: This property is deprecated and will be + * removed. + * @property {boolean} lookAtSnappingEnabled - true if the avatar's eyes snap to look at another avatar's eyes + * when the other avatar is in the line of sight and also has lookAtSnappingEnabled == true. + * + * @property {string} skeletonModelURL - The avatar's FST file. + * @property {AttachmentData[]} attachmentData - Information on the avatar's attachments. + *

Deprecated: This property is deprecated and will be removed. Use avatar entities instead.

+ * @property {string[]} jointNames - The list of joints in the current avatar model. + * + * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the + * domain. + * @property {number} audioAverageLoudness - The rolling average loudness of the audio input that the avatar is injecting into + * the domain. + * + * @property {Mat4} sensorToWorldMatrix - The scale, rotation, and translation transform from the user's real world to the + * avatar's size, orientation, and position in the virtual world. + * @property {Mat4} controllerLeftHandMatrix - The rotation and translation of the left hand controller relative to the avatar. + * @property {Mat4} controllerRightHandMatrix - The rotation and translation of the right hand controller relative to the + * avatar. + * + * @property {boolean} hasPriority - true if the avatar is in a "hero" zone, false if it isn't. + */ class ScriptAvatarData : public QObject { Q_OBJECT diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 2189e7bdc3..eb02ac2241 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -31,11 +31,9 @@ #include #include "ModelBakingLoggingCategory.h" -#include "TextureBaker.h" -FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : - ModelBaker(inputModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { +FBXBaker::FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputModelURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { if (hasBeenBaked) { // Look for the original model file one directory higher. Perhaps this is an oven output directory. QUrl originalRelativePath = QUrl("../original/" + inputModelURL.fileName().replace(BAKED_FBX_EXTENSION, FBX_EXTENSION)); @@ -45,15 +43,6 @@ FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputText } void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { - _hfmModel = hfmModel; - - if (shouldStop()) { - return; - } - - // enumerate the models and textures found in the scene and start a bake for them - rewriteAndBakeSceneTextures(); - if (shouldStop()) { return; } @@ -114,15 +103,17 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const int meshIndex = 0; for (FBXNode& rootChild : _rootNode.children) { if (rootChild.name == "Objects") { - for (FBXNode& object : rootChild.children) { - if (object.name == "Geometry") { - if (object.properties.at(2) == "Mesh") { + auto object = rootChild.children.begin(); + while (object != rootChild.children.end()) { + if (object->name == "Geometry") { + if (object->properties.at(2) == "Mesh") { int meshNum = meshIndexToRuntimeOrder[meshIndex]; - replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); meshIndex++; } - } else if (object.name == "Model") { - for (FBXNode& modelChild : object.children) { + object++; + } else if (object->name == "Model") { + for (FBXNode& modelChild : object->children) { if (modelChild.name == "Properties60" || modelChild.name == "Properties70") { // This is a properties node // Remove the geometric transform because that has been applied directly to the vertices in FBXSerializer @@ -142,10 +133,37 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const } else if (modelChild.name == "Vertices") { // This model is also a mesh int meshNum = meshIndexToRuntimeOrder[meshIndex]; - replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); meshIndex++; } } + object++; + } else if (object->name == "Texture" || object->name == "Video") { + // this is an embedded texture, we need to remove it from the FBX + object = rootChild.children.erase(object); + } else if (object->name == "Material") { + for (FBXNode& materialChild : object->children) { + if (materialChild.name == "Properties60" || materialChild.name == "Properties70") { + // This is a properties node + // Remove the material texture scale because that is now included in the material JSON + // Texture nodes are removed, so their texture scale is effectively gone already + static const QVariant MAYA_UV_SCALE = hifi::ByteArray("Maya|uv_scale"); + static const QVariant MAYA_UV_OFFSET = hifi::ByteArray("Maya|uv_offset"); + for (int i = 0; i < materialChild.children.size(); i++) { + const auto& prop = materialChild.children[i]; + const auto& propertyName = prop.properties.at(0); + if (propertyName == MAYA_UV_SCALE || + propertyName == MAYA_UV_OFFSET) { + materialChild.children.removeAt(i); + --i; + } + } + } + } + + object++; + } else { + object++; } if (hasErrors()) { @@ -154,82 +172,4 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const } } } -} - -void FBXBaker::rewriteAndBakeSceneTextures() { - using namespace image::TextureUsage; - QHash textureTypes; - - // enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID - for (const auto& material : _hfmModel->materials) { - if (material.normalTexture.isBumpmap) { - textureTypes[material.normalTexture.id] = BUMP_TEXTURE; - } else { - textureTypes[material.normalTexture.id] = NORMAL_TEXTURE; - } - - textureTypes[material.albedoTexture.id] = ALBEDO_TEXTURE; - textureTypes[material.glossTexture.id] = GLOSS_TEXTURE; - textureTypes[material.roughnessTexture.id] = ROUGHNESS_TEXTURE; - textureTypes[material.specularTexture.id] = SPECULAR_TEXTURE; - textureTypes[material.metallicTexture.id] = METALLIC_TEXTURE; - textureTypes[material.emissiveTexture.id] = EMISSIVE_TEXTURE; - textureTypes[material.occlusionTexture.id] = OCCLUSION_TEXTURE; - textureTypes[material.lightmapTexture.id] = LIGHTMAP_TEXTURE; - } - - // enumerate the children of the root node - for (FBXNode& rootChild : _rootNode.children) { - - if (rootChild.name == "Objects") { - - // enumerate the objects - auto object = rootChild.children.begin(); - while (object != rootChild.children.end()) { - if (object->name == "Texture") { - - // double check that we didn't get an abort while baking the last texture - if (shouldStop()) { - return; - } - - // enumerate the texture children - for (FBXNode& textureChild : object->children) { - - if (textureChild.name == "RelativeFilename") { - QString hfmTextureFileName { textureChild.properties.at(0).toString() }; - - // grab the ID for this texture so we can figure out the - // texture type from the loaded materials - auto textureID { object->properties[0].toString() }; - auto textureType = textureTypes[textureID]; - - // Compress the texture information and return the new filename to be added into the FBX scene - auto bakedTextureFile = compressTexture(hfmTextureFileName, textureType); - - // If no errors or warnings have occurred during texture compression add the filename to the FBX scene - if (!bakedTextureFile.isNull()) { - textureChild.properties[0] = bakedTextureFile; - } else { - // if bake fails - return, if there were errors and continue, if there were warnings. - if (hasErrors()) { - return; - } else if (hasWarnings()) { - continue; - } - } - } - } - - ++object; - - } else if (object->name == "Video") { - // this is an embedded texture, we need to remove it from the FBX - object = rootChild.children.erase(object); - } else { - ++object; - } - } - } - } -} +} \ No newline at end of file diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 59ef5e349d..a528de512d 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -18,33 +18,23 @@ #include #include "Baker.h" -#include "TextureBaker.h" #include "ModelBaker.h" #include "ModelBakingLoggingCategory.h" -#include - #include -using TextureBakerThreadGetter = std::function; class FBXBaker : public ModelBaker { Q_OBJECT public: - FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); protected: virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: void rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists); - void rewriteAndBakeSceneTextures(); void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); - - hfm::Model::Pointer _hfmModel; - - bool _pendingErrorEmission { false }; }; #endif // hifi_FBXBaker_h diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index dd1ba55e54..e23b76f73a 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -27,21 +27,12 @@ std::function MaterialBaker::_getNextOvenWorkerThreadOperator; static int materialNum = 0; -namespace std { - template <> - struct hash { - size_t operator()(const graphics::Material::MapChannel& a) const { - return std::hash()((size_t)a); - } - }; -}; - -MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath) : +MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, QUrl destinationPath) : _materialData(materialData), _isURL(isURL), + _destinationPath(destinationPath), _bakedOutputDir(bakedOutputDir), - _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)), - _destinationPath(destinationPath) + _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)) { } @@ -64,6 +55,14 @@ void MaterialBaker::bake() { } } +void MaterialBaker::abort() { + Baker::abort(); + + for (auto& textureBaker : _textureBakers) { + textureBaker->abort(); + } +} + void MaterialBaker::loadMaterial() { if (!_isURL) { qCDebug(material_baking) << "Loading local material" << _materialData; @@ -104,52 +103,54 @@ void MaterialBaker::processMaterial() { for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { if (networkMaterial.second) { - auto textureMaps = networkMaterial.second->getTextureMaps(); - for (auto textureMap : textureMaps) { - if (textureMap.second && textureMap.second->getTextureSource()) { - graphics::Material::MapChannel mapChannel = textureMap.first; - auto texture = textureMap.second->getTextureSource(); + auto textures = networkMaterial.second->getTextures(); + for (auto texturePair : textures) { + auto mapChannel = texturePair.first; + auto textureMap = texturePair.second; + if (textureMap.texture && textureMap.texture->_textureSource) { + auto type = textureMap.texture->getTextureType(); - QUrl url = texture->getUrl(); - QString cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + QByteArray content; + QUrl textureURL; + { + bool foundEmbeddedTexture = false; + auto textureContentMapIter = _textureContentMap.find(networkMaterial.second->getName()); + if (textureContentMapIter != _textureContentMap.end()) { + auto textureUsageIter = textureContentMapIter->second.find(type); + if (textureUsageIter != textureContentMapIter->second.end()) { + content = textureUsageIter->second.first; + textureURL = textureUsageIter->second.second; + foundEmbeddedTexture = true; + } + } + if (!foundEmbeddedTexture && textureMap.texture->_textureSource) { + textureURL = textureMap.texture->_textureSource->getUrl().adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + } + } + + QString cleanURL = textureURL.toDisplayString(); auto idx = cleanURL.lastIndexOf('.'); - auto extension = idx >= 0 ? url.toDisplayString().mid(idx + 1).toLower() : ""; + QString extension = idx >= 0 ? cleanURL.mid(idx + 1).toLower() : ""; if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { - QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - - // FIXME: this isn't properly handling bumpMaps or glossMaps - static std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP; - if (MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.empty()) { - MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::EMISSIVE_MAP] = image::TextureUsage::EMISSIVE_TEXTURE; - MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ALBEDO_MAP] = image::TextureUsage::ALBEDO_TEXTURE; - MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::METALLIC_MAP] = image::TextureUsage::METALLIC_TEXTURE; - MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ROUGHNESS_MAP] = image::TextureUsage::ROUGHNESS_TEXTURE; - MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::NORMAL_MAP] = image::TextureUsage::NORMAL_TEXTURE; - MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::OCCLUSION_MAP] = image::TextureUsage::OCCLUSION_TEXTURE; - MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::LIGHTMAP_MAP] = image::TextureUsage::LIGHTMAP_TEXTURE; - MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::SCATTERING_MAP] = image::TextureUsage::SCATTERING_TEXTURE; - } - - auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel); - if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) { - handleError("Unknown map channel"); - return; - } - - QPair textureKey(textureURL, it->second); + TextureKey textureKey(textureURL, type); if (!_textureBakers.contains(textureKey)) { - auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), it->second); + auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), type); QSharedPointer textureBaker { - new TextureBaker(textureURL, it->second, _textureOutputDir, "", baseTextureFileName), + new TextureBaker(textureURL, type, _textureOutputDir, baseTextureFileName, content), &TextureBaker::deleteLater }; textureBaker->setMapChannel(mapChannel); connect(textureBaker.data(), &TextureBaker::finished, this, &MaterialBaker::handleFinishedTextureBaker); _textureBakers.insert(textureKey, textureBaker); textureBaker->moveToThread(_getNextOvenWorkerThreadOperator ? _getNextOvenWorkerThreadOperator() : thread()); - QMetaObject::invokeMethod(textureBaker.data(), "bake"); + // By default, Qt will invoke this bake immediately if the TextureBaker is on the same worker thread as this MaterialBaker. + // We don't want that, because threads may be waiting for work while this thread is stuck processing a TextureBaker. + // On top of that, _textureBakers isn't fully populated. + // So, use Qt::QueuedConnection. + // TODO: Better thread utilization at the top level, not just the MaterialBaker level + QMetaObject::invokeMethod(textureBaker.data(), "bake", Qt::QueuedConnection); } _materialsNeedingRewrite.insert(textureKey, networkMaterial.second); } else { @@ -169,7 +170,7 @@ void MaterialBaker::handleFinishedTextureBaker() { auto baker = qobject_cast(sender()); if (baker) { - QPair textureKey = { baker->getTextureURL(), baker->getTextureType() }; + TextureKey textureKey = { baker->getTextureURL(), baker->getTextureType() }; if (!baker->hasErrors()) { // this TextureBaker is done and everything went according to plan qCDebug(material_baking) << "Re-writing texture references to" << baker->getTextureURL(); @@ -177,9 +178,13 @@ void MaterialBaker::handleFinishedTextureBaker() { auto newURL = QUrl(_textureOutputDir).resolved(baker->getMetaTextureFileName()); auto relativeURL = QDir(_bakedOutputDir).relativeFilePath(newURL.toString()); + if (!_destinationPath.isEmpty()) { + relativeURL = _destinationPath.resolved(relativeURL).toDisplayString(); + } + // Replace the old texture URLs for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) { - networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(_destinationPath.resolved(relativeURL)); + networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(relativeURL); } } else { // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from @@ -245,3 +250,34 @@ void MaterialBaker::outputMaterial() { // emit signal to indicate the material baking is finished emit finished(); } + +void MaterialBaker::addTexture(const QString& materialName, image::TextureUsage::Type textureUsage, const hfm::Texture& texture) { + auto& textureUsageMap = _textureContentMap[materialName.toStdString()]; + if (textureUsageMap.find(textureUsage) == textureUsageMap.end() && !texture.content.isEmpty()) { + textureUsageMap[textureUsage] = { texture.content, texture.filename }; + } +}; + +void MaterialBaker::setMaterials(const QHash& materials, const QString& baseURL) { + _materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); }); + for (auto& material : materials) { + _materialResource->parsedMaterials.names.push_back(material.name.toStdString()); + _materialResource->parsedMaterials.networkMaterials[material.name.toStdString()] = std::make_shared(material, baseURL); + + // Store any embedded texture content + addTexture(material.name, image::TextureUsage::NORMAL_TEXTURE, material.normalTexture); + addTexture(material.name, image::TextureUsage::ALBEDO_TEXTURE, material.albedoTexture); + addTexture(material.name, image::TextureUsage::GLOSS_TEXTURE, material.glossTexture); + addTexture(material.name, image::TextureUsage::ROUGHNESS_TEXTURE, material.roughnessTexture); + addTexture(material.name, image::TextureUsage::SPECULAR_TEXTURE, material.specularTexture); + addTexture(material.name, image::TextureUsage::METALLIC_TEXTURE, material.metallicTexture); + addTexture(material.name, image::TextureUsage::EMISSIVE_TEXTURE, material.emissiveTexture); + addTexture(material.name, image::TextureUsage::OCCLUSION_TEXTURE, material.occlusionTexture); + addTexture(material.name, image::TextureUsage::SCATTERING_TEXTURE, material.scatteringTexture); + addTexture(material.name, image::TextureUsage::LIGHTMAP_TEXTURE, material.lightmapTexture); + } +} + +void MaterialBaker::setMaterials(const NetworkMaterialResourcePointer& materialResource) { + _materialResource = materialResource; +} \ No newline at end of file diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index 41ce31380e..04782443f0 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -21,19 +21,27 @@ static const QString BAKED_MATERIAL_EXTENSION = ".baked.json"; +using TextureKey = QPair; + class MaterialBaker : public Baker { Q_OBJECT public: - MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath); + MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, QUrl destinationPath = QUrl()); QString getMaterialData() const { return _materialData; } bool isURL() const { return _isURL; } QString getBakedMaterialData() const { return _bakedMaterialData; } + void setMaterials(const QHash& materials, const QString& baseURL); + void setMaterials(const NetworkMaterialResourcePointer& materialResource); + + NetworkMaterialResourcePointer getNetworkMaterialResource() const { return _materialResource; } + static void setNextOvenWorkerThreadOperator(std::function getNextOvenWorkerThreadOperator) { _getNextOvenWorkerThreadOperator = getNextOvenWorkerThreadOperator; } public slots: virtual void bake() override; + virtual void abort() override; signals: void originalMaterialLoaded(); @@ -48,20 +56,28 @@ private: QString _materialData; bool _isURL; + QUrl _destinationPath; NetworkMaterialResourcePointer _materialResource; - QHash, QSharedPointer> _textureBakers; - QMultiHash, std::shared_ptr> _materialsNeedingRewrite; + QHash> _textureBakers; + QMultiHash> _materialsNeedingRewrite; QString _bakedOutputDir; QString _textureOutputDir; QString _bakedMaterialData; - QUrl _destinationPath; QScriptEngine _scriptEngine; static std::function _getNextOvenWorkerThreadOperator; TextureFileNamer _textureFileNamer; + + void addTexture(const QString& materialName, image::TextureUsage::Type textureUsage, const hfm::Texture& texture); + struct TextureUsageHash { + std::size_t operator()(image::TextureUsage::Type textureUsage) const { + return static_cast(textureUsage); + } + }; + std::unordered_map, TextureUsageHash>> _textureContentMap; }; #endif // !hifi_MaterialBaker_h diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 9568a81578..9de32158c5 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -42,12 +42,12 @@ #include "baking/BakerLibrary.h" -ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : +#include + +ModelBaker::ModelBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : _modelURL(inputModelURL), _bakedOutputDir(bakedOutputDirectory), _originalOutputDir(originalOutputDirectory), - _textureThreadGetter(inputTextureThreadGetter), _hasBeenBaked(hasBeenBaked) { auto bakedFilename = _modelURL.fileName(); @@ -167,6 +167,10 @@ void ModelBaker::saveSourceModel() { connect(networkReply, &QNetworkReply::finished, this, &ModelBaker::handleModelNetworkReply); } + + if (_mappingURL.isEmpty()) { + outputUnbakedFST(); + } } void ModelBaker::handleModelNetworkReply() { @@ -209,7 +213,6 @@ void ModelBaker::bakeSourceCopy() { } hifi::ByteArray modelData = modelFile.readAll(); - hfm::Model::Pointer bakedModel; std::vector dracoMeshes; std::vector> dracoMaterialLists; // Material order for per-mesh material lookup used by dracoMeshes @@ -238,47 +241,155 @@ void ModelBaker::bakeSourceCopy() { config->getJobConfig("BuildDracoMesh")->setEnabled(true); // Do not permit potentially lossy modification of joint data meant for runtime ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; - // The resources parsed from this job will not be used for now - // TODO: Proper full baking of all materials for a model - config->getJobConfig("ParseMaterialMapping")->setEnabled(false); // Begin hfm baking baker.run(); - bakedModel = baker.getHFMModel(); + _hfmModel = baker.getHFMModel(); + _materialMapping = baker.getMaterialMapping(); dracoMeshes = baker.getDracoMeshes(); dracoMaterialLists = baker.getDracoMaterialLists(); } - // Populate _textureContentMap with path to content mappings, for quick lookup by URL - for (auto materialIt = bakedModel->materials.cbegin(); materialIt != bakedModel->materials.cend(); materialIt++) { - static const auto addTexture = [](QHash& textureContentMap, const hfm::Texture& texture) { - if (!textureContentMap.contains(texture.filename)) { - // Content may be empty, unless the data is inlined - textureContentMap[texture.filename] = texture.content; - } - }; - const hfm::Material& material = *materialIt; - addTexture(_textureContentMap, material.normalTexture); - addTexture(_textureContentMap, material.albedoTexture); - addTexture(_textureContentMap, material.opacityTexture); - addTexture(_textureContentMap, material.glossTexture); - addTexture(_textureContentMap, material.roughnessTexture); - addTexture(_textureContentMap, material.specularTexture); - addTexture(_textureContentMap, material.metallicTexture); - addTexture(_textureContentMap, material.emissiveTexture); - addTexture(_textureContentMap, material.occlusionTexture); - addTexture(_textureContentMap, material.scatteringTexture); - addTexture(_textureContentMap, material.lightmapTexture); - } - // Do format-specific baking - bakeProcessedSource(bakedModel, dracoMeshes, dracoMaterialLists); + bakeProcessedSource(_hfmModel, dracoMeshes, dracoMaterialLists); if (shouldStop()) { return; } + if (!_hfmModel->materials.isEmpty()) { + _materialBaker = QSharedPointer( + new MaterialBaker(_modelURL.fileName(), true, _bakedOutputDir), + &MaterialBaker::deleteLater + ); + _materialBaker->setMaterials(_hfmModel->materials, _modelURL.toString()); + connect(_materialBaker.data(), &MaterialBaker::finished, this, &ModelBaker::handleFinishedMaterialBaker); + _materialBaker->bake(); + } else { + bakeMaterialMap(); + } +} + +void ModelBaker::handleFinishedMaterialBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this MaterialBaker is done and everything went according to plan + qCDebug(model_baking) << "Adding baked material to FST mapping " << baker->getBakedMaterialData(); + + QString relativeBakedMaterialURL = _modelURL.fileName(); + auto baseName = relativeBakedMaterialURL.left(relativeBakedMaterialURL.lastIndexOf('.')); + relativeBakedMaterialURL = baseName + BAKED_MATERIAL_EXTENSION; + + auto materialResource = baker->getNetworkMaterialResource(); + if (materialResource) { + for (auto materialName : materialResource->parsedMaterials.names) { + QJsonObject json; + json[QString("mat::" + QString(materialName.c_str()))] = relativeBakedMaterialURL + "#" + materialName.c_str(); + _materialMappingJSON.push_back(json); + } + } + } else { + // this material failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the material to our warnings + _warningList << baker->getWarnings(); + } + } else { + handleWarning("Failed to bake the materials for model with URL " + _modelURL.toString()); + } + + bakeMaterialMap(); +} + +void ModelBaker::bakeMaterialMap() { + if (!_materialMapping.empty()) { + // TODO: The existing material map must be baked in order, so we do it all on this thread to preserve the order. + // It could be spread over multiple threads if we had a good way of preserving the order once all of the bakers are done + _materialBaker = QSharedPointer( + new MaterialBaker("materialMap" + QString::number(_materialMapIndex++), true, _bakedOutputDir), + &MaterialBaker::deleteLater + ); + _materialBaker->setMaterials(_materialMapping.front().second); + connect(_materialBaker.data(), &MaterialBaker::finished, this, &ModelBaker::handleFinishedMaterialMapBaker); + _materialBaker->bake(); + } else { + outputBakedFST(); + } +} + +void ModelBaker::handleFinishedMaterialMapBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this MaterialBaker is done and everything went according to plan + qCDebug(model_baking) << "Adding baked material to FST mapping " << baker->getBakedMaterialData(); + + QString materialName; + { + auto materialResource = baker->getNetworkMaterialResource(); + if (materialResource) { + auto url = materialResource->getURL(); + if (!url.isEmpty()) { + QString urlString = url.toDisplayString(); + auto index = urlString.lastIndexOf("#"); + if (index != -1) { + materialName = urlString.right(urlString.length() - index); + } + } + } + } + + QJsonObject json; + json[QString(_materialMapping.front().first.c_str())] = baker->getMaterialData() + BAKED_MATERIAL_EXTENSION + materialName; + _materialMappingJSON.push_back(json); + } else { + // this material failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the material to our warnings + _warningList << baker->getWarnings(); + } + } else { + handleWarning("Failed to bake the materialMap for model with URL " + _modelURL.toString() + " and mapping target " + _materialMapping.front().first.c_str()); + } + + _materialMapping.erase(_materialMapping.begin()); + bakeMaterialMap(); +} + +void ModelBaker::outputUnbakedFST() { + // Output an unbaked FST file in the original output folder to make it easier for FSTBaker to rebake this model + // TODO: Consider a more robust method that does not depend on FSTBaker navigating to a hardcoded relative path + QString outputFSTFilename = _modelURL.fileName(); + auto extensionStart = outputFSTFilename.indexOf("."); + if (extensionStart != -1) { + outputFSTFilename.resize(extensionStart); + } + outputFSTFilename += FST_EXTENSION; + QString outputFSTURL = _originalOutputDir + "/" + outputFSTFilename; + + hifi::VariantHash outputMapping; + outputMapping[FST_VERSION_FIELD] = FST_VERSION; + outputMapping[FILENAME_FIELD] = _modelURL.fileName(); + outputMapping[COMMENT_FIELD] = "This FST file was generated by Oven for use during rebaking. It is not part of the original model. This file's existence is subject to change."; + hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); + + QFile fstOutputFile { outputFSTURL }; + if (fstOutputFile.exists()) { + handleWarning("The file '" + outputFSTURL + "' already exists. Should that be baked instead of '" + _modelURL.toString() + "'?"); + return; + } + if (!fstOutputFile.open(QIODevice::WriteOnly)) { + handleWarning("Failed to open file '" + outputFSTURL + "' for writing. Rebaking may fail on the associated model."); + return; + } + if (fstOutputFile.write(fstOut) == -1) { + handleWarning("Failed to write to file '" + outputFSTURL + "'. Rebaking may fail on the associated model."); + } +} + +void ModelBaker::outputBakedFST() { // Output FST file, copying over input mappings if available QString outputFSTFilename = !_mappingURL.isEmpty() ? _mappingURL.fileName() : _modelURL.fileName(); auto extensionStart = outputFSTFilename.indexOf("."); @@ -291,8 +402,11 @@ void ModelBaker::bakeSourceCopy() { auto outputMapping = _mapping; outputMapping[FST_VERSION_FIELD] = FST_VERSION; outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName(); - // All textures will be found in the same directory as the model - outputMapping[TEXDIR_FIELD] = "."; + outputMapping.remove(TEXDIR_FIELD); + outputMapping.remove(COMMENT_FIELD); + if (!_materialMappingJSON.isEmpty()) { + outputMapping[MATERIAL_MAPPING_FIELD] = QJsonDocument(_materialMappingJSON).toJson(QJsonDocument::Compact); + } hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); QFile fstOutputFile { outputFSTURL }; @@ -307,17 +421,16 @@ void ModelBaker::bakeSourceCopy() { _outputFiles.push_back(outputFSTURL); _outputMappingURL = outputFSTURL; - // check if we're already done with textures (in case we had none to re-write) - checkIfTexturesFinished(); + exportScene(); + qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL; + emit finished(); } void ModelBaker::abort() { Baker::abort(); - // tell our underlying TextureBaker instances to abort - // the ModelBaker will wait until all are aborted before emitting its own abort signal - for (auto& textureBaker : _bakingTextures) { - textureBaker->abort(); + if (_materialBaker) { + _materialBaker->abort(); } } @@ -354,247 +467,6 @@ bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dr return true; } -QString ModelBaker::compressTexture(QString modelTextureFileName, image::TextureUsage::Type textureType) { - - QFileInfo modelTextureFileInfo { modelTextureFileName.replace("\\", "/") }; - - if (modelTextureFileInfo.suffix().toLower() == BAKED_TEXTURE_KTX_EXT.mid(1)) { - // re-baking a model that already references baked textures - // this is an error - return from here - handleError("Cannot re-bake a file that already references compressed textures"); - return QString::null; - } - - if (!image::getSupportedFormats().contains(modelTextureFileInfo.suffix())) { - // this is a texture format we don't bake, skip it - handleWarning(modelTextureFileName + " is not a bakeable texture format"); - return QString::null; - } - - // make sure this texture points to something and isn't one we've already re-mapped - QString textureChild { QString::null }; - if (!modelTextureFileInfo.filePath().isEmpty()) { - // check if this was an embedded texture that we already have in-memory content for - QByteArray textureContent; - - // figure out the URL to this texture, embedded or external - if (!modelTextureFileInfo.filePath().isEmpty()) { - textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit()); - } - auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull()); - - TextureKey textureKey { urlToTexture, textureType }; - auto bakingTextureIt = _bakingTextures.find(textureKey); - if (bakingTextureIt == _bakingTextures.cend()) { - // construct the new baked texture file name and file path - // ensuring that the baked texture will have a unique name - // even if there was another texture with the same name at a different path - QString baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); - - QString bakedTextureFilePath { - _bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX - }; - - textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX; - - _outputFiles.push_back(bakedTextureFilePath); - - // bake this texture asynchronously - bakeTexture(textureKey, _bakedOutputDir, baseTextureFileName, textureContent); - } else { - // Fetch existing texture meta name - textureChild = (*bakingTextureIt)->getBaseFilename() + BAKED_META_TEXTURE_SUFFIX; - } - } - - qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName - << "to" << textureChild; - - return textureChild; -} - -void ModelBaker::bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { - // start a bake for this texture and add it to our list to keep track of - QSharedPointer bakingTexture{ - new TextureBaker(textureKey.first, textureKey.second, outputDir, "../", bakedFilename, textureContent), - &TextureBaker::deleteLater - }; - - // make sure we hear when the baking texture is done or aborted - connect(bakingTexture.data(), &Baker::finished, this, &ModelBaker::handleBakedTexture); - connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture); - - // keep a shared pointer to the baking texture - _bakingTextures.insert(textureKey, bakingTexture); - - // start baking the texture on one of our available worker threads - bakingTexture->moveToThread(_textureThreadGetter()); - QMetaObject::invokeMethod(bakingTexture.data(), "bake"); -} - -void ModelBaker::handleBakedTexture() { - TextureBaker* bakedTexture = qobject_cast(sender()); - qDebug() << "Handling baked texture" << bakedTexture->getTextureURL(); - - // make sure we haven't already run into errors, and that this is a valid texture - if (bakedTexture) { - if (!shouldStop()) { - if (!bakedTexture->hasErrors()) { - if (!_originalOutputDir.isEmpty()) { - // we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture - - // use the path to the texture being baked to determine if this was an embedded or a linked texture - - // it is embeddded if the texure being baked was inside a folder with the name of the model - // since that is the fake URL we provide when baking external textures - - if (!_modelURL.isParentOf(bakedTexture->getTextureURL())) { - // for linked textures we want to save a copy of original texture beside the original model - - qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); - - // check if we have a relative path to use for the texture - auto relativeTexturePath = texturePathRelativeToModel(_modelURL, bakedTexture->getTextureURL()); - - QFile originalTextureFile{ - _originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName() - }; - - if (relativeTexturePath.length() > 0) { - // make the folders needed by the relative path - } - - if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { - qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() - << "for" << _modelURL; - } else { - handleError("Could not save original external texture " + originalTextureFile.fileName() - + " for " + _modelURL.toString()); - return; - } - } - } - - - // now that this texture has been baked and handled, we can remove that TextureBaker from our hash - _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); - - checkIfTexturesFinished(); - } else { - // there was an error baking this texture - add it to our list of errors - _errorList.append(bakedTexture->getErrors()); - - // we don't emit finished yet so that the other textures can finish baking first - _pendingErrorEmission = true; - - // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list - _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); - - // abort any other ongoing texture bakes since we know we'll end up failing - for (auto& bakingTexture : _bakingTextures) { - bakingTexture->abort(); - } - - checkIfTexturesFinished(); - } - } else { - // we have errors to attend to, so we don't do extra processing for this texture - // but we do need to remove that TextureBaker from our list - // and then check if we're done with all textures - _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); - - checkIfTexturesFinished(); - } - } -} - -void ModelBaker::handleAbortedTexture() { - // grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore - TextureBaker* bakedTexture = qobject_cast(sender()); - - qDebug() << "Texture aborted: " << bakedTexture->getTextureURL(); - - if (bakedTexture) { - _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); - } - - // since a texture we were baking aborted, our status is also aborted - _shouldAbort.store(true); - - // abort any other ongoing texture bakes since we know we'll end up failing - for (auto& bakingTexture : _bakingTextures) { - bakingTexture->abort(); - } - - checkIfTexturesFinished(); -} - -QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded) { - QUrl urlToTexture; - - if (isEmbedded) { - urlToTexture = _modelURL.toString() + "/" + textureFileInfo.filePath(); - } else { - if (textureFileInfo.exists() && textureFileInfo.isFile()) { - // set the texture URL to the local texture that we have confirmed exists - urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); - } else { - // external texture that we'll need to download or find - - // this is a relative file path which will require different handling - // depending on the location of the original model - if (_modelURL.isLocalFile() && textureFileInfo.exists() && textureFileInfo.isFile()) { - // the absolute path we ran into for the texture in the model exists on this machine - // so use that file - urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); - } else { - // we didn't find the texture on this machine at the absolute path - // so assume that it is right beside the model to match the behaviour of interface - urlToTexture = _modelURL.resolved(textureFileInfo.fileName()); - } - } - } - - return urlToTexture; -} - -QString ModelBaker::texturePathRelativeToModel(QUrl modelURL, QUrl textureURL) { - auto modelPath = modelURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); - auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); - - if (texturePath.startsWith(modelPath)) { - // texture path is a child of the model path, return the texture path without the model path - return texturePath.mid(modelPath.length()); - } else { - // the texture path was not a child of the model path, return the empty string - return ""; - } -} - -void ModelBaker::checkIfTexturesFinished() { - // check if we're done everything we need to do for this model - // and emit our finished signal if we're done - - if (_bakingTextures.isEmpty()) { - if (shouldStop()) { - // if we're checking for completion but we have errors - // that means one or more of our texture baking operations failed - - if (_pendingErrorEmission) { - setIsFinished(true); - } - - return; - } else { - qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL; - - texturesFinished(); - - setIsFinished(true); - } - } -} - void ModelBaker::setWasAborted(bool wasAborted) { if (wasAborted != _wasAborted.load()) { Baker::setWasAborted(wasAborted); @@ -605,70 +477,6 @@ void ModelBaker::setWasAborted(bool wasAborted) { } } -void ModelBaker::texturesFinished() { - embedTextureMetaData(); - exportScene(); -} - -void ModelBaker::embedTextureMetaData() { - std::vector embeddedTextureNodes; - - for (FBXNode& rootChild : _rootNode.children) { - if (rootChild.name == "Objects") { - qlonglong maxId = 0; - for (auto &child : rootChild.children) { - if (child.properties.length() == 3) { - maxId = std::max(maxId, child.properties[0].toLongLong()); - } - } - - for (auto& object : rootChild.children) { - if (object.name == "Texture") { - QVariant relativeFilename; - for (auto& child : object.children) { - if (child.name == "RelativeFilename") { - relativeFilename = child.properties[0]; - break; - } - } - - if (relativeFilename.isNull() - || !relativeFilename.toString().endsWith(BAKED_META_TEXTURE_SUFFIX)) { - continue; - } - if (object.properties.length() < 2) { - qWarning() << "Found texture with unexpected number of properties: " << object.name; - continue; - } - - FBXNode videoNode; - videoNode.name = "Video"; - videoNode.properties.append(++maxId); - videoNode.properties.append(object.properties[1]); - videoNode.properties.append("Clip"); - - QString bakedTextureFilePath { - _bakedOutputDir + "/" + relativeFilename.toString() - }; - - QFile textureFile { bakedTextureFilePath }; - if (!textureFile.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open: " << bakedTextureFilePath; - continue; - } - - videoNode.children.append({ "RelativeFilename", { relativeFilename }, { } }); - videoNode.children.append({ "Content", { textureFile.readAll() }, { } }); - - rootChild.children.append(videoNode); - - textureFile.close(); - } - } - } - } -} - void ModelBaker::exportScene() { auto fbxData = FBXWriter::encodeFBX(_rootNode); diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index d9a559392f..b98d9716e1 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -16,19 +16,16 @@ #include #include #include +#include #include "Baker.h" -#include "TextureBaker.h" -#include "baking/TextureFileNamer.h" +#include "MaterialBaker.h" #include "ModelBakingLoggingCategory.h" -#include - #include #include -using TextureBakerThreadGetter = std::function; using GetMaterialIDCallback = std::function ; static const QString FST_EXTENSION { ".fst" }; @@ -42,10 +39,7 @@ class ModelBaker : public Baker { Q_OBJECT public: - using TextureKey = QPair; - - ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + ModelBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); void setOutputURLSuffix(const QUrl& urlSuffix); void setMappingURL(const QUrl& mappingURL); @@ -54,7 +48,6 @@ public: void initializeOutputDirs(); bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); - QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; QUrl getModelURL() const { return _modelURL; } @@ -71,20 +64,15 @@ public slots: protected: void saveSourceModel(); virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) = 0; - void checkIfTexturesFinished(); - void texturesFinished(); - void embedTextureMetaData(); void exportScene(); FBXNode _rootNode; - QHash _textureContentMap; QUrl _modelURL; QUrl _outputURLSuffix; QUrl _mappingURL; hifi::VariantHash _mapping; QString _bakedOutputDir; QString _originalOutputDir; - TextureBakerThreadGetter _textureThreadGetter; QString _originalOutputModelPath; QString _outputMappingURL; QUrl _bakedModelURL; @@ -92,23 +80,21 @@ protected: protected slots: void handleModelNetworkReply(); virtual void bakeSourceCopy(); - -private slots: - void handleBakedTexture(); - void handleAbortedTexture(); + void handleFinishedMaterialBaker(); + void handleFinishedMaterialMapBaker(); private: - QUrl getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded = false); - void bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent); - QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); - - QMultiHash> _bakingTextures; - QHash _textureNameMatchCount; - bool _pendingErrorEmission { false }; + void outputUnbakedFST(); + void outputBakedFST(); + void bakeMaterialMap(); bool _hasBeenBaked { false }; - TextureFileNamer _textureFileNamer; + hfm::Model::Pointer _hfmModel; + MaterialMapping _materialMapping; + int _materialMapIndex { 0 }; + QJsonArray _materialMappingJSON; + QSharedPointer _materialBaker; }; #endif // hifi_ModelBaker_h diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index 70bdeb2071..a2d0ab1094 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -132,55 +132,6 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h handleWarning("Baked mesh for OBJ model '" + _modelURL.toString() + "' is empty"); } - // Generating Texture Node - // iterate through mesh parts and process the associated textures - auto size = meshParts.size(); - for (int i = 0; i < size; i++) { - QString material = meshParts[i].materialID; - HFMMaterial currentMaterial = hfmModel->materials[material]; - if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { - auto textureID = nextNodeID(); - _mapTextureMaterial.emplace_back(textureID, i); - - FBXNode textureNode; - { - textureNode.name = TEXTURE_NODE_NAME; - textureNode.properties = { textureID, "texture" + QString::number(textureID) }; - } - - // Texture node child - TextureName node - FBXNode textureNameNode; - { - textureNameNode.name = TEXTURENAME_NODE_NAME; - QByteArray propertyString = (!currentMaterial.albedoTexture.filename.isEmpty()) ? "Kd" : "Ka"; - textureNameNode.properties = { propertyString }; - } - - // Texture node child - Relative Filename node - FBXNode relativeFilenameNode; - { - relativeFilenameNode.name = RELATIVEFILENAME_NODE_NAME; - } - - QByteArray textureFileName = (!currentMaterial.albedoTexture.filename.isEmpty()) ? currentMaterial.albedoTexture.filename : currentMaterial.specularTexture.filename; - - auto textureType = (!currentMaterial.albedoTexture.filename.isEmpty()) ? image::TextureUsage::Type::ALBEDO_TEXTURE : image::TextureUsage::Type::SPECULAR_TEXTURE; - - // Compress the texture using ModelBaker::compressTexture() and store compressed file's name in the node - auto textureFile = compressTexture(textureFileName, textureType); - if (textureFile.isNull()) { - // Baking failed return - handleError("Failed to compress texture: " + textureFileName); - return; - } - relativeFilenameNode.properties = { textureFile }; - - textureNode.children = { textureNameNode, relativeFilenameNode }; - - objectNode.children.append(textureNode); - } - } - // Generating Connections node connectionsNode.name = CONNECTIONS_NODE_NAME; @@ -199,29 +150,6 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h cNode.properties = { CONNECTIONS_NODE_PROPERTY, materialID, modelID }; connectionsNode.children.append(cNode); } - - // Connect textures to materials - for (const auto& texMat : _mapTextureMaterial) { - FBXNode cAmbientNode; - cAmbientNode.name = C_NODE_NAME; - cAmbientNode.properties = { - CONNECTIONS_NODE_PROPERTY_1, - texMat.first, - _materialIDs[texMat.second], - "AmbientFactor" - }; - connectionsNode.children.append(cAmbientNode); - - FBXNode cDiffuseNode; - cDiffuseNode.name = C_NODE_NAME; - cDiffuseNode.properties = { - CONNECTIONS_NODE_PROPERTY_1, - texMat.first, - _materialIDs[texMat.second], - "DiffuseColor" - }; - connectionsNode.children.append(cDiffuseNode); - } } // Set properties for material nodes diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index d1eced5452..9d0fe53e3c 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -13,13 +13,9 @@ #define hifi_OBJBaker_h #include "Baker.h" -#include "TextureBaker.h" #include "ModelBaker.h" - #include "ModelBakingLoggingCategory.h" -using TextureBakerThreadGetter = std::function; - using NodeID = qlonglong; class OBJBaker : public ModelBaker { @@ -35,9 +31,7 @@ private: void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); NodeID nextNodeID() { return _nodeID++; } - NodeID _nodeID { 0 }; std::vector _materialIDs; - std::vector> _mapTextureMaterial; }; #endif // hifi_OBJBaker_h diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index 3756ae86de..e4b0abcef6 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -33,14 +33,13 @@ const QString BAKED_META_TEXTURE_SUFFIX = ".texmeta.json"; bool TextureBaker::_compressionEnabled = true; TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, - const QDir& outputDirectory, const QString& metaTexturePathPrefix, - const QString& baseFilename, const QByteArray& textureContent) : + const QDir& outputDirectory, const QString& baseFilename, + const QByteArray& textureContent) : _textureURL(textureURL), _originalTexture(textureContent), _textureType(textureType), _baseFilename(baseFilename), - _outputDirectory(outputDirectory), - _metaTexturePathPrefix(metaTexturePathPrefix) + _outputDirectory(outputDirectory) { if (baseFilename.isEmpty()) { // figure out the baked texture filename @@ -131,7 +130,10 @@ void TextureBaker::handleTextureNetworkReply() { void TextureBaker::processTexture() { // the baked textures need to have the source hash added for cache checks in Interface // so we add that to the processed texture before handling it off to be serialized - auto hashData = QCryptographicHash::hash(_originalTexture, QCryptographicHash::Md5); + QCryptographicHash hasher(QCryptographicHash::Md5); + hasher.addData(_originalTexture); + hasher.addData((const char*)&_textureType, sizeof(_textureType)); + auto hashData = hasher.result(); std::string hash = hashData.toHex().toStdString(); TextureMeta meta; @@ -148,7 +150,7 @@ void TextureBaker::processTexture() { // IMPORTANT: _originalTexture is empty past this point _originalTexture.clear(); _outputFiles.push_back(originalCopyFilePath); - meta.original = _metaTexturePathPrefix + _originalCopyFilePath.fileName(); + meta.original = _originalCopyFilePath.fileName(); } // Load the copy of the original file from the baked output directory. New images will be created using the original as the source data. @@ -201,12 +203,12 @@ void TextureBaker::processTexture() { return; } _outputFiles.push_back(filePath); - meta.availableTextureTypes[memKTX->_header.getGLInternaFormat()] = _metaTexturePathPrefix + fileName; + meta.availableTextureTypes[memKTX->_header.getGLInternaFormat()] = fileName; } } // Uncompressed KTX - if (_textureType == image::TextureUsage::Type::CUBE_TEXTURE) { + if (_textureType == image::TextureUsage::Type::SKY_TEXTURE || _textureType == image::TextureUsage::Type::AMBIENT_TEXTURE) { buffer->reset(); auto processedTexture = image::processImage(std::move(buffer), _textureURL.toString().toStdString(), image::ColorChannel::NONE, ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, false, gpu::BackendTarget::GL45, _abortProcessing); @@ -237,7 +239,7 @@ void TextureBaker::processTexture() { return; } _outputFiles.push_back(filePath); - meta.uncompressed = _metaTexturePathPrefix + fileName; + meta.uncompressed = fileName; } else { buffer.reset(); } diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index 13ad82cff4..5fda05e9b4 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -32,8 +32,8 @@ class TextureBaker : public Baker { public: TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, - const QDir& outputDirectory, const QString& metaTexturePathPrefix = "", - const QString& baseFilename = QString(), const QByteArray& textureContent = QByteArray()); + const QDir& outputDirectory, const QString& baseFilename = QString(), + const QByteArray& textureContent = QByteArray()); const QByteArray& getOriginalTexture() const { return _originalTexture; } @@ -74,7 +74,6 @@ private: QString _baseFilename; QDir _outputDirectory; QString _metaTextureFileName; - QString _metaTexturePathPrefix; QUrl _originalCopyFilePath; std::atomic _abortProcessing { false }; diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index f9445bd432..e500f9d83f 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -44,7 +44,7 @@ bool isModelBaked(const QUrl& bakeableModelURL) { return beforeModelExtension.endsWith(".baked"); } -std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath) { +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, const QString& contentOutputPath) { auto filename = bakeableModelURL.fileName(); // Output in a sub-folder with the name of the model, potentially suffixed by a number to make it unique @@ -58,20 +58,20 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked"; QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; - return getModelBakerWithOutputDirectories(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + return getModelBakerWithOutputDirectories(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory); } -std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) { +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) { auto filename = bakeableModelURL.fileName(); std::unique_ptr baker; if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { - baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); + baker = std::make_unique(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { - baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); + baker = std::make_unique(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) { - baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + baker = std::make_unique(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory); //} else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); } else { diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h index a646c8d36a..8f82661b25 100644 --- a/libraries/baking/src/baking/BakerLibrary.h +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -23,9 +23,9 @@ bool isModelBaked(const QUrl& bakeableModelURL); // Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored // Returns an empty pointer if a baker could not be created -std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, const QString& contentOutputPath); // Similar to getModelBaker, but gives control over where the output folders will be -std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory); +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory); #endif // hifi_BakerLibrary_h diff --git a/libraries/baking/src/baking/FSTBaker.cpp b/libraries/baking/src/baking/FSTBaker.cpp index acf3bfe1c7..176c35c059 100644 --- a/libraries/baking/src/baking/FSTBaker.cpp +++ b/libraries/baking/src/baking/FSTBaker.cpp @@ -18,9 +18,8 @@ #include -FSTBaker::FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : - ModelBaker(inputMappingURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { +FSTBaker::FSTBaker(const QUrl& inputMappingURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputMappingURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { if (hasBeenBaked) { // Look for the original model file one directory higher. Perhaps this is an oven output directory. QUrl originalRelativePath = QUrl("../original/" + inputMappingURL.fileName().replace(BAKED_FST_EXTENSION, FST_EXTENSION)); @@ -70,7 +69,7 @@ void FSTBaker::bakeSourceCopy() { return; } - auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir); + auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _bakedOutputDir, _originalOutputDir); _modelBaker = std::unique_ptr(dynamic_cast(baker.release())); if (!_modelBaker) { handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker"); diff --git a/libraries/baking/src/baking/FSTBaker.h b/libraries/baking/src/baking/FSTBaker.h index 85c7c93a37..32997680f5 100644 --- a/libraries/baking/src/baking/FSTBaker.h +++ b/libraries/baking/src/baking/FSTBaker.h @@ -18,8 +18,7 @@ class FSTBaker : public ModelBaker { Q_OBJECT public: - FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + FSTBaker(const QUrl& inputMappingURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); virtual QUrl getFullOutputMappingURL() const override; diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 364fea6b15..ffb5bd85ad 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -201,15 +201,16 @@ namespace controller { * UiNavSelectnumbernumberGenerate a keyboard Enter key event. * * UiNavBacknumbernumberGenerate a keyboard Esc key event. - * LeftHandClicknumbernumberDeprecated: No action. - * - * RightHandClicknumbernumberDeprecated: No action. - * - * ShiftnumbernumberDeprecated: No action. - * PrimaryActionnumbernumberDeprecated: No action. - * - * SecondaryActionnumbernumberDeprecated: No action. - * + * LeftHandClicknumbernumberDeprecated: This + * action is deprecated and will be removed. It takes no action. + * RightHandClicknumbernumberDeprecated: This + * action is deprecated and will be removed. It takes no action. + * ShiftnumbernumberDeprecated: This + * action is deprecated and will be removed. It takes no action. + * PrimaryActionnumbernumberDeprecated: This + * action is deprecated and will be removed. It takes no action. + * SecondaryActionnumbernumberDeprecated: This + * action is deprecated and will be removed. It takes no action. * * Aliases * BackwardnumbernumberAlias for TranslateZ in the @@ -234,84 +235,86 @@ namespace controller { * direction. * * Deprecated Aliases - * LEFT_HANDnumber{@link Pose}Deprecated: Use - * LeftHand instead. - * RIGHT_HANDnumber{@link Pose}Deprecated: Use - * RightHand instead. - * BOOM_INnumbernumberDeprecated: Use - * BoomIn instead. - * BOOM_OUTnumbernumberDeprecated: Use - * BoomOut instead. - * CONTEXT_MENUnumbernumberDeprecated: Use - * ContextMenu instead. - * TOGGLE_MUTEnumbernumberDeprecated: Use - * ToggleMute instead. - * SPRINTnumbernumberDeprecated: Use - * Sprint instead. - * LONGITUDINAL_BACKWARDnumbernumberDeprecated: Use - * Backward instead. - * LONGITUDINAL_FORWARDnumbernumberDeprecated: Use - * Forward instead. - * LATERAL_LEFTnumbernumberDeprecated: Use - * StrafeLeft instead. - * LATERAL_RIGHTnumbernumberDeprecated: Use - * StrafeRight instead. - * VERTICAL_UPnumbernumberDeprecated: Use - * Up instead. - * VERTICAL_DOWNnumbernumberDeprecated: Use - * Down instead. - * PITCH_DOWNnumbernumberDeprecated: Use - * PitchDown instead. - * PITCH_UPnumbernumberDeprecated: Use - * PitchUp instead. - * YAW_LEFTnumbernumberDeprecated: Use - * YawLeft instead. - * YAW_RIGHTnumbernumberDeprecated: Use - * YawRight instead. - * LEFT_HAND_CLICKnumbernumberDeprecated: Use - * LeftHandClick instead. - * RIGHT_HAND_CLICKnumbernumberDeprecated: Use - * RightHandClick instead. - * SHIFTnumbernumberDeprecated: Use - * Shift instead. - * ACTION1numbernumberDeprecated: Use - * PrimaryAction instead. - * ACTION2numbernumberDeprecated: Use - * SecondaryAction instead. + * LEFT_HANDnumber{@link Pose}Deprecated: This + * action is deprecated and will be removed. Use LeftHand instead. + * RIGHT_HANDnumber{@link Pose}Deprecated: This + * action is deprecated and will be removed. Use RightHand instead. + * BOOM_INnumbernumberDeprecated: This + * action is deprecated and will be removed. Use BoomIn instead. + * BOOM_OUTnumbernumberDeprecated: This + * action is deprecated and will be removed. Use BoomOut instead. + * CONTEXT_MENUnumbernumberDeprecated: This + * action is deprecated and will be removed. Use ContextMenu instead. + * TOGGLE_MUTEnumbernumberDeprecated: This + * action is deprecated and will be removed. Use ToggleMute instead. + * TOGGLE_PUSHTOTALKnumbernumberDeprecated: This + * action is deprecated and will be removed. Use TogglePushToTalk instead. + * SPRINTnumbernumberDeprecated: This + * action is deprecated and will be removed. Use Sprint instead. + * LONGITUDINAL_BACKWARDnumbernumberDeprecated: This + * action is deprecated and will be removed. Use Backward instead. + * LONGITUDINAL_FORWARDnumbernumberDeprecated: This + * action is deprecated and will be removed. Use Forward instead. + * LATERAL_LEFTnumbernumberDeprecated: This + * action is deprecated and will be removed. Use StrafeLeft instead. + * LATERAL_RIGHTnumbernumberDeprecated: This + * action is deprecated and will be removed. Use StrafeRight instead. + * VERTICAL_UPnumbernumberDeprecated: This + * action is deprecated and will be removed. Use Up instead. + * VERTICAL_DOWNnumbernumberDeprecated: This + * action is deprecated and will be removed. Use Down instead. + * PITCH_DOWNnumbernumberDeprecated: This + * action is deprecated and will be removed. Use PitchDown instead. + * PITCH_UPnumbernumberDeprecated: This + * action is deprecated and will be removed. Use PitchUp instead. + * YAW_LEFTnumbernumberDeprecated: This + * action is deprecated and will be removed. Use YawLeft instead. + * YAW_RIGHTnumbernumberDeprecated: This + * action is deprecated and will be removed. Use YawRight instead. + * LEFT_HAND_CLICKnumbernumberDeprecated: This + * action is deprecated and will be removed. Use LeftHandClick instead. + * RIGHT_HAND_CLICKnumbernumberDeprecated: This + * action is deprecated and will be removed. Use RightHandClick instead. + * SHIFTnumbernumberDeprecated: This + * action is deprecated and will be removed. Use Shift instead. + * ACTION1numbernumberDeprecated: This + * action is deprecated and will be removed. Use PrimaryAction instead. + * ACTION2numbernumberDeprecated: This + * action is deprecated and will be removed. Use SecondaryAction instead. * * Deprecated Trackers - * TrackedObject00number{@link Pose}Deprecated: No - * action. - * TrackedObject01number{@link Pose}Deprecated: No - * action. - * TrackedObject02number{@link Pose}Deprecated: No - * action. - * TrackedObject03number{@link Pose}Deprecated: No - * action. - * TrackedObject04number{@link Pose}Deprecated: No - * action. - * TrackedObject05number{@link Pose}Deprecated: No - * action. - * TrackedObject06number{@link Pose}Deprecated: No - * action. - * TrackedObject07number{@link Pose}Deprecated: No - * action. - * TrackedObject08number{@link Pose}Deprecated: No - * action. - * TrackedObject09number{@link Pose}Deprecated: No - * action. - * TrackedObject10number{@link Pose}Deprecated: No - * action. - * TrackedObject11number{@link Pose}Deprecated: No - * action. - * TrackedObject12number{@link Pose}Deprecated: No - * action. - * TrackedObject13number{@link Pose}Deprecated: No - * action. - * TrackedObject14number{@link Pose}Deprecated: No - * action. - * TrackedObject15number{@link Pose}Deprecated: No - * action. + * TrackedObject00number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject01number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject02number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject03number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject04number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject05number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject06number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject07number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject08number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject09number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject10number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject11number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject12number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject13number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject14number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. + * TrackedObject15number{@link Pose}Deprecated: + * This action is deprecated and will be removed. It takes no action. * * * @typedef {object} Controller.Actions diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index de1cada97b..688b59e18c 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -76,7 +76,7 @@ namespace controller { * Gets a list of all available actions. * @function Controller.getAllActions * @returns {Action[]} All available actions. - * @deprecated This function no longer works. + * @deprecated This function is deprecated and will be removed. It no longer works. */ // FIXME: This function causes a JavaScript crash: https://highfidelity.manuscript.com/f/cases/edit/13921 Q_INVOKABLE QVector getAllActions(); @@ -86,7 +86,7 @@ namespace controller { * @function Controller.getAvailableInputs * @param {number} deviceID - Integer ID of the hardware device. * @returns {NamedPair[]} All available inputs for the device. - * @deprecated This function no longer works. + * @deprecated This function is deprecated and will be removed. It no longer works. */ // FIXME: This function causes a JavaScript crash: https://highfidelity.manuscript.com/f/cases/edit/13922 Q_INVOKABLE QVector getAvailableInputs(unsigned int device); diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 47a213cf71..fcd695bed8 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -25,12 +25,4 @@ void NullDisplayPlugin::submitFrame(const gpu::FramePointer& frame) { if (frame) { _gpuContext->consumeFrameUpdates(frame); } -} - -QImage NullDisplayPlugin::getScreenshot(float aspectRatio) const { - return QImage(); -} - -QImage NullDisplayPlugin::getSecondaryCameraScreenshot() const { - return QImage(); -} +} \ No newline at end of file diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index e4ff1b8b37..7aa763bab9 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -17,8 +17,6 @@ public: glm::uvec2 getRecommendedRenderSize() const override; void submitFrame(const gpu::FramePointer& newFrame) override; - QImage getScreenshot(float aspectRatio = 0.0f) const override; - QImage getSecondaryCameraScreenshot() const override; void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override {}; void pluginUpdate() override {}; private: diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index c536e6b6e2..a7e27ca770 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -46,7 +46,7 @@ #include #include "CompositorHelper.h" #include "Logging.h" - +#include "RefreshRateController.h" extern QThread* RENDER_THREAD; class PresentThread : public QThread, public Dependency { @@ -60,12 +60,16 @@ public: shutdown(); }); setObjectName("Present"); + + _refreshRateController = std::make_shared(); } ~PresentThread() { shutdown(); } + auto getRefreshRateController() { return _refreshRateController; } + void shutdown() { if (isRunning()) { // First ensure that we turn off any current display plugin @@ -109,7 +113,6 @@ public: Q_ASSERT(_context); _context->makeCurrent(); CHECK_GL_ERROR(); - _context->doneCurrent(); while (!_shutdown) { if (_pendingOtherThreadOperation) { PROFILE_RANGE(render, "MainThreadOp") @@ -129,6 +132,7 @@ public: Lock lock(_mutex); _condition.wait(lock, [&] { return _finishedOtherThreadOperation; }); } + _context->makeCurrent(); } // Check for a new display plugin @@ -140,18 +144,16 @@ public: if (newPlugin != currentPlugin) { // Deactivate the old plugin if (currentPlugin != nullptr) { - _context->makeCurrent(); currentPlugin->uncustomizeContext(); CHECK_GL_ERROR(); - _context->doneCurrent(); + // Force completion of all pending GL commands + glFinish(); } if (newPlugin) { bool hasVsync = true; QThread::setPriority(newPlugin->getPresentPriority()); bool wantVsync = newPlugin->wantVsync(); - _context->makeCurrent(); - CHECK_GL_ERROR(); #if defined(Q_OS_MAC) newPlugin->swapBuffers(); #endif @@ -163,7 +165,8 @@ public: newPlugin->setVsyncEnabled(hasVsync); newPlugin->customizeContext(); CHECK_GL_ERROR(); - _context->doneCurrent(); + // Force completion of all pending GL commands + glFinish(); } currentPlugin = newPlugin; _newPluginQueue.pop(); @@ -180,17 +183,17 @@ public: } // Execute the frame and present it to the display device. - _context->makeCurrent(); { PROFILE_RANGE(render, "PluginPresent") gl::globalLock(); - currentPlugin->present(); + currentPlugin->present(_refreshRateController); gl::globalRelease(false); CHECK_GL_ERROR(); } - _context->doneCurrent(); + _refreshRateController->sleepThreadIfNeeded(this, currentPlugin->isHmd()); } + _context->doneCurrent(); Lock lock(_mutex); _context->moveToThread(qApp->thread()); _shutdown = false; @@ -236,6 +239,7 @@ private: bool _finishedOtherThreadOperation { false }; std::queue _newPluginQueue; gl::Context* _context { nullptr }; + std::shared_ptr _refreshRateController { nullptr }; }; bool OpenGLDisplayPlugin::activate() { @@ -689,11 +693,11 @@ void OpenGLDisplayPlugin::internalPresent() { _presentRate.increment(); } -void OpenGLDisplayPlugin::present() { +void OpenGLDisplayPlugin::present(const std::shared_ptr& refreshRateController) { auto frameId = (uint64_t)presentCount(); PROFILE_RANGE_EX(render, __FUNCTION__, 0xffffff00, frameId) uint64_t startPresent = usecTimestampNow(); - + refreshRateController->clockStartTime(); { PROFILE_RANGE_EX(render, "updateFrameData", 0xff00ff00, frameId) updateFrameData(); @@ -723,7 +727,21 @@ void OpenGLDisplayPlugin::present() { compositeLayers(); } + { // If we have any snapshots this frame, handle them + PROFILE_RANGE_EX(render, "snapshotOperators", 0xffff00ff, frameId) + while (!_currentFrame->snapshotOperators.empty()) { + auto& snapshotOperator = _currentFrame->snapshotOperators.front(); + if (std::get<2>(snapshotOperator)) { + std::get<0>(snapshotOperator)(getScreenshot(std::get<1>(snapshotOperator))); + } else { + std::get<0>(snapshotOperator)(getSecondaryCameraScreenshot()); + } + _currentFrame->snapshotOperators.pop(); + } + } + // Take the composite framebuffer and send it to the output device + refreshRateController->clockEndTime(); { PROFILE_RANGE_EX(render, "internalPresent", 0xff00ffff, frameId) internalPresent(); @@ -731,7 +749,10 @@ void OpenGLDisplayPlugin::present() { gpu::Backend::freeGPUMemSize.set(gpu::gl::getFreeDedicatedMemory()); } else if (alwaysPresent()) { + refreshRateController->clockEndTime(); internalPresent(); + } else { + refreshRateController->clockEndTime(); } _movingAveragePresent.addSample((float)(usecTimestampNow() - startPresent)); } @@ -748,6 +769,13 @@ float OpenGLDisplayPlugin::presentRate() const { return _presentRate.rate(); } +std::function OpenGLDisplayPlugin::getRefreshRateOperator() { + return [](int targetRefreshRate) { + auto refreshRateController = DependencyManager::get()->getRefreshRateController(); + refreshRateController->setRefreshRateLimitPeriod(targetRefreshRate); + }; +} + void OpenGLDisplayPlugin::resetPresentRate() { // FIXME // _presentRate = RateCounter<100>(); @@ -787,7 +815,7 @@ bool OpenGLDisplayPlugin::setDisplayTexture(const QString& name) { return !!_displayTexture; } -QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { +QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) { auto size = _compositeFramebuffer->getSize(); if (isHmd()) { size.x /= 2; @@ -803,24 +831,18 @@ QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { corner.x = round((size.x - bestSize.x) / 2.0f); corner.y = round((size.y - bestSize.y) / 2.0f); } - auto glBackend = const_cast(*this).getGLBackend(); QImage screenshot(bestSize.x, bestSize.y, QImage::Format_ARGB32); - withOtherThreadContext([&] { - glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); - }); + getGLBackend()->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); return screenshot.mirrored(false, true); } -QImage OpenGLDisplayPlugin::getSecondaryCameraScreenshot() const { +QImage OpenGLDisplayPlugin::getSecondaryCameraScreenshot() { auto textureCache = DependencyManager::get(); auto secondaryCameraFramebuffer = textureCache->getSpectatorCameraFramebuffer(); gpu::Vec4i region(0, 0, secondaryCameraFramebuffer->getWidth(), secondaryCameraFramebuffer->getHeight()); - auto glBackend = const_cast(*this).getGLBackend(); QImage screenshot(region.z, region.w, QImage::Format_ARGB32); - withOtherThreadContext([&] { - glBackend->downloadFramebuffer(secondaryCameraFramebuffer, region, screenshot); - }); + getGLBackend()->downloadFramebuffer(secondaryCameraFramebuffer, region, screenshot); return screenshot.mirrored(false, true); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 49a38ecb4c..562c5af5cf 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -29,6 +29,8 @@ namespace gpu { } } +class RefreshRateController; + class OpenGLDisplayPlugin : public DisplayPlugin { Q_OBJECT Q_PROPERTY(float hudAlpha MEMBER _hudAlpha) @@ -41,6 +43,9 @@ public: ~OpenGLDisplayPlugin(); // These must be final to ensure proper ordering of operations // between the main thread and the presentation thread + + static std::function getRefreshRateOperator(); + bool activate() override final; void deactivate() override final; bool startStandBySession() override final; @@ -60,8 +65,6 @@ public: virtual bool setDisplayTexture(const QString& name) override; virtual bool onDisplayTextureReset() { return false; }; - QImage getScreenshot(float aspectRatio = 0.0f) const override; - QImage getSecondaryCameraScreenshot() const override; float presentRate() const override; @@ -125,7 +128,7 @@ protected: void withOtherThreadContext(std::function f) const; - void present(); + void present(const std::shared_ptr& refreshRateController); virtual void swapBuffers(); ivec4 eyeViewport(Eye eye) const; @@ -185,5 +188,8 @@ protected: // be serialized through this mutex mutable Mutex _presentMutex; float _hudAlpha{ 1.0f }; + + QImage getScreenshot(float aspectRatio); + QImage getSecondaryCameraScreenshot(); }; diff --git a/libraries/display-plugins/src/display-plugins/RefreshRateController.cpp b/libraries/display-plugins/src/display-plugins/RefreshRateController.cpp new file mode 100644 index 0000000000..2d9b553163 --- /dev/null +++ b/libraries/display-plugins/src/display-plugins/RefreshRateController.cpp @@ -0,0 +1,43 @@ +// +// RefreshRateController.cpp +// libraries/display-pluging/src/display-plugin +// +// Created by Dante Ruiz on 2019-04-15. +// 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 "RefreshRateController.h" + +#include +#include + +long int hzToDurationNanoseconds(int refreshRate) { + return (int64_t) (NSECS_PER_SECOND / (quint64) refreshRate); +} + +int durationNanosecondsToHz(int64_t refreshRateLimitPeriod) { + return (int) (NSECS_PER_SECOND / (quint64) refreshRateLimitPeriod); +} + +void RefreshRateController::setRefreshRateLimitPeriod(int refreshRateLimit) { + _refreshRateLimitPeriod = hzToDurationNanoseconds(refreshRateLimit); +} + +int RefreshRateController::getRefreshRateLimitPeriod() const { + return durationNanosecondsToHz(_refreshRateLimitPeriod); +} + +void RefreshRateController::sleepThreadIfNeeded(QThread* thread, bool isHmd) { + if (!isHmd) { + static const std::chrono::nanoseconds EPSILON = std::chrono::milliseconds(1); + auto duration = std::chrono::duration_cast(_endTime - _startTime); + auto refreshRateLimitPeriod = std::chrono::nanoseconds(_refreshRateLimitPeriod); + auto sleepDuration = refreshRateLimitPeriod - (duration + EPSILON); + if (sleepDuration.count() > 0) { + thread->msleep(std::chrono::duration_cast(sleepDuration).count()); + } + } +} diff --git a/libraries/display-plugins/src/display-plugins/RefreshRateController.h b/libraries/display-plugins/src/display-plugins/RefreshRateController.h new file mode 100644 index 0000000000..15adee3d3d --- /dev/null +++ b/libraries/display-plugins/src/display-plugins/RefreshRateController.h @@ -0,0 +1,40 @@ +// +// RefreshRateController.h +// libraries/display-pluging/src/display-plugin +// +// Created by Dante Ruiz on 2019-04-15. +// 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_RefreshRateController_h +#define hifi_RefreshRateController_h + +#include + +#include +#include + +class QThread; + +class RefreshRateController { +public: + RefreshRateController() = default; + ~RefreshRateController() = default; + + void setRefreshRateLimitPeriod(int refreshRateLimit); + int getRefreshRateLimitPeriod() const; + + void clockStartTime() { _startTime = std::chrono::high_resolution_clock::now(); } + void clockEndTime() { _endTime = std::chrono::high_resolution_clock::now(); } + void sleepThreadIfNeeded(QThread* thread, bool isHmd); +private: + std::chrono::time_point _startTime { std::chrono::high_resolution_clock::now() }; + std::chrono::time_point _endTime { std::chrono::high_resolution_clock::now() }; + std::atomic _refreshRateLimitPeriod { 50 }; + +}; + +#endif diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 321bcc3fd2..874454b391 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -199,7 +199,7 @@ void HmdDisplayPlugin::internalPresent() { float newWidth = sourceSize.x - shiftLeftBy; // Experimentally adjusted the region presented in preview to avoid seeing the masked pixels and recenter the center... - static float SCALE_WIDTH = 0.9f; + static float SCALE_WIDTH = 0.8f; static float SCALE_OFFSET = 2.0f; newWidth *= SCALE_WIDTH; shiftLeftBy *= SCALE_OFFSET; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 4d2f1018a7..e952b1e8db 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -48,7 +48,7 @@ public: void pluginUpdate() override {}; - virtual StencilMode getStencilMaskMode() const override { return StencilMode::PAINT; } + virtual StencilMaskMode getStencilMaskMode() const override { return StencilMaskMode::PAINT; } signals: void hmdMountedChanged(); diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index 3a56521702..fb6fbad2ac 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -166,7 +166,10 @@ ShapeKey EntityRenderer::getShapeKey() { } render::hifi::Tag EntityRenderer::getTagMask() const { - return _isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW; + render::hifi::Tag mask = render::hifi::TAG_NONE; + mask = (render::hifi::Tag)(mask | (!_cauterized * render::hifi::TAG_MAIN_VIEW)); + mask = (render::hifi::Tag)(mask | (_isVisibleInSecondaryCamera * render::hifi::TAG_SECONDARY_VIEW)); + return mask; } render::hifi::Layer EntityRenderer::getHifiRenderLayer() const { @@ -215,12 +218,7 @@ void EntityRenderer::render(RenderArgs* args) { emit requestRenderUpdate(); } - auto& renderMode = args->_renderMode; - bool cauterized = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE && - renderMode != RenderArgs::RenderMode::SECONDARY_CAMERA_RENDER_MODE && - _cauterized); - - if (_visible && !cauterized) { + if (_visible && (args->_renderMode != RenderArgs::RenderMode::DEFAULT_RENDER_MODE || !_cauterized)) { doRender(args); } } diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index b474fb2266..9a634a85ad 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -121,7 +121,11 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo QString materialURL = entity->getMaterialURL(); if (materialURL != _materialURL) { _materialURL = materialURL; - if (_materialURL.contains("?")) { + if (_materialURL.contains("#")) { + auto split = _materialURL.split("#"); + newCurrentMaterialName = split.last().toStdString(); + } else if (_materialURL.contains("?")) { + qDebug() << "DEPRECATED: Use # instead of ? for material URLS:" << _materialURL; auto split = _materialURL.split("?"); newCurrentMaterialName = split.last().toStdString(); } @@ -309,11 +313,9 @@ void MaterialEntityRenderer::doRender(RenderArgs* args) { batch.setModelTransform(renderTransform); - if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - drawMaterial->setTextureTransforms(textureTransform, MaterialMappingMode::UV, true); - - // bind the material - RenderPipelines::bindMaterial(drawMaterial, batch, args->_enableTexturing); + drawMaterial->setTextureTransforms(textureTransform, MaterialMappingMode::UV, true); + // bind the material + if (RenderPipelines::bindMaterial(drawMaterial, batch, args->_renderMode, args->_enableTexturing)) { args->_details._materialSwitches++; } @@ -358,7 +360,13 @@ void MaterialEntityRenderer::deleteMaterial(const QUuid& oldParentID, const QStr return; } - // if a remove fails, our parent is gone, so we don't need to retry + // if a remove fails, our parent is gone, so we don't need to retry, EXCEPT: + // MyAvatar can change UUIDs when you switch domains, which leads to a timing issue. Let's just make + // sure we weren't attached to MyAvatar by trying this (if we weren't, this will have no effect) + if (EntityTreeRenderer::removeMaterialFromAvatar(AVATAR_SELF_ID, material, oldParentMaterialNameStd)) { + _appliedMaterial = nullptr; + return; + } } void MaterialEntityRenderer::applyTextureTransform(std::shared_ptr& material) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index f921f6eca6..bfbbe12ea6 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1066,13 +1066,6 @@ ItemKey ModelEntityRenderer::getKey() { return _itemKey; } -render::hifi::Tag ModelEntityRenderer::getTagMask() const { - // Default behavior for model is to not be visible in main view if cauterized (aka parented to the avatar's neck joint) - return _cauterized ? - (_isVisibleInSecondaryCamera ? render::hifi::TAG_SECONDARY_VIEW : render::hifi::TAG_NONE) : - Parent::getTagMask(); // calculate which views to be shown in -} - uint32_t ModelEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) { if (_model) { auto metaSubItems = _model->fetchRenderItemIDs(); @@ -1409,6 +1402,10 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce model->setVisibleInScene(_visible, scene); } + if (model->isCauterized() != _cauterized) { + model->setCauterized(_cauterized, scene); + } + render::hifi::Tag tagMask = getTagMask(); if (model->getTagMask() != tagMask) { model->setTagMask(tagMask, scene); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 2fd1041c5f..ee6e7d0b04 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -161,8 +161,6 @@ protected: virtual void doRender(RenderArgs* args) override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; - render::hifi::Tag getTagMask() const override; - void setIsVisibleInSecondaryCamera(bool value) override; void setRenderLayer(RenderLayer value) override; void setPrimitiveMode(PrimitiveMode value) override; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index d859d4b739..2548ae5914 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -291,8 +291,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); } } else { - if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - RenderPipelines::bindMaterials(materials, batch, args->_enableTexturing); + if (RenderPipelines::bindMaterials(materials, batch, args->_renderMode, args->_enableTexturing)) { args->_details._materialSwitches++; } diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 8a7fa3f8e7..64cca404cb 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -465,7 +465,7 @@ void ZoneEntityRenderer::setAmbientURL(const QString& ambientUrl) { } else { _pendingAmbientTexture = true; auto textureCache = DependencyManager::get(); - _ambientTexture = textureCache->getTexture(_ambientTextureURL, image::TextureUsage::CUBE_TEXTURE); + _ambientTexture = textureCache->getTexture(_ambientTextureURL, image::TextureUsage::AMBIENT_TEXTURE); // keep whatever is assigned on the ambient map/sphere until texture is loaded } @@ -506,7 +506,7 @@ void ZoneEntityRenderer::setSkyboxURL(const QString& skyboxUrl) { } else { _pendingSkyboxTexture = true; auto textureCache = DependencyManager::get(); - _skyboxTexture = textureCache->getTexture(_skyboxTextureURL, image::TextureUsage::CUBE_TEXTURE); + _skyboxTexture = textureCache->getTexture(_skyboxTextureURL, image::TextureUsage::SKY_TEXTURE); } } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 8a50c39da9..e1ede9192a 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -791,7 +791,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef bool otherOverwrites = overwriteLocalData && !weOwnSimulation; // calculate hasGrab once outside the lambda rather than calling it every time inside bool hasGrab = stillHasGrabAction(); - auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) { + auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) { if (hasGrab) { return false; } @@ -3007,6 +3007,26 @@ void EntityItem::setPrimitiveMode(PrimitiveMode value) { } } +bool EntityItem::getCauterized() const { + return resultWithReadLock([&] { + return _cauterized; + }); +} + +void EntityItem::setCauterized(bool value) { + bool changed = false; + withWriteLock([&] { + if (_cauterized != value) { + changed = true; + _cauterized = value; + } + }); + + if (changed) { + emit requestRenderUpdate(); + } +} + bool EntityItem::getIgnorePickIntersection() const { return resultWithReadLock([&] { return _ignorePickIntersection; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index c57fd16a2e..29a1a8d73c 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -303,6 +303,9 @@ public: bool getCanCastShadow() const; void setCanCastShadow(bool value); + void setCauterized(bool value); + bool getCauterized() const; + inline bool isVisible() const { return getVisible(); } inline bool isInvisible() const { return !getVisible(); } @@ -530,9 +533,6 @@ public: static QString _marketplacePublicKey; static void retrieveMarketplacePublicKey(); - void setCauterized(bool value) { _cauterized = value; } - bool getCauterized() const { return _cauterized; } - float getBoundingRadius() const { return _boundingRadius; } void setSpaceIndex(int32_t index); int32_t getSpaceIndex() const { return _spaceIndex; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 5958af66dd..2b738bc4e7 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -976,7 +976,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * by setting the entityHostType parameter in {@link Entities.addEntity} to "avatar". * Material entities render as non-scalable spheres if they don't have their parent set. * @typedef {object} Entities.EntityProperties-Material - * @property {string} materialURL="" - URL to a {@link MaterialResource}. If you append ?name to the URL, the + * @property {string} materialURL="" - URL to a {@link MaterialResource}. If you append #name to the URL, the * material with that name in the {@link MaterialResource} will be applied to the entity.
* Alternatively, set the property value to "materialData" to use the materialData property * for the {@link MaterialResource} values. @@ -2630,11 +2630,11 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr ENTITY_ITEM_MIN_FRICTION, ENTITY_ITEM_MAX_FRICTION); ADD_PROPERTY_TO_MAP(PROP_LIFETIME, Lifetime, lifetime, float); ADD_PROPERTY_TO_MAP(PROP_COLLISIONLESS, Collisionless, collisionless, bool); - ADD_PROPERTY_TO_MAP(PROP_COLLISIONLESS, unused, ignoreForCollisions, unused); // legacy support - ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collisionMask, unused); - ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collidesWith, unused); - ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, collisionsWillMove, unused); // legacy support - ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, dynamic, unused); + ADD_PROPERTY_TO_MAP(PROP_COLLISIONLESS, unused, ignoreForCollisions, bool); // legacy support + ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collisionMask, uint16_t); + ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collidesWith, uint16_t); + ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, collisionsWillMove, bool); // legacy support + ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, dynamic, bool); ADD_PROPERTY_TO_MAP(PROP_COLLISION_SOUND_URL, CollisionSoundURL, collisionSoundURL, QString); ADD_PROPERTY_TO_MAP(PROP_ACTION_DATA, ActionData, actionData, QByteArray); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index d030f4f2e4..0142f42536 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -14,6 +14,9 @@ #include +#include +#include + #include #include @@ -85,6 +88,16 @@ struct EntityPropertyInfo { QVariant maximum; }; +template +EntityPropertyInfo makePropertyInfo(EntityPropertyList p, typename std::enable_if::value>::type* = 0) { + return EntityPropertyInfo(p); +} + +template +EntityPropertyInfo makePropertyInfo(EntityPropertyList p, typename std::enable_if::value>::type* = 0) { + return EntityPropertyInfo(p, std::numeric_limits::min(), std::numeric_limits::max()); +} + /// A collection of properties of an entity item used in the scripting API. Translates between the actual properties of an /// entity and a JavaScript style hash/QScriptValue storing a set of properties. Used in scripting to set/get the complete /// set of entity item properties via JavaScript hashes/QScriptValues diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 7064f3e62e..4c989ef74e 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -416,9 +416,10 @@ inline QRect QRect_convertFromScriptValue(const QScriptValue& v, bool& isValid) T _##n; \ static T _static##N; + #define ADD_PROPERTY_TO_MAP(P, N, n, T) \ { \ - EntityPropertyInfo propertyInfo = EntityPropertyInfo(P); \ + EntityPropertyInfo propertyInfo { makePropertyInfo(P) }; \ _propertyInfos[#n] = propertyInfo; \ _enumsToPropertyStrings[P] = #n; \ } diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index f6aedac3fc..38b73aaf45 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -1550,7 +1550,7 @@ public slots: * @function Entities.getMeshes * @param {Uuid} entityID - The ID of the Model or PolyVox entity to get the meshes of. * @param {Entities~getMeshesCallback} callback - The function to call upon completion. - * @deprecated Use the {@link Graphics} API instead. + * @deprecated This function is deprecated and will be removed. Use the {@link Graphics} API instead. */ /**jsdoc * Called when {@link Entities.getMeshes} is complete. @@ -1559,7 +1559,7 @@ public slots: * Model or PolyVox entity; otherwise undefined. * @param {boolean} success - true if the {@link Entities.getMeshes} call was successful, false * otherwise. The call may be unsuccessful if the requested entity could not be found. - * @deprecated Use the {@link Graphics} API instead. + * @deprecated This function is deprecated and will be removed. Use the {@link Graphics} API instead. */ // FIXME move to a renderable entity interface Q_INVOKABLE void getMeshes(const QUuid& entityID, QScriptValue callback); diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index aab98adb52..60eaafc0dd 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -148,9 +148,13 @@ bool EntityTreeElement::checkFilterSettings(const EntityItemPointer& entity, Pic (!searchFilter.doesPickLocalEntities() && hostType == entity::HostType::LOCAL)) { return false; } - // We only check the collidable filters for non-local entities, because local entities are always collisionless - bool collidable = !entity->getCollisionless() && (entity->getShapeType() != SHAPE_TYPE_NONE); + // We only check the collidable filters for non-local entities, because local entities are always collisionless, + // but picks always include COLLIDABLE (see PickScriptingInterface::getPickFilter()), so if we were to respect + // the getCollisionless() property of Local entities then we would *never* intersect them in a pick. + // An unfortunate side effect of the following code is that Local entities are intersected even if the + // pick explicitly requested only COLLIDABLE entities (but, again, Local entities are always collisionless). if (hostType != entity::HostType::LOCAL) { + bool collidable = !entity->getCollisionless() && (entity->getShapeType() != SHAPE_TYPE_NONE); if ((collidable && !searchFilter.doesPickCollidable()) || (!collidable && !searchFilter.doesPickNonCollidable())) { return false; } diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 79bd7431cc..f8339ddd31 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -97,7 +97,7 @@ QString processID(const QString& id) { return id.mid(id.lastIndexOf(':') + 1); } -QString getName(const QVariantList& properties) { +QString getModelName(const QVariantList& properties) { QString name; if (properties.size() == 3) { name = properties.at(1).toString(); @@ -108,6 +108,17 @@ QString getName(const QVariantList& properties) { return name; } +QString getMaterialName(const QVariantList& properties) { + QString name; + if (properties.size() == 1 || properties.at(1).toString().isEmpty()) { + name = properties.at(0).toString(); + name = processID(name.left(name.indexOf(QChar('\0')))); + } else { + name = processID(properties.at(1).toString()); + } + return name; +} + QString getID(const QVariantList& properties, int index = 0) { return processID(properties.at(index).toString()); } @@ -508,7 +519,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const blendshapes.append(extracted); } } else if (object.name == "Model") { - QString name = getName(object.properties); + QString name = getModelName(object.properties); QString id = getID(object.properties); modelIDsToNames.insert(id, name); @@ -827,7 +838,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const } else if (object.name == "Material") { HFMMaterial material; MaterialParam materialParam; - material.name = (object.properties.at(1).toString()); + material.name = getMaterialName(object.properties); foreach (const FBXNode& subobject, object.children) { bool properties = false; diff --git a/libraries/fbx/src/FBXSerializer_Mesh.cpp b/libraries/fbx/src/FBXSerializer_Mesh.cpp index c34b4678c7..802db4b428 100644 --- a/libraries/fbx/src/FBXSerializer_Mesh.cpp +++ b/libraries/fbx/src/FBXSerializer_Mesh.cpp @@ -358,7 +358,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me std::vector dracoMaterialList; for (const auto& dracoChild : child.children) { if (dracoChild.name == "FBXDracoMeshVersion") { - if (!dracoChild.children.isEmpty()) { + if (!dracoChild.properties.isEmpty()) { dracoMeshNodeVersion = dracoChild.properties[0].toUInt(); } } else if (dracoChild.name == "MaterialList") { @@ -492,7 +492,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me // Figure out what material this part is if (dracoMeshNodeVersion >= 2) { // Define the materialID now - if (dracoMaterialList.size() - 1 <= materialID) { + if (materialID < dracoMaterialList.size()) { part.materialID = dracoMaterialList[materialID]; } } else { diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index fade0fa5bc..3945fe1d8b 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -32,6 +32,8 @@ static const QString JOINT_FIELD = "joint"; static const QString BLENDSHAPE_FIELD = "bs"; static const QString SCRIPT_FIELD = "script"; static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; +static const QString MATERIAL_MAPPING_FIELD = "materialMap"; +static const QString COMMENT_FIELD = "comment"; class FSTReader { public: diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 1699722215..622fb92ce7 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include "FBXSerializer.h" @@ -483,7 +484,7 @@ bool GLTFSerializer::addMesh(const QJsonObject& object) { GLTFMeshPrimitiveAttr target; foreach(const QString & tarKey, tarKeys) { int tarVal; - getIntVal(jsAttributes, tarKey, tarVal, target.defined); + getIntVal(jsTarget, tarKey, tarVal, target.defined); target.values.insert(tarKey, tarVal); } primitive.targets.push_back(target); @@ -493,7 +494,18 @@ bool GLTFSerializer::addMesh(const QJsonObject& object) { mesh.primitives.push_back(primitive); } } + } + QJsonObject jsExtras; + GLTFMeshExtra extras; + if (getObjectVal(object, "extras", jsExtras, mesh.defined)) { + QJsonArray jsTargetNames; + if (getObjectArrayVal(jsExtras, "targetNames", jsTargetNames, extras.defined)) { + foreach (const QJsonValue& tarName, jsTargetNames) { + extras.targetNames.push_back(tarName.toString()); + } + } + mesh.extras = extras; } _file.meshes.push_back(mesh); @@ -751,107 +763,112 @@ void GLTFSerializer::getSkinInverseBindMatrices(std::vector>& } } -void GLTFSerializer::getNodeQueueByDepthFirstChildren(std::vector& children, int stride, std::vector& result) { - int startingIndex = 0; - int finalIndex = (int)children.size(); - if (stride == -1) { - startingIndex = (int)children.size() - 1; - finalIndex = -1; - } - for (int index = startingIndex; index != finalIndex; index += stride) { - int c = children[index]; - result.push_back(c); - std::vector nested = _file.nodes[c].children.toStdVector(); - if (nested.size() != 0) { - std::sort(nested.begin(), nested.end()); - for (int r : nested) { - if (result.end() == std::find(result.begin(), result.end(), r)) { - getNodeQueueByDepthFirstChildren(nested, stride, result); - } - } - } +void GLTFSerializer::generateTargetData(int index, float weight, QVector& returnVector) { + GLTFAccessor& accessor = _file.accessors[index]; + GLTFBufferView& bufferview = _file.bufferviews[accessor.bufferView]; + GLTFBuffer& buffer = _file.buffers[bufferview.buffer]; + int accBoffset = accessor.defined["byteOffset"] ? accessor.byteOffset : 0; + QVector storedValues; + addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + storedValues, + accessor.type, + accessor.componentType); + for (int n = 0; n < storedValues.size(); n = n + 3) { + returnVector.push_back(glm::vec3(weight * storedValues[n], weight * storedValues[n + 1], weight * storedValues[n + 2])); } } - -bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { +bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& url) { int numNodes = _file.nodes.size(); - //Build dependencies - QVector> nodeDependencies(numNodes); + // Build dependencies + QVector parents; + QVector sortedNodes; + parents.fill(-1, numNodes); + sortedNodes.reserve(numNodes); int nodecount = 0; foreach(auto &node, _file.nodes) { - //nodes_transforms.push_back(getModelTransform(node)); - foreach(int child, node.children) nodeDependencies[child].push_back(nodecount); + foreach(int child, node.children) { + parents[child] = nodecount; + } + sortedNodes.push_back(nodecount); nodecount++; } + + // Build transforms nodecount = 0; foreach(auto &node, _file.nodes) { // collect node transform - _file.nodes[nodecount].transforms.push_back(getModelTransform(node)); - if (nodeDependencies[nodecount].size() == 1) { - int parentidx = nodeDependencies[nodecount][0]; - while (true) { // iterate parents - // collect parents transforms - _file.nodes[nodecount].transforms.push_back(getModelTransform(_file.nodes[parentidx])); - if (nodeDependencies[parentidx].size() == 1) { - parentidx = nodeDependencies[parentidx][0]; - } else break; - } + _file.nodes[nodecount].transforms.push_back(getModelTransform(node)); + int parentIndex = parents[nodecount]; + while (parentIndex != -1) { + const auto& parentNode = _file.nodes[parentIndex]; + // collect transforms for a node's parents, grandparents, etc. + _file.nodes[nodecount].transforms.push_back(getModelTransform(parentNode)); + parentIndex = parents[parentIndex]; } - nodecount++; } + + // since parent indices must exist in the sorted list before any of their children, sortedNodes might not be initialized in the correct order + // therefore we need to re-initialize the order in which nodes will be parsed + QVector hasBeenSorted; + hasBeenSorted.fill(false, numNodes); + int i = 0; // initial index + while (i < numNodes) { + int currentNode = sortedNodes[i]; + int parentIndex = parents[currentNode]; + if (parentIndex == -1 || hasBeenSorted[parentIndex]) { + hasBeenSorted[currentNode] = true; + i++; + } else { + int j = i + 1; // index of node to be sorted + while (j < numNodes) { + int nextNode = sortedNodes[j]; + parentIndex = parents[nextNode]; + if (parentIndex == -1 || hasBeenSorted[parentIndex]) { + // swap with currentNode + hasBeenSorted[nextNode] = true; + sortedNodes[i] = nextNode; + sortedNodes[j] = currentNode; + i++; + currentNode = sortedNodes[i]; + } + j++; + } + } + } - // initialize order in which nodes will be parsed - std::vector nodeQueue; - nodeQueue.reserve(numNodes); - int rootNode = 0; - int finalNode = numNodes; - if (!_file.scenes[_file.scene].nodes.contains(0)) { - rootNode = numNodes - 1; - finalNode = -1; - } - bool rootAtStartOfList = rootNode < finalNode; - int nodeListStride = 1; - if (!rootAtStartOfList) { nodeListStride = -1; } - QVector initialSceneNodes = _file.scenes[_file.scene].nodes; - std::sort(initialSceneNodes.begin(), initialSceneNodes.end()); - int sceneRootNode = 0; - int sceneFinalNode = initialSceneNodes.size(); - if (!rootAtStartOfList) { - sceneRootNode = initialSceneNodes.size() - 1; - sceneFinalNode = -1; - } - for (int index = sceneRootNode; index != sceneFinalNode; index += nodeListStride) { - int i = initialSceneNodes[index]; - nodeQueue.push_back(i); - std::vector children = _file.nodes[i].children.toStdVector(); - std::sort(children.begin(), children.end()); - getNodeQueueByDepthFirstChildren(children, nodeListStride, nodeQueue); + // Build map from original to new indices + QVector originalToNewNodeIndexMap; + originalToNewNodeIndexMap.fill(-1, numNodes); + for (int i = 0; i < numNodes; i++) { + originalToNewNodeIndexMap[sortedNodes[i]] = i; } + // Build joints HFMJoint joint; joint.distanceToParent = 0; hfmModel.jointIndices["x"] = numNodes; - hfmModel.hasSkeletonJoints = false; - for (int nodeIndex : nodeQueue) { + for (int nodeIndex : sortedNodes) { auto& node = _file.nodes[nodeIndex]; - joint.parentIndex = -1; - if (!_file.scenes[_file.scene].nodes.contains(nodeIndex)) { - joint.parentIndex = std::distance(nodeQueue.begin(), std::find(nodeQueue.begin(), nodeQueue.end(), nodeDependencies[nodeIndex][0])); + joint.parentIndex = parents[nodeIndex]; + if (joint.parentIndex != -1) { + joint.parentIndex = originalToNewNodeIndexMap[joint.parentIndex]; } joint.transform = node.transforms.first(); joint.translation = extractTranslation(joint.transform); joint.rotation = glmExtractRotation(joint.transform); glm::vec3 scale = extractScale(joint.transform); - joint.postTransform = glm::scale(glm::mat4(), scale); + joint.postTransform = glm::scale(glm::mat4(), scale); joint.name = node.name; joint.isSkeletonJoint = false; @@ -862,24 +879,25 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { // Build skeleton std::vector jointInverseBindTransforms; jointInverseBindTransforms.resize(numNodes); - if (!_file.skins.isEmpty()) { + hfmModel.hasSkeletonJoints = !_file.skins.isEmpty(); + if (hfmModel.hasSkeletonJoints) { + hfmModel.hasSkeletonJoints = true; std::vector> inverseBindValues; getSkinInverseBindMatrices(inverseBindValues); - int jointIndex = finalNode; - while (jointIndex != rootNode) { - rootAtStartOfList ? jointIndex-- : jointIndex++; - int jOffset = nodeQueue[jointIndex]; + for (int jointIndex = 0; jointIndex < numNodes; jointIndex++) { + int nodeIndex = sortedNodes[jointIndex]; auto joint = hfmModel.joints[jointIndex]; - hfmModel.hasSkeletonJoints = true; for (int s = 0; s < _file.skins.size(); s++) { - auto skin = _file.skins[s]; - joint.isSkeletonJoint = skin.joints.contains(jOffset); + const auto& skin = _file.skins[s]; + int matrixIndex = skin.joints.indexOf(nodeIndex); + joint.isSkeletonJoint = skin.joints.contains(nodeIndex); + // build inverse bind matrices if (joint.isSkeletonJoint) { - std::vector value = inverseBindValues[s]; - int matrixCount = 16 * skin.joints.indexOf(jOffset); + std::vector& value = inverseBindValues[s]; + int matrixCount = 16 * matrixIndex; jointInverseBindTransforms[jointIndex] = glm::mat4(value[matrixCount], value[matrixCount + 1], value[matrixCount + 2], value[matrixCount + 3], value[matrixCount + 4], value[matrixCount + 5], value[matrixCount + 6], value[matrixCount + 7], @@ -896,7 +914,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } - //Build materials + // Build materials QVector materialIDs; QString unknown = "Default"; int ukcount = 0; @@ -916,22 +934,21 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { // Build meshes nodecount = 0; - for (int nodeIndex = rootNode; nodeIndex != finalNode; nodeIndex += nodeListStride) { + for (int nodeIndex : sortedNodes) { auto& node = _file.nodes[nodeIndex]; if (node.defined["mesh"]) { - qCDebug(modelformat) << "node_transforms" << node.transforms; foreach(auto &primitive, _file.meshes[node.mesh].primitives) { hfmModel.meshes.append(HFMMesh()); HFMMesh& mesh = hfmModel.meshes[hfmModel.meshes.size() - 1]; - if (!hfmModel.hasSkeletonJoints) { + if (!hfmModel.hasSkeletonJoints) { HFMCluster cluster; cluster.jointIndex = nodecount; cluster.inverseBindMatrix = glm::mat4(); cluster.inverseBindTransform = Transform(cluster.inverseBindMatrix); mesh.clusters.append(cluster); - } else { - for (int j = rootNode; j != finalNode; j += nodeListStride) { + } else { // skinned model + for (int j = 0; j < numNodes; j++) { HFMCluster cluster; cluster.jointIndex = j; cluster.inverseBindMatrix = jointInverseBindTransforms[j]; @@ -940,10 +957,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } } HFMCluster root; - root.jointIndex = rootNode; - if (root.jointIndex == -1) { - root.jointIndex = 0; - } + root.jointIndex = 0; root.inverseBindMatrix = jointInverseBindTransforms[root.jointIndex]; root.inverseBindTransform = Transform(root.inverseBindMatrix); mesh.clusters.append(root); @@ -1043,6 +1057,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { qWarning(modelformat) << "There was a problem reading glTF TANGENT data for model " << _url; continue; } + // tangents can be a vec3 or a vec4 which includes a w component (of -1 or 1) 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; @@ -1111,7 +1126,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { } } - // adapted from FBXSerializer.cpp + // Build weights (adapted from FBXSerializer.cpp) if (hfmModel.hasSkeletonJoints) { int numClusterIndices = clusterJoints.size(); const int WEIGHTS_PER_VERTEX = 4; @@ -1121,7 +1136,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { mesh.clusterWeights.fill(0, numClusterIndices); for (int c = 0; c < clusterJoints.size(); c++) { - mesh.clusterIndices[c] = _file.skins[node.skin].joints[clusterJoints[c]]; + mesh.clusterIndices[c] = originalToNewNodeIndexMap[_file.skins[node.skin].joints[clusterJoints[c]]]; } // normalize and compress to 16-bits @@ -1152,6 +1167,82 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { if (mesh.texCoords.size() == 0 && !hfmModel.hasSkeletonJoints) { for (int i = 0; i < part.triangleIndices.size(); i++) { mesh.texCoords.push_back(glm::vec2(0.0, 1.0)); } } + + // Build morph targets (blend shapes) + if (!primitive.targets.isEmpty()) { + + // Build list of blendshapes from FST + typedef QPair WeightedIndex; + hifi::VariantHash blendshapeMappings = mapping.value("bs").toHash(); + QMultiHash blendshapeIndices; + + for (int i = 0;; i++) { + hifi::ByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i]; + if (blendshapeName.isEmpty()) { + break; + } + QList mappings = blendshapeMappings.values(blendshapeName); + foreach (const QVariant& mapping, mappings) { + QVariantList blendshapeMapping = mapping.toList(); + blendshapeIndices.insert(blendshapeMapping.at(0).toByteArray(), WeightedIndex(i, blendshapeMapping.at(1).toFloat())); + } + } + + // glTF morph targets may or may not have names. if they are labeled, add them based on + // the corresponding names from the FST. otherwise, just add them in the order they are given + mesh.blendshapes.resize(blendshapeMappings.size()); + auto values = blendshapeIndices.values(); + auto keys = blendshapeIndices.keys(); + auto names = _file.meshes[node.mesh].extras.targetNames; + QVector weights = _file.meshes[node.mesh].weights; + + for (int weightedIndex = 0; weightedIndex < values.size(); weightedIndex++) { + float weight = 0.1f; + int indexFromMapping = weightedIndex; + int targetIndex = weightedIndex; + hfmModel.blendshapeChannelNames.push_back("target_" + QString::number(weightedIndex)); + + if (!names.isEmpty()) { + targetIndex = names.indexOf(keys[weightedIndex]); + indexFromMapping = values[weightedIndex].first; + weight = weight * values[weightedIndex].second; + hfmModel.blendshapeChannelNames[weightedIndex] = keys[weightedIndex]; + } + HFMBlendshape& blendshape = mesh.blendshapes[indexFromMapping]; + blendshape.indices = part.triangleIndices; + auto target = primitive.targets[targetIndex]; + + QVector normals; + QVector vertices; + + if (weights.size() == primitive.targets.size()) { + int targetWeight = weights[targetIndex]; + if (targetWeight != 0) { + weight = weight * targetWeight; + } + } + + if (target.values.contains((QString) "NORMAL")) { + generateTargetData(target.values.value((QString) "NORMAL"), weight, normals); + } + if (target.values.contains((QString) "POSITION")) { + generateTargetData(target.values.value((QString) "POSITION"), weight, vertices); + } + bool isNewBlendshape = blendshape.vertices.size() < vertices.size(); + int count = 0; + for (int i : blendshape.indices) { + if (isNewBlendshape) { + blendshape.vertices.push_back(vertices[i]); + blendshape.normals.push_back(normals[i]); + } else { + blendshape.vertices[count] = blendshape.vertices[count] + vertices[i]; + blendshape.normals[count] = blendshape.normals[count] + normals[i]; + count++; + } + } + } + } + mesh.meshExtents.reset(); foreach(const glm::vec3& vertex, mesh.vertices) { mesh.meshExtents.addPoint(vertex); @@ -1160,12 +1251,10 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { mesh.meshIndex = hfmModel.meshes.size(); } - } nodecount++; } - return true; } @@ -1199,7 +1288,7 @@ HFMModel::Pointer GLTFSerializer::read(const hifi::ByteArray& data, const hifi:: //_file.dump(); auto hfmModelPtr = std::make_shared(); HFMModel& hfmModel = *hfmModelPtr; - buildGeometry(hfmModel, _url); + buildGeometry(hfmModel, mapping, _url); //hfmDebugDump(data); return hfmModelPtr; @@ -1291,7 +1380,6 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) { QString fname = hifi::URL(url).fileName(); hifi::URL textureUrl = _url.resolved(url); - qCDebug(modelformat) << "fname: " << fname; fbxtex.name = fname; fbxtex.filename = textureUrl.toEncoded(); @@ -1385,10 +1473,7 @@ bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int c blobstream.setByteOrder(QDataStream::LittleEndian); blobstream.setVersion(QDataStream::Qt_5_9); blobstream.setFloatingPointPrecision(QDataStream::FloatingPointPrecision::SinglePrecision); - - qCDebug(modelformat) << "size1: " << count; - int dataskipped = blobstream.skipRawData(byteOffset); - qCDebug(modelformat) << "dataskipped: " << dataskipped; + blobstream.skipRawData(byteOffset); int bufferCount = 0; switch (accessorType) { @@ -1482,6 +1567,38 @@ void GLTFSerializer::retriangulate(const QVector& inIndices, const QVector< } } +void GLTFSerializer::glTFDebugDump() { + qCDebug(modelformat) << "---------------- Nodes ----------------"; + for (GLTFNode node : _file.nodes) { + if (node.defined["mesh"]) { + qCDebug(modelformat) << "\n"; + qCDebug(modelformat) << " node_transforms" << node.transforms; + qCDebug(modelformat) << "\n"; + } + } + + qCDebug(modelformat) << "---------------- Accessors ----------------"; + for (GLTFAccessor accessor : _file.accessors) { + qCDebug(modelformat) << "\n"; + qCDebug(modelformat) << "count: " << accessor.count; + qCDebug(modelformat) << "byteOffset: " << accessor.byteOffset; + qCDebug(modelformat) << "\n"; + } + + qCDebug(modelformat) << "---------------- Textures ----------------"; + for (GLTFTexture texture : _file.textures) { + if (texture.defined["source"]) { + qCDebug(modelformat) << "\n"; + QString url = _file.images[texture.source].uri; + QString fname = hifi::URL(url).fileName(); + qCDebug(modelformat) << "fname: " << fname; + qCDebug(modelformat) << "\n"; + } + } + + qCDebug(modelformat) << "\n"; +} + void GLTFSerializer::hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << "---------------- hfmModel ----------------"; qCDebug(modelformat) << " hasSkeletonJoints =" << hfmModel.hasSkeletonJoints; @@ -1622,5 +1739,8 @@ void GLTFSerializer::hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << "\n"; } + qCDebug(modelformat) << "---------------- GLTF Model ----------------"; + glTFDebugDump(); + qCDebug(modelformat) << "\n"; } diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index d9c477bd99..b1c1bc4e44 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -159,9 +159,20 @@ struct GLTFMeshPrimitive { } }; +struct GLTFMeshExtra { + QVector targetNames; + QMap defined; + void dump() { + if (defined["targetNames"]) { + qCDebug(modelformat) << "targetNames: " << targetNames; + } + } +}; + struct GLTFMesh { QString name; QVector primitives; + GLTFMeshExtra extras; QVector weights; QMap defined; void dump() { @@ -172,6 +183,10 @@ struct GLTFMesh { qCDebug(modelformat) << "primitives: "; foreach(auto prim, primitives) prim.dump(); } + if (defined["extras"]) { + qCDebug(modelformat) << "extras: "; + extras.dump(); + } if (defined["weights"]) { qCDebug(modelformat) << "weights: " << weights; } @@ -713,9 +728,9 @@ private: glm::mat4 getModelTransform(const GLTFNode& node); void getSkinInverseBindMatrices(std::vector>& inverseBindMatrixValues); - void getNodeQueueByDepthFirstChildren(std::vector& children, int stride, std::vector& result); + void generateTargetData(int index, float weight, QVector& returnVector); - bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url); + bool buildGeometry(HFMModel& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& url); bool parseGLTF(const hifi::ByteArray& data); bool getStringVal(const QJsonObject& object, const QString& fieldname, @@ -785,6 +800,7 @@ private: void setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& material); HFMTexture getHFMTexture(const GLTFTexture& texture); + void glTFDebugDump(); void hfmDebugDump(const HFMModel& hfmModel); }; diff --git a/libraries/fbx/src/OBJSerializer.cpp b/libraries/fbx/src/OBJSerializer.cpp index c2e9c08463..416f343a47 100644 --- a/libraries/fbx/src/OBJSerializer.cpp +++ b/libraries/fbx/src/OBJSerializer.cpp @@ -891,12 +891,14 @@ HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::V if (!objMaterial.used) { continue; } - hfmModel.materials[materialID] = HFMMaterial(objMaterial.diffuseColor, - objMaterial.specularColor, - objMaterial.emissiveColor, - objMaterial.shininess, - objMaterial.opacity); - HFMMaterial& hfmMaterial = hfmModel.materials[materialID]; + + HFMMaterial& hfmMaterial = hfmModel.materials[materialID] = HFMMaterial(objMaterial.diffuseColor, + objMaterial.specularColor, + objMaterial.emissiveColor, + objMaterial.shininess, + objMaterial.opacity); + + hfmMaterial.name = materialID; hfmMaterial.materialID = materialID; hfmMaterial._material = std::make_shared(); graphics::MaterialPointer modelMaterial = hfmMaterial._material; diff --git a/libraries/gpu/src/gpu/Frame.h b/libraries/gpu/src/gpu/Frame.h index 3787ebfacd..9aa99e50f7 100644 --- a/libraries/gpu/src/gpu/Frame.h +++ b/libraries/gpu/src/gpu/Frame.h @@ -9,6 +9,7 @@ #define hifi_gpu_Frame_h #include +#include #include "Forward.h" #include "Batch.h" @@ -41,6 +42,8 @@ namespace gpu { /// How to process the framebuffer when the frame dies. MUST BE THREAD SAFE FramebufferRecycler framebufferRecycler; + std::queue, float, bool>> snapshotOperators; + protected: friend class Deserializer; diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 5c2e181810..1edd7d33cf 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -37,7 +37,6 @@ ContextMetricCount Texture::_textureCPUCount; ContextMetricSize Texture::_textureCPUMemSize; std::atomic Texture::_allowedCPUMemoryUsage { 0 }; - #define MIN_CORES_FOR_INCREMENTAL_TEXTURES 5 bool recommendedSparseTextures = (QThread::idealThreadCount() >= MIN_CORES_FOR_INCREMENTAL_TEXTURES); diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 26ff86af9c..5e2485941d 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -550,7 +550,7 @@ public: void setUsage(const Usage& usage) { _usage = usage; } Usage getUsage() const { return _usage; } - // For Cube Texture, it's possible to generate the irradiance spherical harmonics and make them availalbe with the texture + // For Cube Texture, it's possible to generate the irradiance spherical harmonics and make them available with the texture bool generateIrradiance(gpu::BackendTarget target); const SHPointer& getIrradiance(uint16 slice = 0) const { return _irradiance; } void overrideIrradiance(SHPointer irradiance) { _irradiance = irradiance; } diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h index d2d330167d..6d1b9d83d2 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h +++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h @@ -59,8 +59,8 @@ namespace scriptable { * @property {string} occlusionMap * @property {string} lightmapMap * @property {string} scatteringMap - * @property {string} texCoordTransform0 - * @property {string} texCoordTransform1 + * @property {Mat4|string} texCoordTransform0 + * @property {Mat4|string} texCoordTransform1 * @property {string} lightmapParams * @property {string} materialParams * @property {boolean} defaultFallthrough @@ -93,6 +93,7 @@ namespace scriptable { QString occlusionMap; QString lightmapMap; QString scatteringMap; + std::array texCoordTransforms; bool defaultFallthrough; std::unordered_map propertyFallthroughs; // not actually exposed to script diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 3bd4af601c..eb4bfa197c 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -470,9 +470,13 @@ namespace scriptable { // These need to be implemented, but set the fallthrough for now if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { obj.setProperty("texCoordTransform0", FALLTHROUGH); + } else if (material.texCoordTransforms[0] != mat4()) { + obj.setProperty("texCoordTransform0", mat4toScriptValue(engine, material.texCoordTransforms[0])); } if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { obj.setProperty("texCoordTransform1", FALLTHROUGH); + } else if (material.texCoordTransforms[1] != mat4()) { + obj.setProperty("texCoordTransform1", mat4toScriptValue(engine, material.texCoordTransforms[1])); } if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { obj.setProperty("lightmapParams", FALLTHROUGH); diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp index caee4ceb2a..8825a26bfe 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp @@ -119,6 +119,10 @@ scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPoint if (map && map->getTextureSource()) { scatteringMap = map->getTextureSource()->getUrl().toString(); } + + for (int i = 0; i < graphics::Material::NUM_TEXCOORD_TRANSFORMS; i++) { + texCoordTransforms[i] = material->getTexCoordTransform(i); + } } } diff --git a/libraries/graphics/src/graphics/Geometry.cpp b/libraries/graphics/src/graphics/Geometry.cpp index cee2b0e3c9..a983ba07b4 100755 --- a/libraries/graphics/src/graphics/Geometry.cpp +++ b/libraries/graphics/src/graphics/Geometry.cpp @@ -362,6 +362,7 @@ MeshPointer Mesh::createIndexedTriangles_P3F(uint32_t numVertices, uint32_t numI mesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(numIndices * sizeof(uint32_t), (gpu::Byte*) indices), gpu::Element::INDEX_INT32)); } + std::vector parts; parts.push_back(graphics::Mesh::Part(0, numIndices, 0, graphics::Mesh::TRIANGLES)); mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(graphics::Mesh::Part), (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); diff --git a/libraries/graphics/src/graphics/Light.cpp b/libraries/graphics/src/graphics/Light.cpp index 76d8a6030a..8a7281880e 100755 --- a/libraries/graphics/src/graphics/Light.cpp +++ b/libraries/graphics/src/graphics/Light.cpp @@ -73,6 +73,22 @@ bool Light::getCastShadows() const { return _castShadows; } +void Light::setShadowsMaxDistance(const float maxDistance) { + _shadowsMaxDistance = std::max(0.0f, maxDistance); +} + +float Light::getShadowsMaxDistance() const { + return _shadowsMaxDistance; +} + +void Light::setShadowsBiasScale(const float scale) { + _shadowsBiasScale = std::max(0.0f, scale); +} + +float Light::getShadowsBiasScale() const { + return _shadowsBiasScale; +} + void Light::setColor(const Color& color) { _lightSchemaBuffer.edit().irradiance.color = color; updateLightRadius(); diff --git a/libraries/graphics/src/graphics/Light.h b/libraries/graphics/src/graphics/Light.h index bb9fb3e5b9..824a9138c0 100755 --- a/libraries/graphics/src/graphics/Light.h +++ b/libraries/graphics/src/graphics/Light.h @@ -106,6 +106,12 @@ public: void setCastShadows(const bool castShadows); bool getCastShadows() const; + void setShadowsMaxDistance(const float maxDistance); + float getShadowsMaxDistance() const; + + void setShadowsBiasScale(const float scale); + float getShadowsBiasScale() const; + void setOrientation(const Quat& orientation); const glm::quat& getOrientation() const { return _transform.getRotation(); } @@ -192,10 +198,11 @@ protected: Type _type { SUN }; float _spotCos { -1.0f }; // stored here to be able to reset the spot angle when turning the type spot on/off - void updateLightRadius(); - + float _shadowsMaxDistance{ 40.0f }; + float _shadowsBiasScale{ 1.0f }; bool _castShadows{ false }; + void updateLightRadius(); }; typedef std::shared_ptr< Light > LightPointer; diff --git a/libraries/graphics/src/graphics/Material.h b/libraries/graphics/src/graphics/Material.h index d24e906f98..330feaa61c 100755 --- a/libraries/graphics/src/graphics/Material.h +++ b/libraries/graphics/src/graphics/Material.h @@ -318,11 +318,13 @@ public: void setTextureTransforms(const Transform& transform, MaterialMappingMode mode, bool repeat); const std::string& getName() const { return _name; } + void setName(const std::string& name) { _name = name; } const std::string& getModel() const { return _model; } void setModel(const std::string& model) { _model = model; } glm::mat4 getTexCoordTransform(uint i) const { return _texcoordTransforms[i]; } + void setTexCoordTransform(uint i, const glm::mat4& mat4) { _texcoordTransforms[i] = mat4; } glm::vec2 getLightmapParams() const { return _lightmapParams; } glm::vec2 getMaterialParams() const { return _materialParams; } diff --git a/libraries/graphics/src/graphics/MaterialTextures.slh b/libraries/graphics/src/graphics/MaterialTextures.slh index c725aae9bb..92e76e5736 100644 --- a/libraries/graphics/src/graphics/MaterialTextures.slh +++ b/libraries/graphics/src/graphics/MaterialTextures.slh @@ -149,7 +149,6 @@ float fetchScatteringMap(vec2 uv) { <@endfunc@> - <@func fetchMaterialTexturesCoord0(matKey, texcoord0, albedo, roughness, normal, metallic, emissive, scattering)@> if (getTexMapArray()._materialParams.y != 1.0 && clamp(<$texcoord0$>, vec2(0.0), vec2(1.0)) != <$texcoord0$>) { discard; diff --git a/libraries/hfm/src/hfm/HFM.cpp b/libraries/hfm/src/hfm/HFM.cpp index e930f30d1a..236445bfda 100644 --- a/libraries/hfm/src/hfm/HFM.cpp +++ b/libraries/hfm/src/hfm/HFM.cpp @@ -166,7 +166,9 @@ void HFMModel::computeKdops() { glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3) }; - + if (joints.size() != (int)shapeVertices.size()) { + return; + } // now that all joints have been scanned compute a k-Dop bounding volume of mesh for (int i = 0; i < joints.size(); ++i) { HFMJoint& joint = joints[i]; diff --git a/libraries/image/CMakeLists.txt b/libraries/image/CMakeLists.txt index 0c733ae789..62f48f66e2 100644 --- a/libraries/image/CMakeLists.txt +++ b/libraries/image/CMakeLists.txt @@ -2,6 +2,7 @@ set(TARGET_NAME image) setup_hifi_library() link_hifi_libraries(shared gpu) target_nvtt() +target_tbb() target_etc2comp() target_openexr() diff --git a/libraries/image/src/image/CubeMap.cpp b/libraries/image/src/image/CubeMap.cpp new file mode 100644 index 0000000000..9196377daa --- /dev/null +++ b/libraries/image/src/image/CubeMap.cpp @@ -0,0 +1,660 @@ +// +// CubeMap.h +// image/src/image +// +// Created by Olivier Prat on 03/27/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 "CubeMap.h" + +#include +#include + +#include "RandomAndNoise.h" +#include "BRDF.h" +#include "ImageLogging.h" + +#ifndef M_PI +#define M_PI 3.14159265359 +#endif + +#include + +using namespace image; + +static const glm::vec3 FACE_NORMALS[24] = { + // POSITIVE X + glm::vec3(1.0f, 1.0f, 1.0f), + glm::vec3(1.0f, 1.0f, -1.0f), + glm::vec3(1.0f, -1.0f, 1.0f), + glm::vec3(1.0f, -1.0f, -1.0f), + // NEGATIVE X + glm::vec3(-1.0f, 1.0f, -1.0f), + glm::vec3(-1.0f, 1.0f, 1.0f), + glm::vec3(-1.0f, -1.0f, -1.0f), + glm::vec3(-1.0f, -1.0f, 1.0f), + // POSITIVE Y + glm::vec3(-1.0f, 1.0f, -1.0f), + glm::vec3(1.0f, 1.0f, -1.0f), + glm::vec3(-1.0f, 1.0f, 1.0f), + glm::vec3(1.0f, 1.0f, 1.0f), + // NEGATIVE Y + glm::vec3(-1.0f, -1.0f, 1.0f), + glm::vec3(1.0f, -1.0f, 1.0f), + glm::vec3(-1.0f, -1.0f, -1.0f), + glm::vec3(1.0f, -1.0f, -1.0f), + // POSITIVE Z + glm::vec3(-1.0f, 1.0f, 1.0f), + glm::vec3(1.0f, 1.0f, 1.0f), + glm::vec3(-1.0f, -1.0f, 1.0f), + glm::vec3(1.0f, -1.0f, 1.0f), + // NEGATIVE Z + glm::vec3(1.0f, 1.0f, -1.0f), + glm::vec3(-1.0f, 1.0f, -1.0f), + glm::vec3(1.0f, -1.0f, -1.0f), + glm::vec3(-1.0f, -1.0f, -1.0f) +}; + +struct CubeFaceMip { + + CubeFaceMip(gpu::uint16 level, const CubeMap* cubemap) { + _dims = cubemap->getMipDimensions(level); + _lineStride = cubemap->getMipLineStride(level); + } + + CubeFaceMip(const CubeFaceMip& other) : _dims(other._dims), _lineStride(other._lineStride) { + + } + + gpu::Vec2i _dims; + size_t _lineStride; +}; + +class CubeMap::ConstMip : public CubeFaceMip { +public: + + ConstMip(gpu::uint16 level, const CubeMap* cubemap) : + CubeFaceMip(level, cubemap), _faces(cubemap->_mips[level]) { + } + + glm::vec4 fetch(int face, glm::vec2 uv) const { + glm::vec2 coordFrac = uv * glm::vec2(_dims) - 0.5f; + glm::vec2 coords = glm::floor(coordFrac); + + coordFrac -= coords; + + coords += (float)EDGE_WIDTH; + + const auto& pixels = _faces[face]; + gpu::Vec2i loCoords(coords); + gpu::Vec2i hiCoords; + + hiCoords = glm::clamp(loCoords + 1, gpu::Vec2i(0, 0), _dims - 1 + (int)EDGE_WIDTH); + loCoords = glm::clamp(loCoords, gpu::Vec2i(0, 0), _dims - 1 + (int)EDGE_WIDTH); + + const size_t offsetLL = loCoords.x + loCoords.y * _lineStride; + const size_t offsetHL = hiCoords.x + loCoords.y * _lineStride; + const size_t offsetLH = loCoords.x + hiCoords.y * _lineStride; + const size_t offsetHH = hiCoords.x + hiCoords.y * _lineStride; + assert(offsetLL >= 0 && offsetLL < _lineStride * (_dims.y + 2 * EDGE_WIDTH)); + assert(offsetHL >= 0 && offsetHL < _lineStride * (_dims.y + 2 * EDGE_WIDTH)); + assert(offsetLH >= 0 && offsetLH < _lineStride * (_dims.y + 2 * EDGE_WIDTH)); + assert(offsetHH >= 0 && offsetHH < _lineStride * (_dims.y + 2 * EDGE_WIDTH)); + glm::vec4 colorLL = pixels[offsetLL]; + glm::vec4 colorHL = pixels[offsetHL]; + glm::vec4 colorLH = pixels[offsetLH]; + glm::vec4 colorHH = pixels[offsetHH]; + + colorLL += (colorHL - colorLL) * coordFrac.x; + colorLH += (colorHH - colorLH) * coordFrac.x; + return colorLL + (colorLH - colorLL) * coordFrac.y; + } + +private: + + const Faces& _faces; + +}; + +class CubeMap::Mip : public CubeFaceMip { +public: + + explicit Mip(gpu::uint16 level, CubeMap* cubemap) : + CubeFaceMip(level, cubemap), _faces(cubemap->_mips[level]) { + } + + Mip(const Mip& other) : CubeFaceMip(other), _faces(other._faces) { + } + + void applySeams() { + if (EDGE_WIDTH == 0) { + return; + } + + // Copy edge rows and columns from neighbouring faces to fix seam filtering issues + seamColumnAndRow(gpu::Texture::CUBE_FACE_TOP_POS_Y, _dims.x, gpu::Texture::CUBE_FACE_RIGHT_POS_X, -1, -1); + seamColumnAndRow(gpu::Texture::CUBE_FACE_BOTTOM_NEG_Y, _dims.x, gpu::Texture::CUBE_FACE_RIGHT_POS_X, _dims.y, 1); + seamColumnAndColumn(gpu::Texture::CUBE_FACE_FRONT_NEG_Z, -1, gpu::Texture::CUBE_FACE_RIGHT_POS_X, _dims.x, 1); + seamColumnAndColumn(gpu::Texture::CUBE_FACE_BACK_POS_Z, _dims.x, gpu::Texture::CUBE_FACE_RIGHT_POS_X, -1, 1); + + seamRowAndRow(gpu::Texture::CUBE_FACE_BACK_POS_Z, -1, gpu::Texture::CUBE_FACE_TOP_POS_Y, _dims.y, 1); + seamRowAndRow(gpu::Texture::CUBE_FACE_BACK_POS_Z, _dims.y, gpu::Texture::CUBE_FACE_BOTTOM_NEG_Y, -1, 1); + seamColumnAndColumn(gpu::Texture::CUBE_FACE_BACK_POS_Z, -1, gpu::Texture::CUBE_FACE_LEFT_NEG_X, _dims.x, 1); + + seamRowAndRow(gpu::Texture::CUBE_FACE_TOP_POS_Y, -1, gpu::Texture::CUBE_FACE_FRONT_NEG_Z, -1, -1); + seamColumnAndRow(gpu::Texture::CUBE_FACE_TOP_POS_Y, -1, gpu::Texture::CUBE_FACE_LEFT_NEG_X, -1, 1); + + seamColumnAndColumn(gpu::Texture::CUBE_FACE_LEFT_NEG_X, -1, gpu::Texture::CUBE_FACE_FRONT_NEG_Z, _dims.x, 1); + seamColumnAndRow(gpu::Texture::CUBE_FACE_BOTTOM_NEG_Y, -1, gpu::Texture::CUBE_FACE_LEFT_NEG_X, _dims.y, -1); + + seamRowAndRow(gpu::Texture::CUBE_FACE_FRONT_NEG_Z, _dims.y, gpu::Texture::CUBE_FACE_BOTTOM_NEG_Y, _dims.y, -1); + + // Duplicate corner pixels + for (int face = 0; face < 6; face++) { + auto& pixels = _faces[face]; + + pixels[0] = pixels[1]; + pixels[_dims.x + 1] = pixels[_dims.x]; + pixels[(_dims.y + 1)*(_dims.x + 2)] = pixels[(_dims.y + 1)*(_dims.x + 2) + 1]; + pixels[(_dims.y + 2)*(_dims.x + 2) - 1] = pixels[(_dims.y + 2)*(_dims.x + 2) - 2]; + } + } + +private: + + Faces& _faces; + + inline static void copy(CubeMap::Face::const_iterator srcFirst, CubeMap::Face::const_iterator srcLast, size_t srcStride, CubeMap::Face::iterator dstBegin, size_t dstStride) { + while (srcFirst <= srcLast) { + *dstBegin = *srcFirst; + srcFirst += srcStride; + dstBegin += dstStride; + } + } + + static std::pair getSrcAndDst(int dim, int value) { + int src; + int dst; + + if (value < 0) { + src = 1; + dst = 0; + } else if (value >= dim) { + src = dim; + dst = dim + 1; + } + return std::make_pair(src, dst); + } + + void seamColumnAndColumn(int face0, int col0, int face1, int col1, int inc) { + auto coords0 = getSrcAndDst(_dims.x, col0); + auto coords1 = getSrcAndDst(_dims.x, col1); + + copyColumnToColumn(face0, coords0.first, face1, coords1.second, inc); + copyColumnToColumn(face1, coords1.first, face0, coords0.second, inc); + } + + void seamColumnAndRow(int face0, int col0, int face1, int row1, int inc) { + auto coords0 = getSrcAndDst(_dims.x, col0); + auto coords1 = getSrcAndDst(_dims.y, row1); + + copyColumnToRow(face0, coords0.first, face1, coords1.second, inc); + copyRowToColumn(face1, coords1.first, face0, coords0.second, inc); + } + + void seamRowAndRow(int face0, int row0, int face1, int row1, int inc) { + auto coords0 = getSrcAndDst(_dims.y, row0); + auto coords1 = getSrcAndDst(_dims.y, row1); + + copyRowToRow(face0, coords0.first, face1, coords1.second, inc); + copyRowToRow(face1, coords1.first, face0, coords0.second, inc); + } + + void copyColumnToColumn(int srcFace, int srcCol, int dstFace, int dstCol, const int dstInc) { + const auto lastOffset = _lineStride * (_dims.y - 1); + auto srcFirst = _faces[srcFace].begin() + srcCol + _lineStride; + auto srcLast = srcFirst + lastOffset; + + auto dstFirst = _faces[dstFace].begin() + dstCol + _lineStride; + auto dstLast = dstFirst + lastOffset; + const auto dstStride = _lineStride * dstInc; + + assert(srcFirst < _faces[srcFace].end()); + assert(srcLast < _faces[srcFace].end()); + assert(dstFirst < _faces[dstFace].end()); + assert(dstLast < _faces[dstFace].end()); + + if (dstInc < 0) { + std::swap(dstFirst, dstLast); + } + + copy(srcFirst, srcLast, _lineStride, dstFirst, dstStride); + } + + void copyRowToRow(int srcFace, int srcRow, int dstFace, int dstRow, const int dstInc) { + const auto lastOffset =(_dims.x - 1); + auto srcFirst = _faces[srcFace].begin() + srcRow * _lineStride + 1; + auto srcLast = srcFirst + lastOffset; + + auto dstFirst = _faces[dstFace].begin() + dstRow * _lineStride + 1; + auto dstLast = dstFirst + lastOffset; + + assert(srcFirst < _faces[srcFace].end()); + assert(srcLast < _faces[srcFace].end()); + assert(dstFirst < _faces[dstFace].end()); + assert(dstLast < _faces[dstFace].end()); + + if (dstInc < 0) { + std::swap(dstFirst, dstLast); + } + + copy(srcFirst, srcLast, 1, dstFirst, dstInc); + } + + void copyColumnToRow(int srcFace, int srcCol, int dstFace, int dstRow, int dstInc) { + const auto srcLastOffset = _lineStride * (_dims.y - 1); + auto srcFirst = _faces[srcFace].begin() + srcCol + _lineStride; + auto srcLast = srcFirst + srcLastOffset; + + const auto dstLastOffset = (_dims.x - 1); + auto dstFirst = _faces[dstFace].begin() + dstRow * _lineStride + 1; + auto dstLast = dstFirst + dstLastOffset; + + assert(srcFirst < _faces[srcFace].end()); + assert(srcLast < _faces[srcFace].end()); + assert(dstFirst < _faces[dstFace].end()); + assert(dstLast < _faces[dstFace].end()); + + if (dstInc < 0) { + std::swap(dstFirst, dstLast); + } + + copy(srcFirst, srcLast, _lineStride, dstFirst, dstInc); + } + + void copyRowToColumn(int srcFace, int srcRow, int dstFace, int dstCol, int dstInc) { + const auto srcLastOffset = (_dims.x - 1); + auto srcFirst = _faces[srcFace].begin() + srcRow * _lineStride + 1; + auto srcLast = srcFirst + srcLastOffset; + + const auto dstLastOffset = _lineStride * (_dims.y - 1); + auto dstFirst = _faces[dstFace].begin() + dstCol + _lineStride; + auto dstLast = dstFirst + dstLastOffset; + const auto dstStride = _lineStride * dstInc; + + assert(srcFirst < _faces[srcFace].end()); + assert(srcLast < _faces[srcFace].end()); + assert(dstFirst < _faces[dstFace].end()); + assert(dstLast < _faces[dstFace].end()); + + if (dstInc < 0) { + std::swap(dstFirst, dstLast); + } + + copy(srcFirst, srcLast, 1, dstFirst, dstStride); + } +}; + +static void copySurface(const nvtt::Surface& source, glm::vec4* dest, size_t dstLineStride) { + const float* srcRedIt = source.channel(0); + const float* srcGreenIt = source.channel(1); + const float* srcBlueIt = source.channel(2); + const float* srcAlphaIt = source.channel(3); + + for (int y = 0; y < source.height(); y++) { + glm::vec4* dstColIt = dest; + for (int x = 0; x < source.width(); x++) { + *dstColIt = glm::vec4(*srcRedIt, *srcGreenIt, *srcBlueIt, *srcAlphaIt); + dstColIt++; + srcRedIt++; + srcGreenIt++; + srcBlueIt++; + srcAlphaIt++; + } + dest += dstLineStride; + } +} + +CubeMap::CubeMap(int width, int height, int mipCount) { + reset(width, height, mipCount); +} + +CubeMap::CubeMap(const std::vector& faces, int mipCount, const std::atomic& abortProcessing) { + reset(faces.front().getWidth(), faces.front().getHeight(), mipCount); + + int face; + + nvtt::Surface surface; + surface.setAlphaMode(nvtt::AlphaMode_None); + surface.setWrapMode(nvtt::WrapMode_Mirror); + + // Compute mips + for (face = 0; face < 6; face++) { + Image faceImage = faces[face].getConvertedToFormat(Image::Format_RGBAF); + + surface.setImage(nvtt::InputFormat_RGBA_32F, _width, _height, 1, faceImage.editBits()); + + auto mipLevel = 0; + copySurface(surface, editFace(0, face), getMipLineStride(0)); + + while (surface.canMakeNextMipmap() && !abortProcessing.load()) { + surface.buildNextMipmap(nvtt::MipmapFilter_Box); + mipLevel++; + + copySurface(surface, editFace(mipLevel, face), getMipLineStride(mipLevel)); + } + } + + if (abortProcessing.load()) { + return; + } + + for (gpu::uint16 mipLevel = 0; mipLevel < mipCount; ++mipLevel) { + Mip mip(mipLevel, this); + mip.applySeams(); + } +} + +void CubeMap::applyGamma(float value) { + for (auto& mip : _mips) { + for (auto& face : mip) { + for (auto& pixel : face) { + pixel.r = std::pow(pixel.r, value); + pixel.g = std::pow(pixel.g, value); + pixel.b = std::pow(pixel.b, value); + } + } + } +} + +void CubeMap::copyFace(int width, int height, const glm::vec4* source, size_t srcLineStride, glm::vec4* dest, size_t dstLineStride) { + for (int y = 0; y < height; y++) { + std::copy(source, source + width, dest); + source += srcLineStride; + dest += dstLineStride; + } +} + +Image CubeMap::getFaceImage(gpu::uint16 mipLevel, int face) const { + auto mipDims = getMipDimensions(mipLevel); + Image faceImage(mipDims.x, mipDims.y, Image::Format_RGBAF); + copyFace(mipDims.x, mipDims.y, getFace(mipLevel, face), getMipLineStride(mipLevel), (glm::vec4*)faceImage.editBits(), faceImage.getBytesPerLineCount() / sizeof(glm::vec4)); + return faceImage; +} + +void CubeMap::reset(int width, int height, int mipCount) { + assert(mipCount >0 && width > 0 && height > 0); + _width = width; + _height = height; + _mips.resize(mipCount); + for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++) { + auto mipDimensions = getMipDimensions(mipLevel); + // Add extra pixels on edges to perform edge seam fixup (we will duplicate pixels from + // neighbouring faces) + auto mipPixelCount = (mipDimensions.x + 2 * EDGE_WIDTH) * (mipDimensions.y + 2 * EDGE_WIDTH); + + for (auto& face : _mips[mipLevel]) { + face.resize(mipPixelCount); + } + } +} + +void CubeMap::copyTo(CubeMap& other) const { + other._width = _width; + other._height = _height; + other._mips = _mips; +} + +void CubeMap::getFaceUV(const glm::vec3& dir, int* index, glm::vec2* uv) { + // Taken from https://en.wikipedia.org/wiki/Cube_mapping + float absX = std::abs(dir.x); + float absY = std::abs(dir.y); + float absZ = std::abs(dir.z); + + auto isXPositive = dir.x > 0; + auto isYPositive = dir.y > 0; + auto isZPositive = dir.z > 0; + + float maxAxis = 1.0f; + float uc = 0.0f; + float vc = 0.0f; + + // POSITIVE X + if (isXPositive && absX >= absY && absX >= absZ) { + // u (0 to 1) goes from +z to -z + // v (0 to 1) goes from -y to +y + maxAxis = absX; + uc = -dir.z; + vc = -dir.y; + *index = 0; + } + // NEGATIVE X + else if (!isXPositive && absX >= absY && absX >= absZ) { + // u (0 to 1) goes from -z to +z + // v (0 to 1) goes from -y to +y + maxAxis = absX; + uc = dir.z; + vc = -dir.y; + *index = 1; + } + // POSITIVE Y + else if (isYPositive && absY >= absX && absY >= absZ) { + // u (0 to 1) goes from -x to +x + // v (0 to 1) goes from +z to -z + maxAxis = absY; + uc = dir.x; + vc = dir.z; + *index = 2; + } + // NEGATIVE Y + else if (!isYPositive && absY >= absX && absY >= absZ) { + // u (0 to 1) goes from -x to +x + // v (0 to 1) goes from -z to +z + maxAxis = absY; + uc = dir.x; + vc = -dir.z; + *index = 3; + } + // POSITIVE Z + else if (isZPositive && absZ >= absX && absZ >= absY) { + // u (0 to 1) goes from -x to +x + // v (0 to 1) goes from -y to +y + maxAxis = absZ; + uc = dir.x; + vc = -dir.y; + *index = 4; + } + // NEGATIVE Z + else if (!isZPositive && absZ >= absX && absZ >= absY) { + // u (0 to 1) goes from +x to -x + // v (0 to 1) goes from -y to +y + maxAxis = absZ; + uc = -dir.x; + vc = -dir.y; + *index = 5; + } + + // Convert range from -1 to 1 to 0 to 1 + uv->x = 0.5f * (uc / maxAxis + 1.0f); + uv->y = 0.5f * (vc / maxAxis + 1.0f); +} + +glm::vec4 CubeMap::fetchLod(const glm::vec3& dir, float lod) const { + lod = glm::clamp(lod, 0.0f, _mips.size() - 1); + + gpu::uint16 loLevel = (gpu::uint16)std::floor(lod); + gpu::uint16 hiLevel = (gpu::uint16)std::ceil(lod); + float lodFrac = lod - (float)loLevel; + ConstMip loMip(loLevel, this); + ConstMip hiMip(hiLevel, this); + int face; + glm::vec2 uv; + glm::vec4 loColor; + glm::vec4 hiColor; + + getFaceUV(dir, &face, &uv); + + loColor = loMip.fetch(face, uv); + hiColor = hiMip.fetch(face, uv); + + return loColor + (hiColor - loColor) * lodFrac; +} + +struct CubeMap::GGXSamples { + float invTotalWeight; + std::vector points; +}; + +// All the GGX convolution code is inspired from: +// https://placeholderart.wordpress.com/2015/07/28/implementation-notes-runtime-environment-map-filtering-for-image-based-lighting/ +// Computation is done in tangent space so normal is always (0,0,1) which simplifies a lot of things + +void CubeMap::generateGGXSamples(GGXSamples& data, float roughness, const int resolution) { + glm::vec2 xi; + glm::vec3 L; + glm::vec3 H; + const float saTexel = (float)(4.0 * M_PI / (6.0 * resolution * resolution)); + const float mipBias = 3.0f; + const auto sampleCount = data.points.size(); + const auto hammersleySequenceLength = data.points.size(); + size_t sampleIndex = 0; + size_t hammersleySampleIndex = 0; + float NdotL; + + data.invTotalWeight = 0.0f; + + // Do some computation in tangent space + while (sampleIndex < sampleCount) { + if (hammersleySampleIndex < hammersleySequenceLength) { + xi = hammersley::evaluate((int)hammersleySampleIndex, (int)hammersleySequenceLength); + H = ggx::sample(xi, roughness); + L = H * (2.0f * H.z) - glm::vec3(0.0f, 0.0f, 1.0f); + NdotL = L.z; + hammersleySampleIndex++; + } else { + NdotL = -1.0f; + } + + while (NdotL <= 0.0f) { + // Create a purely random sample + xi.x = rand() / float(RAND_MAX); + xi.y = rand() / float(RAND_MAX); + H = ggx::sample(xi, roughness); + L = H * (2.0f * H.z) - glm::vec3(0.0f, 0.0f, 1.0f); + NdotL = L.z; + } + + float NdotH = std::max(0.0f, H.z); + float HdotV = NdotH; + float D = ggx::evaluate(NdotH, roughness); + float pdf = (D * NdotH / (4.0f * HdotV)) + 0.0001f; + float saSample = 1.0f / (float(sampleCount) * pdf + 0.0001f); + float mipLevel = std::max(0.5f * std::log2(saSample / saTexel) + mipBias, 0.0f); + + auto& sample = data.points[sampleIndex]; + sample.x = L.x; + sample.y = L.y; + sample.z = L.z; + sample.w = mipLevel; + + data.invTotalWeight += NdotL; + + sampleIndex++; + } + data.invTotalWeight = 1.0f / data.invTotalWeight; +} + +void CubeMap::convolveForGGX(CubeMap& output, const std::atomic& abortProcessing) const { + // This should match the value in the getMipLevelFromRoughness function (LightAmbient.slh) + static const float ROUGHNESS_1_MIP_RESOLUTION = 1.5f; + static const size_t MAX_SAMPLE_COUNT = 4000; + + const auto mipCount = getMipCount(); + GGXSamples params; + + params.points.reserve(MAX_SAMPLE_COUNT); + + for (gpu::uint16 mipLevel = 0; mipLevel < mipCount; ++mipLevel) { + // This is the inverse code found in LightAmbient.slh in getMipLevelFromRoughness + float levelAlpha = float(mipLevel) / (mipCount - ROUGHNESS_1_MIP_RESOLUTION); + float mipRoughness = levelAlpha * (1.0f + 2.0f * levelAlpha) / 3.0f; + + mipRoughness = std::max(1e-3f, mipRoughness); + mipRoughness = std::min(1.0f, mipRoughness); + + size_t mipTotalPixelCount = getMipWidth(mipLevel) * getMipHeight(mipLevel) * 6; + size_t sampleCount = 1U + size_t(4000 * mipRoughness * mipRoughness); + + sampleCount = std::min(sampleCount, 2 * mipTotalPixelCount); + sampleCount = std::min(MAX_SAMPLE_COUNT, sampleCount); + + params.points.resize(sampleCount); + generateGGXSamples(params, mipRoughness, _width); + + for (int face = 0; face < 6; face++) { + convolveMipFaceForGGX(params, output, mipLevel, face, abortProcessing); + if (abortProcessing.load()) { + return; + } + } + } +} + +void CubeMap::convolveMipFaceForGGX(const GGXSamples& samples, CubeMap& output, gpu::uint16 mipLevel, int face, const std::atomic& abortProcessing) const { + const glm::vec3* faceNormals = FACE_NORMALS + face * 4; + const glm::vec3 deltaYNormalLo = faceNormals[2] - faceNormals[0]; + const glm::vec3 deltaYNormalHi = faceNormals[3] - faceNormals[1]; + const auto mipDimensions = output.getMipDimensions(mipLevel); + const auto outputLineStride = output.getMipLineStride(mipLevel); + auto outputFacePixels = output.editFace(mipLevel, face); + + tbb::parallel_for(tbb::blocked_range2d(0, mipDimensions.y, 32, 0, mipDimensions.x, 32), [&](const tbb::blocked_range2d& range) { + auto rowRange = range.rows(); + auto colRange = range.cols(); + + for (auto y = rowRange.begin(); y < rowRange.end(); y++) { + if (abortProcessing.load()) { + break; + } + + const float yAlpha = (y + 0.5f) / mipDimensions.y; + const glm::vec3 normalXLo = faceNormals[0] + deltaYNormalLo * yAlpha; + const glm::vec3 normalXHi = faceNormals[1] + deltaYNormalHi * yAlpha; + const glm::vec3 deltaXNormal = normalXHi - normalXLo; + + for (auto x = colRange.begin(); x < colRange.end(); x++) { + const float xAlpha = (x + 0.5f) / mipDimensions.x; + // Interpolate normal for this pixel + const glm::vec3 normal = glm::normalize(normalXLo + deltaXNormal * xAlpha); + + outputFacePixels[x + y * outputLineStride] = computeConvolution(normal, samples); + } + } + }); +} + +glm::vec4 CubeMap::computeConvolution(const glm::vec3& N, const GGXSamples& samples) const { + // from tangent-space vector to world-space + glm::vec3 bitangent = std::abs(N.z) < 0.999f ? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(1.0f, 0.0f, 0.0f); + glm::vec3 tangent = glm::normalize(glm::cross(bitangent, N)); + bitangent = glm::cross(N, tangent); + + const size_t sampleCount = samples.points.size(); + glm::vec4 prefilteredColor = glm::vec4(0.0f); + + for (size_t i = 0; i < sampleCount; ++i) { + const auto& sample = samples.points[i]; + glm::vec3 L(sample.x, sample.y, sample.z); + float NdotL = L.z; + float mipLevel = sample.w; + // Now back to world space + L = tangent * L.x + bitangent * L.y + N * L.z; + prefilteredColor += fetchLod(L, mipLevel) * NdotL; + } + prefilteredColor = prefilteredColor * samples.invTotalWeight; + prefilteredColor.a = 1.0f; + return prefilteredColor; +} \ No newline at end of file diff --git a/libraries/image/src/image/CubeMap.h b/libraries/image/src/image/CubeMap.h new file mode 100644 index 0000000000..0745267cb6 --- /dev/null +++ b/libraries/image/src/image/CubeMap.h @@ -0,0 +1,92 @@ +// +// CubeMap.h +// image/src/image +// +// Created by Olivier Prat on 03/27/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_image_CubeMap_h +#define hifi_image_CubeMap_h + +#include +#include +#include +#include +#include + +#include "Image.h" + +namespace image { + + class CubeMap { + + enum { + EDGE_WIDTH = 1 + }; + + public: + + CubeMap(int width, int height, int mipCount); + CubeMap(const std::vector& faces, int mipCount, const std::atomic& abortProcessing = false); + + void reset(int width, int height, int mipCount); + void copyTo(CubeMap& other) const; + + void applyGamma(float value); + + gpu::uint16 getMipCount() const { return (gpu::uint16)_mips.size(); } + int getMipWidth(gpu::uint16 mipLevel) const { + return std::max(1, _width >> mipLevel); + } + int getMipHeight(gpu::uint16 mipLevel) const { + return std::max(1, _height >> mipLevel); + } + gpu::Vec2i getMipDimensions(gpu::uint16 mipLevel) const { + return gpu::Vec2i(getMipWidth(mipLevel), getMipHeight(mipLevel)); + } + + size_t getMipLineStride(gpu::uint16 mipLevel) const { + return getMipWidth(mipLevel) + 2 * EDGE_WIDTH; + } + + glm::vec4* editFace(gpu::uint16 mipLevel, int face) { + return _mips[mipLevel][face].data() + (getMipLineStride(mipLevel) + 1)*EDGE_WIDTH; + } + + const glm::vec4* getFace(gpu::uint16 mipLevel, int face) const { + return _mips[mipLevel][face].data() + (getMipLineStride(mipLevel) + 1)*EDGE_WIDTH; + } + + Image getFaceImage(gpu::uint16 mipLevel, int face) const; + + void convolveForGGX(CubeMap& output, const std::atomic& abortProcessing) const; + glm::vec4 fetchLod(const glm::vec3& dir, float lod) const; + + private: + + struct GGXSamples; + class Mip; + class ConstMip; + + using Face = std::vector; + using Faces = std::array; + + int _width; + int _height; + std::vector _mips; + + static void getFaceUV(const glm::vec3& dir, int* index, glm::vec2* uv); + static void generateGGXSamples(GGXSamples& data, float roughness, const int resolution); + static void copyFace(int width, int height, const glm::vec4* source, size_t srcLineStride, glm::vec4* dest, size_t dstLineStride); + void convolveMipFaceForGGX(const GGXSamples& samples, CubeMap& output, gpu::uint16 mipLevel, int face, const std::atomic& abortProcessing) const; + glm::vec4 computeConvolution(const glm::vec3& normal, const GGXSamples& samples) const; + + }; + +} + +#endif // hifi_image_CubeMap_h diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index df5ed15867..2ef83e42d8 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -6,28 +6,91 @@ using namespace image; +Image::Image(int width, int height, Format format) : + _dims(width, height), + _format(format) { + if (_format == Format_RGBAF) { + _floatData.resize(width*height); + } else { + _packedData = QImage(width, height, (QImage::Format)format); + } +} + +size_t Image::getByteCount() const { + if (_format == Format_RGBAF) { + return sizeof(FloatPixels::value_type) * _floatData.size(); + } else { + return _packedData.byteCount(); + } +} + +size_t Image::getBytesPerLineCount() const { + if (_format == Format_RGBAF) { + return sizeof(FloatPixels::value_type) * _dims.x; + } else { + return _packedData.bytesPerLine(); + } +} + +glm::uint8* Image::editScanLine(int y) { + if (_format == Format_RGBAF) { + return reinterpret_cast(_floatData.data() + y * _dims.x); + } else { + return _packedData.scanLine(y); + } +} + +const glm::uint8* Image::getScanLine(int y) const { + if (_format == Format_RGBAF) { + return reinterpret_cast(_floatData.data() + y * _dims.x); + } else { + return _packedData.scanLine(y); + } +} + +glm::uint8* Image::editBits() { + if (_format == Format_RGBAF) { + return reinterpret_cast(_floatData.data()); + } else { + return _packedData.bits(); + } +} + +const glm::uint8* Image::getBits() const { + if (_format == Format_RGBAF) { + return reinterpret_cast(_floatData.data()); + } else { + return _packedData.bits(); + } +} + Image Image::getScaled(glm::uvec2 dstSize, AspectRatioMode ratioMode, TransformationMode transformMode) const { - if ((Image::Format)_data.format() == Image::Format_PACKED_FLOAT) { - // Start by converting to full float - glm::vec4* floatPixels = new glm::vec4[getWidth()*getHeight()]; - auto unpackFunc = getHDRUnpackingFunction(); - auto floatDataIt = floatPixels; - for (glm::uint32 lineNb = 0; lineNb < getHeight(); lineNb++) { - const glm::uint32* srcPixelIt = reinterpret_cast(getScanLine((int)lineNb)); - const glm::uint32* srcPixelEnd = srcPixelIt + getWidth(); - - while (srcPixelIt < srcPixelEnd) { - *floatDataIt = glm::vec4(unpackFunc(*srcPixelIt), 1.0f); - ++srcPixelIt; - ++floatDataIt; - } - } - - // Perform filtered resize with NVTT - static_assert(sizeof(glm::vec4) == 4 * sizeof(float), "Assuming glm::vec4 holds 4 floats"); + if (_format == Format_PACKED_FLOAT || _format == Format_RGBAF) { nvtt::Surface surface; - surface.setImage(nvtt::InputFormat_RGBA_32F, getWidth(), getHeight(), 1, floatPixels); - delete[] floatPixels; + + if (_format == Format_RGBAF) { + surface.setImage(nvtt::InputFormat_RGBA_32F, getWidth(), getHeight(), 1, _floatData.data()); + } else { + // Start by converting to full float + glm::vec4* floatPixels = new glm::vec4[getWidth()*getHeight()]; + auto unpackFunc = getHDRUnpackingFunction(); + auto floatDataIt = floatPixels; + for (glm::uint32 lineNb = 0; lineNb < getHeight(); lineNb++) { + const glm::uint32* srcPixelIt = reinterpret_cast(getScanLine((int)lineNb)); + const glm::uint32* srcPixelEnd = srcPixelIt + getWidth(); + + while (srcPixelIt < srcPixelEnd) { + *floatDataIt = glm::vec4(unpackFunc(*srcPixelIt), 1.0f); + ++srcPixelIt; + ++floatDataIt; + } + } + + // Perform filtered resize with NVTT + static_assert(sizeof(glm::vec4) == 4 * sizeof(float), "Assuming glm::vec4 holds 4 floats"); + surface.setImage(nvtt::InputFormat_RGBA_32F, getWidth(), getHeight(), 1, floatPixels); + delete[] floatPixels; + } nvtt::ResizeFilter filter = nvtt::ResizeFilter_Kaiser; if (transformMode == Qt::TransformationMode::FastTransformation) { @@ -35,44 +98,148 @@ Image Image::getScaled(glm::uvec2 dstSize, AspectRatioMode ratioMode, Transforma } surface.resize(dstSize.x, dstSize.y, 1, filter); - // And convert back to original format - QImage resizedImage((int)dstSize.x, (int)dstSize.y, (QImage::Format)Image::Format_PACKED_FLOAT); - - auto packFunc = getHDRPackingFunction(); auto srcRedIt = reinterpret_cast(surface.channel(0)); auto srcGreenIt = reinterpret_cast(surface.channel(1)); auto srcBlueIt = reinterpret_cast(surface.channel(2)); - for (glm::uint32 lineNb = 0; lineNb < dstSize.y; lineNb++) { - glm::uint32* dstPixelIt = reinterpret_cast(resizedImage.scanLine((int)lineNb)); - glm::uint32* dstPixelEnd = dstPixelIt + dstSize.x; + auto srcAlphaIt = reinterpret_cast(surface.channel(3)); + + if (_format == Format_RGBAF) { + Image output(_dims.x, _dims.y, _format); + auto dstPixelIt = output._floatData.begin(); + auto dstPixelEnd = output._floatData.end(); while (dstPixelIt < dstPixelEnd) { - *dstPixelIt = packFunc(glm::vec3(*srcRedIt, *srcGreenIt, *srcBlueIt)); + *dstPixelIt = glm::vec4(*srcRedIt, *srcGreenIt, *srcBlueIt, *srcAlphaIt); ++srcRedIt; ++srcGreenIt; ++srcBlueIt; + ++srcAlphaIt; + ++dstPixelIt; } + + return output; + } else { + // And convert back to original format + QImage resizedImage((int)dstSize.x, (int)dstSize.y, (QImage::Format)Image::Format_PACKED_FLOAT); + + auto packFunc = getHDRPackingFunction(); + for (glm::uint32 lineNb = 0; lineNb < dstSize.y; lineNb++) { + glm::uint32* dstPixelIt = reinterpret_cast(resizedImage.scanLine((int)lineNb)); + glm::uint32* dstPixelEnd = dstPixelIt + dstSize.x; + + while (dstPixelIt < dstPixelEnd) { + *dstPixelIt = packFunc(glm::vec3(*srcRedIt, *srcGreenIt, *srcBlueIt)); + ++srcRedIt; + ++srcGreenIt; + ++srcBlueIt; + ++dstPixelIt; + } + } + return resizedImage; } - return resizedImage; } else { - return _data.scaled(fromGlm(dstSize), ratioMode, transformMode); + return _packedData.scaled(fromGlm(dstSize), ratioMode, transformMode); } } Image Image::getConvertedToFormat(Format newFormat) const { - assert(getFormat() != Format_PACKED_FLOAT); - return _data.convertToFormat((QImage::Format)newFormat); + const float MAX_COLOR_VALUE = 255.0f; + + if (newFormat == _format) { + return *this; + } else if ((_format != Format_R11G11B10F && _format != Format_RGBAF) && (newFormat != Format_R11G11B10F && newFormat != Format_RGBAF)) { + return _packedData.convertToFormat((QImage::Format)newFormat); + } else if (_format == Format_PACKED_FLOAT) { + Image newImage(_dims.x, _dims.y, newFormat); + + switch (newFormat) { + case Format_RGBAF: + convertToFloatFromPacked(getBits(), _dims.x, _dims.y, getBytesPerLineCount(), gpu::Element::COLOR_R11G11B10, newImage._floatData.data(), _dims.x); + break; + + default: + { + auto unpackFunc = getHDRUnpackingFunction(); + const glm::uint32* srcIt = reinterpret_cast(getBits()); + + for (int y = 0; y < _dims.y; y++) { + for (int x = 0; x < _dims.x; x++) { + auto color = glm::clamp(unpackFunc(*srcIt) * MAX_COLOR_VALUE, 0.0f, 255.0f); + newImage.setPackedPixel(x, y, qRgb(color.r, color.g, color.b)); + srcIt++; + } + } + break; + } + } + return newImage; + } else if (_format == Format_RGBAF) { + Image newImage(_dims.x, _dims.y, newFormat); + + switch (newFormat) { + case Format_R11G11B10F: + convertToPackedFromFloat(newImage.editBits(), _dims.x, _dims.y, getBytesPerLineCount(), gpu::Element::COLOR_R11G11B10, _floatData.data(), _dims.x); + break; + + default: + { + FloatPixels::const_iterator srcIt = _floatData.begin(); + + for (int y = 0; y < _dims.y; y++) { + for (int x = 0; x < _dims.x; x++) { + auto color = glm::clamp((*srcIt) * MAX_COLOR_VALUE, 0.0f, 255.0f); + newImage.setPackedPixel(x, y, qRgba(color.r, color.g, color.b, color.a)); + srcIt++; + } + } + break; + } + } + return newImage; + } else { + Image newImage(_dims.x, _dims.y, newFormat); + assert(newImage.hasFloatFormat()); + + if (newFormat == Format_RGBAF) { + FloatPixels::iterator dstIt = newImage._floatData.begin(); + + for (int y = 0; y < _dims.y; y++) { + auto line = (const QRgb*)getScanLine(y); + for (int x = 0; x < _dims.x; x++) { + QRgb pixel = line[x]; + *dstIt = glm::vec4(qRed(pixel), qGreen(pixel), qBlue(pixel), qAlpha(pixel)) / MAX_COLOR_VALUE; + dstIt++; + } + } + } else { + auto packFunc = getHDRPackingFunction(); + glm::uint32* dstIt = reinterpret_cast( newImage.editBits() ); + + for (int y = 0; y < _dims.y; y++) { + auto line = (const QRgb*)getScanLine(y); + for (int x = 0; x < _dims.x; x++) { + QRgb pixel = line[x]; + *dstIt = packFunc(glm::vec3(qRed(pixel), qGreen(pixel), qBlue(pixel)) / MAX_COLOR_VALUE); + dstIt++; + } + } + } + return newImage; + } } void Image::invertPixels() { - _data.invertPixels(QImage::InvertRgba); + assert(_format != Format_PACKED_FLOAT && _format != Format_RGBAF); + _packedData.invertPixels(QImage::InvertRgba); } Image Image::getSubImage(QRect rect) const { - return _data.copy(rect); + assert(_format != Format_RGBAF); + return _packedData.copy(rect); } Image Image::getMirrored(bool horizontal, bool vertical) const { - return _data.mirrored(horizontal, vertical); + assert(_format != Format_RGBAF); + return _packedData.mirrored(horizontal, vertical); } diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index bfecf4f2a1..129061900f 100644 --- a/libraries/image/src/image/Image.h +++ b/libraries/image/src/image/Image.h @@ -48,37 +48,69 @@ namespace image { Format_RGBA8888_Premultiplied = QImage::Format_RGBA8888_Premultiplied, Format_Grayscale8 = QImage::Format_Grayscale8, Format_R11G11B10F = QImage::Format_RGB30, - Format_PACKED_FLOAT = Format_R11G11B10F + Format_PACKED_FLOAT = Format_R11G11B10F, + // RGBA 32 bit single precision float per component + Format_RGBAF = 100 }; using AspectRatioMode = Qt::AspectRatioMode; using TransformationMode = Qt::TransformationMode; - Image() {} - Image(int width, int height, Format format) : _data(width, height, (QImage::Format)format) {} - Image(const QImage& data) : _data(data) {} - void operator=(const QImage& image) { - _data = image; + Image() : _dims(0,0) {} + Image(int width, int height, Format format); + Image(const QImage& data) : _packedData(data), _dims(data.width(), data.height()), _format((Format)data.format()) {} + + void operator=(const QImage& other) { + _packedData = other; + _floatData.clear(); + _dims.x = other.width(); + _dims.y = other.height(); + _format = (Format)other.format(); } - bool isNull() const { return _data.isNull(); } - - Format getFormat() const { return (Format)_data.format(); } - bool hasAlphaChannel() const { return _data.hasAlphaChannel(); } - - glm::uint32 getWidth() const { return (glm::uint32)_data.width(); } - glm::uint32 getHeight() const { return (glm::uint32)_data.height(); } - glm::uvec2 getSize() const { return toGlm(_data.size()); } - size_t getByteCount() const { return _data.byteCount(); } - - QRgb getPixel(int x, int y) const { return _data.pixel(x, y); } - void setPixel(int x, int y, QRgb value) { - _data.setPixel(x, y, value); + void operator=(const Image& other) { + if (&other != this) { + _packedData = other._packedData; + _floatData = other._floatData; + _dims = other._dims; + _format = other._format; + } } - glm::uint8* editScanLine(int y) { return _data.scanLine(y); } - const glm::uint8* getScanLine(int y) const { return _data.scanLine(y); } - const glm::uint8* getBits() const { return _data.constBits(); } + bool isNull() const { return _packedData.isNull() && _floatData.empty(); } + + Format getFormat() const { return _format; } + bool hasAlphaChannel() const { return _packedData.hasAlphaChannel() || _format == Format_RGBAF; } + bool hasFloatFormat() const { return _format == Format_R11G11B10F || _format == Format_RGBAF; } + + glm::uint32 getWidth() const { return (glm::uint32)_dims.x; } + glm::uint32 getHeight() const { return (glm::uint32)_dims.y; } + glm::uvec2 getSize() const { return glm::uvec2(_dims); } + size_t getByteCount() const; + size_t getBytesPerLineCount() const; + + QRgb getPackedPixel(int x, int y) const { + assert(_format != Format_RGBAF); + return _packedData.pixel(x, y); + } + void setPackedPixel(int x, int y, QRgb value) { + assert(_format != Format_RGBAF); + _packedData.setPixel(x, y, value); + } + + glm::vec4 getFloatPixel(int x, int y) const { + assert(_format == Format_RGBAF); + return _floatData[x + y*_dims.x]; + } + void setFloatPixel(int x, int y, const glm::vec4& value) { + assert(_format == Format_RGBAF); + _floatData[x + y * _dims.x] = value; + } + + glm::uint8* editScanLine(int y); + const glm::uint8* getScanLine(int y) const; + glm::uint8* editBits(); + const glm::uint8* getBits() const; Image getScaled(glm::uvec2 newSize, AspectRatioMode ratioMode, TransformationMode transformationMode = Qt::SmoothTransformation) const; Image getConvertedToFormat(Format newFormat) const; @@ -90,7 +122,13 @@ namespace image { private: - QImage _data; + using FloatPixels = std::vector; + + // For QImage supported formats + QImage _packedData; + FloatPixels _floatData; + glm::ivec2 _dims; + Format _format; }; } // namespace image diff --git a/libraries/image/src/image/TextureProcessing.cpp b/libraries/image/src/image/TextureProcessing.cpp index 037229ace5..429859d109 100644 --- a/libraries/image/src/image/TextureProcessing.cpp +++ b/libraries/image/src/image/TextureProcessing.cpp @@ -29,10 +29,10 @@ #include "OpenEXRReader.h" #endif #include "ImageLogging.h" +#include "CubeMap.h" using namespace gpu; -#define CPU_MIPMAPS 1 #include #undef _CRT_SECURE_NO_WARNINGS @@ -103,7 +103,7 @@ gpu::Element getHDRTextureFormatForTarget(BackendTarget target, bool compressed) } } -TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, const QVariantMap& options) { +TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type) { switch (type) { case ALBEDO_TEXTURE: return image::TextureUsage::createAlbedoTextureFromImage; @@ -111,12 +111,10 @@ TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, con return image::TextureUsage::createEmissiveTextureFromImage; case LIGHTMAP_TEXTURE: return image::TextureUsage::createLightmapTextureFromImage; - case CUBE_TEXTURE: - if (options.value("generateIrradiance", true).toBool()) { - return image::TextureUsage::createCubeTextureFromImage; - } else { - return image::TextureUsage::createCubeTextureFromImageWithoutIrradiance; - } + case SKY_TEXTURE: + return image::TextureUsage::createCubeTextureFromImage; + case AMBIENT_TEXTURE: + return image::TextureUsage::createAmbientCubeTextureAndIrradianceFromImage; case BUMP_TEXTURE: return image::TextureUsage::createNormalTextureFromBumpImage; case NORMAL_TEXTURE: @@ -188,12 +186,12 @@ gpu::TexturePointer TextureUsage::createMetallicTextureFromImage(Image&& srcImag gpu::TexturePointer TextureUsage::createCubeTextureFromImage(Image&& srcImage, const std::string& srcImageName, bool compress, BackendTarget target, const std::atomic& abortProcessing) { - return processCubeTextureColorFromImage(std::move(srcImage), srcImageName, compress, target, true, abortProcessing); + return processCubeTextureColorFromImage(std::move(srcImage), srcImageName, compress, target, CUBE_DEFAULT, abortProcessing); } -gpu::TexturePointer TextureUsage::createCubeTextureFromImageWithoutIrradiance(Image&& srcImage, const std::string& srcImageName, - bool compress, BackendTarget target, const std::atomic& abortProcessing) { - return processCubeTextureColorFromImage(std::move(srcImage), srcImageName, compress, target, false, abortProcessing); +gpu::TexturePointer TextureUsage::createAmbientCubeTextureAndIrradianceFromImage(Image&& image, const std::string& srcImageName, + bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing) { + return processCubeTextureColorFromImage(std::move(image), srcImageName, compress, target, CUBE_GENERATE_IRRADIANCE | CUBE_GGX_CONVOLVE, abortProcessing); } static float denormalize(float value, const float minValue) { @@ -215,11 +213,17 @@ static uint32 packR11G11B10F(const glm::vec3& color) { return glm::packF2x11_1x10(ucolor); } +static uint32 packUnorm4x8(const glm::vec3& color) { + return glm::packUnorm4x8(glm::vec4(color, 1.0f)); +} + static std::function getHDRPackingFunction(const gpu::Element& format) { if (format == gpu::Element::COLOR_RGB9E5) { return glm::packF3x9_E1x5; } else if (format == gpu::Element::COLOR_R11G11B10) { return packR11G11B10F; + } else if (format == gpu::Element::COLOR_RGBA_32 || format == gpu::Element::COLOR_SRGBA_32 || format == gpu::Element::COLOR_BGRA_32 || format == gpu::Element::COLOR_SBGRA_32) { + return packUnorm4x8; } else { qCWarning(imagelogging) << "Unknown handler format"; Q_UNREACHABLE(); @@ -231,18 +235,24 @@ std::function getHDRPackingFunction() { return getHDRPackingFunction(GPU_CUBEMAP_HDR_FORMAT); } -std::function getHDRUnpackingFunction() { - if (GPU_CUBEMAP_HDR_FORMAT == gpu::Element::COLOR_RGB9E5) { +std::function getHDRUnpackingFunction(const gpu::Element& format) { + if (format == gpu::Element::COLOR_RGB9E5) { return glm::unpackF3x9_E1x5; - } else if (GPU_CUBEMAP_HDR_FORMAT == gpu::Element::COLOR_R11G11B10) { + } else if (format == gpu::Element::COLOR_R11G11B10) { return glm::unpackF2x11_1x10; + } else if (format == gpu::Element::COLOR_RGBA_32 || format == gpu::Element::COLOR_SRGBA_32 || format == gpu::Element::COLOR_BGRA_32 || format == gpu::Element::COLOR_SBGRA_32) { + return glm::unpackUnorm4x8; } else { - qCWarning(imagelogging) << "Unknown HDR encoding format in Image"; + qCWarning(imagelogging) << "Unknown handler format"; Q_UNREACHABLE(); return nullptr; } } +std::function getHDRUnpackingFunction() { + return getHDRUnpackingFunction(GPU_CUBEMAP_HDR_FORMAT); +} + Image processRawImageData(QIODevice& content, const std::string& filename) { // Help the Image loader by extracting the image file format from the url filename ext. // Some tga are not created properly without it. @@ -364,7 +374,7 @@ gpu::TexturePointer processImage(std::shared_ptr content, const std:: if (sourceChannel != ColorChannel::NONE) { mapToRedChannel(image, sourceChannel); } - + auto loader = TextureUsage::getTextureLoaderForType(textureType); auto texture = loader(std::move(image), filename, compress, target, abortProcessing); @@ -490,13 +500,15 @@ struct MyErrorHandler : public nvtt::ErrorHandler { } }; +#if defined(NVTT_API) class SequentialTaskDispatcher : public nvtt::TaskDispatcher { public: - SequentialTaskDispatcher(const std::atomic& abortProcessing) : _abortProcessing(abortProcessing) {}; + SequentialTaskDispatcher(const std::atomic& abortProcessing = false) : _abortProcessing(abortProcessing) { + } const std::atomic& _abortProcessing; - virtual void dispatch(nvtt::Task* task, void* context, int count) override { + void dispatch(nvtt::Task* task, void* context, int count) override { for (int i = 0; i < count; i++) { if (!_abortProcessing.load()) { task(context, i); @@ -506,108 +518,137 @@ public: } } }; +#endif -void generateHDRMips(gpu::Texture* texture, Image&& image, BackendTarget target, const std::atomic& abortProcessing, int face) { - // Take a local copy to force move construction - // https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#f18-for-consume-parameters-pass-by-x-and-stdmove-the-parameter - Image localCopy = std::move(image); +void convertToFloatFromPacked(const unsigned char* source, int width, int height, size_t srcLineByteStride, gpu::Element sourceFormat, + glm::vec4* output, size_t outputLinePixelStride) { + glm::vec4* outputIt; + auto unpackFunc = getHDRUnpackingFunction(sourceFormat); - assert(localCopy.getFormat() == Image::Format_PACKED_FLOAT); - - const int width = localCopy.getWidth(), height = localCopy.getHeight(); - std::vector data; - std::vector::iterator dataIt; - auto mipFormat = texture->getStoredMipFormat(); - std::function unpackFunc = getHDRUnpackingFunction(); - - nvtt::InputFormat inputFormat = nvtt::InputFormat_RGBA_32F; - nvtt::WrapMode wrapMode = nvtt::WrapMode_Mirror; - nvtt::AlphaMode alphaMode = nvtt::AlphaMode_None; - - nvtt::CompressionOptions compressionOptions; - compressionOptions.setQuality(nvtt::Quality_Production); - - // TODO: gles: generate ETC mips instead? - if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB) { - compressionOptions.setFormat(nvtt::Format_BC6); - } else if (mipFormat == gpu::Element::COLOR_RGB9E5) { - compressionOptions.setFormat(nvtt::Format_RGB); - compressionOptions.setPixelType(nvtt::PixelType_Float); - compressionOptions.setPixelFormat(32, 32, 32, 0); - } else if (mipFormat == gpu::Element::COLOR_R11G11B10) { - compressionOptions.setFormat(nvtt::Format_RGB); - compressionOptions.setPixelType(nvtt::PixelType_Float); - compressionOptions.setPixelFormat(32, 32, 32, 0); - } else { - qCWarning(imagelogging) << "Unknown mip format"; - Q_UNREACHABLE(); - return; - } - - data.resize(width * height); - dataIt = data.begin(); + outputLinePixelStride -= width; + outputIt = output; for (auto lineNb = 0; lineNb < height; lineNb++) { - const uint32* srcPixelIt = reinterpret_cast(localCopy.getScanLine(lineNb)); + const uint32* srcPixelIt = reinterpret_cast(source + lineNb * srcLineByteStride); const uint32* srcPixelEnd = srcPixelIt + width; while (srcPixelIt < srcPixelEnd) { - *dataIt = glm::vec4(unpackFunc(*srcPixelIt), 1.0f); + *outputIt = glm::vec4(unpackFunc(*srcPixelIt), 1.0f); ++srcPixelIt; - ++dataIt; + ++outputIt; } + outputIt += outputLinePixelStride; } - assert(dataIt == data.end()); +} - // We're done with the localCopy, free up the memory to avoid bloating the heap - localCopy = Image(); // Image doesn't have a clear function, so override it with an empty one. +void convertToPackedFromFloat(unsigned char* output, int width, int height, size_t outputLineByteStride, gpu::Element outputFormat, + const glm::vec4* source, size_t srcLinePixelStride) { + const glm::vec4* sourceIt; + auto packFunc = getHDRPackingFunction(outputFormat); + + srcLinePixelStride -= width; + sourceIt = source; + for (auto lineNb = 0; lineNb < height; lineNb++) { + uint32* outPixelIt = reinterpret_cast(output + lineNb * outputLineByteStride); + uint32* outPixelEnd = outPixelIt + width; + + while (outPixelIt < outPixelEnd) { + *outPixelIt = packFunc(*sourceIt); + ++outPixelIt; + ++sourceIt; + } + sourceIt += srcLinePixelStride; + } +} + +nvtt::OutputHandler* getNVTTCompressionOutputHandler(gpu::Texture* outputTexture, int face, nvtt::CompressionOptions& compressionOptions) { + auto outputFormat = outputTexture->getStoredMipFormat(); + bool useNVTT = false; + + compressionOptions.setQuality(nvtt::Quality_Production); + + if (outputFormat == gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB) { + useNVTT = true; + compressionOptions.setFormat(nvtt::Format_BC6); + } else if (outputFormat == gpu::Element::COLOR_RGB9E5) { + compressionOptions.setFormat(nvtt::Format_RGB); + compressionOptions.setPixelType(nvtt::PixelType_Float); + compressionOptions.setPixelFormat(32, 32, 32, 0); + } else if (outputFormat == gpu::Element::COLOR_R11G11B10) { + compressionOptions.setFormat(nvtt::Format_RGB); + compressionOptions.setPixelType(nvtt::PixelType_Float); + compressionOptions.setPixelFormat(32, 32, 32, 0); + } else if (outputFormat == gpu::Element::COLOR_SRGBA_32) { + useNVTT = true; + compressionOptions.setFormat(nvtt::Format_RGB); + compressionOptions.setPixelType(nvtt::PixelType_UnsignedNorm); + compressionOptions.setPixelFormat(8, 8, 8, 0); + } else { + qCWarning(imagelogging) << "Unknown mip format"; + Q_UNREACHABLE(); + return nullptr; + } + + if (!useNVTT) { + // Don't use NVTT (at least version 2.1) as it outputs wrong RGB9E5 and R11G11B10F values from floats + return new PackedFloatOutputHandler(outputTexture, face, outputFormat); + } else { + return new OutputHandler(outputTexture, face); + } +} + +void convertImageToHDRTexture(gpu::Texture* texture, Image&& image, BackendTarget target, int baseMipLevel, bool buildMips, const std::atomic& abortProcessing, int face) { + assert(image.hasFloatFormat()); + + Image localCopy = image.getConvertedToFormat(Image::Format_RGBAF); + + const int width = localCopy.getWidth(); + const int height = localCopy.getHeight(); nvtt::OutputOptions outputOptions; outputOptions.setOutputHeader(false); - std::unique_ptr outputHandler; + + nvtt::CompressionOptions compressionOptions; + std::unique_ptr outputHandler{ getNVTTCompressionOutputHandler(texture, face, compressionOptions) }; + MyErrorHandler errorHandler; outputOptions.setErrorHandler(&errorHandler); nvtt::Context context; - int mipLevel = 0; - - if (mipFormat == gpu::Element::COLOR_RGB9E5 || mipFormat == gpu::Element::COLOR_R11G11B10) { - // Don't use NVTT (at least version 2.1) as it outputs wrong RGB9E5 and R11G11B10F values from floats - outputHandler.reset(new PackedFloatOutputHandler(texture, face, mipFormat)); - } else { - outputHandler.reset(new OutputHandler(texture, face)); - } + int mipLevel = baseMipLevel; outputOptions.setOutputHandler(outputHandler.get()); nvtt::Surface surface; - surface.setImage(inputFormat, width, height, 1, &(*data.begin())); - surface.setAlphaMode(alphaMode); - surface.setWrapMode(wrapMode); + surface.setImage(nvtt::InputFormat_RGBA_32F, width, height, 1, localCopy.getBits()); + surface.setAlphaMode(nvtt::AlphaMode_None); + surface.setWrapMode(nvtt::WrapMode_Mirror); SequentialTaskDispatcher dispatcher(abortProcessing); nvtt::Compressor compressor; context.setTaskDispatcher(&dispatcher); context.compress(surface, face, mipLevel++, compressionOptions, outputOptions); - while (surface.canMakeNextMipmap() && !abortProcessing.load()) { - surface.buildNextMipmap(nvtt::MipmapFilter_Box); - context.compress(surface, face, mipLevel++, compressionOptions, outputOptions); + if (buildMips) { + while (surface.canMakeNextMipmap() && !abortProcessing.load()) { + surface.buildNextMipmap(nvtt::MipmapFilter_Box); + context.compress(surface, face, mipLevel++, compressionOptions, outputOptions); + } } } -void generateLDRMips(gpu::Texture* texture, Image&& image, BackendTarget target, const std::atomic& abortProcessing, int face) { +void convertImageToLDRTexture(gpu::Texture* texture, Image&& image, BackendTarget target, int baseMipLevel, bool buildMips, const std::atomic& abortProcessing, int face) { // Take a local copy to force move construction // https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#f18-for-consume-parameters-pass-by-x-and-stdmove-the-parameter Image localCopy = std::move(image); - assert(localCopy.getFormat() != Image::Format_PACKED_FLOAT); - if (localCopy.getFormat() != Image::Format_ARGB32) { - localCopy = localCopy.getConvertedToFormat(Image::Format_ARGB32); - } - const int width = localCopy.getWidth(), height = localCopy.getHeight(); auto mipFormat = texture->getStoredMipFormat(); + int mipLevel = baseMipLevel; if (target != BackendTarget::GLES32) { + if (localCopy.getFormat() != Image::Format_ARGB32) { + localCopy = localCopy.getConvertedToFormat(Image::Format_ARGB32); + } + const void* data = static_cast(localCopy.getBits()); nvtt::TextureType textureType = nvtt::TextureType_2D; nvtt::InputFormat inputFormat = nvtt::InputFormat_BGRA_8UB; @@ -618,23 +659,22 @@ void generateLDRMips(gpu::Texture* texture, Image&& image, BackendTarget target, float inputGamma = 2.2f; float outputGamma = 2.2f; - nvtt::InputOptions inputOptions; - inputOptions.setTextureLayout(textureType, width, height); + nvtt::Surface surface; + surface.setImage(inputFormat, width, height, 1, data); + surface.setAlphaMode(alphaMode); + surface.setWrapMode(wrapMode); - inputOptions.setMipmapData(data, width, height); - // setMipmapData copies the memory, so free up the memory afterward to avoid bloating the heap + // Surface copies the memory, so free up the memory afterward to avoid bloating the heap data = nullptr; localCopy = Image(); // Image doesn't have a clear function, so override it with an empty one. + nvtt::InputOptions inputOptions; + inputOptions.setTextureLayout(textureType, width, height); + inputOptions.setFormat(inputFormat); inputOptions.setGamma(inputGamma, outputGamma); - inputOptions.setAlphaMode(alphaMode); - inputOptions.setWrapMode(wrapMode); inputOptions.setRoundMode(roundMode); - inputOptions.setMipmapGeneration(true); - inputOptions.setMipmapFilter(nvtt::MipmapFilter_Box); - nvtt::CompressionOptions compressionOptions; compressionOptions.setQuality(nvtt::Quality_Production); @@ -718,11 +758,22 @@ void generateLDRMips(gpu::Texture* texture, Image&& image, BackendTarget target, outputOptions.setErrorHandler(&errorHandler); SequentialTaskDispatcher dispatcher(abortProcessing); - nvtt::Compressor compressor; - compressor.setTaskDispatcher(&dispatcher); - compressor.process(inputOptions, compressionOptions, outputOptions); + nvtt::Compressor context; + + context.compress(surface, face, mipLevel++, compressionOptions, outputOptions); + if (buildMips) { + while (surface.canMakeNextMipmap() && !abortProcessing.load()) { + surface.buildNextMipmap(nvtt::MipmapFilter_Box); + context.compress(surface, face, mipLevel++, compressionOptions, outputOptions); + } + } } else { - int numMips = 1 + (int)log2(std::max(width, height)); + int numMips = 1; + + if (buildMips) { + numMips += (int)log2(std::max(width, height)) - baseMipLevel; + } + assert(numMips > 0); Etc::RawImage *mipMaps = new Etc::RawImage[numMips]; Etc::Image::Format etcFormat = Etc::Image::Format::DEFAULT; @@ -756,23 +807,13 @@ void generateLDRMips(gpu::Texture* texture, Image&& image, BackendTarget target, const float effort = 1.0f; const int numEncodeThreads = 4; int encodingTime; - const float MAX_COLOR = 255.0f; - std::vector floatData; - floatData.resize(width * height); - for (int y = 0; y < height; y++) { - QRgb *line = (QRgb *)localCopy.editScanLine(y); - for (int x = 0; x < width; x++) { - QRgb &pixel = line[x]; - floatData[x + y * width] = vec4(qRed(pixel), qGreen(pixel), qBlue(pixel), qAlpha(pixel)) / MAX_COLOR; - } + if (localCopy.getFormat() != Image::Format_RGBAF) { + localCopy = localCopy.getConvertedToFormat(Image::Format_RGBAF); } - // free up the memory afterward to avoid bloating the heap - localCopy = Image(); // Image doesn't have a clear function, so override it with an empty one. - Etc::EncodeMipmaps( - (float *)floatData.data(), width, height, + (float *)localCopy.editBits(), width, height, etcFormat, errorMetric, effort, numEncodeThreads, numEncodeThreads, numMips, Etc::FILTER_WRAP_NONE, @@ -782,9 +823,9 @@ void generateLDRMips(gpu::Texture* texture, Image&& image, BackendTarget target, for (int i = 0; i < numMips; i++) { if (mipMaps[i].paucEncodingBits.get()) { if (face >= 0) { - texture->assignStoredMipFace(i, face, mipMaps[i].uiEncodingBitsBytes, static_cast(mipMaps[i].paucEncodingBits.get())); + texture->assignStoredMipFace(i+baseMipLevel, face, mipMaps[i].uiEncodingBitsBytes, static_cast(mipMaps[i].paucEncodingBits.get())); } else { - texture->assignStoredMip(i, mipMaps[i].uiEncodingBitsBytes, static_cast(mipMaps[i].paucEncodingBits.get())); + texture->assignStoredMip(i + baseMipLevel, mipMaps[i].uiEncodingBitsBytes, static_cast(mipMaps[i].paucEncodingBits.get())); } } } @@ -795,22 +836,27 @@ void generateLDRMips(gpu::Texture* texture, Image&& image, BackendTarget target, #endif -void generateMips(gpu::Texture* texture, Image&& image, BackendTarget target, const std::atomic& abortProcessing = false, int face = -1) { -#if CPU_MIPMAPS - PROFILE_RANGE(resource_parse, "generateMips"); +void convertImageToTexture(gpu::Texture* texture, Image& image, BackendTarget target, int face, int baseMipLevel, bool buildMips, const std::atomic& abortProcessing) { + PROFILE_RANGE(resource_parse, "convertToTextureWithMips"); if (target == BackendTarget::GLES32) { - generateLDRMips(texture, std::move(image), target, abortProcessing, face); + convertImageToLDRTexture(texture, std::move(image), target, baseMipLevel, buildMips, abortProcessing, face); } else { - if (image.getFormat() == Image::Format_PACKED_FLOAT) { - generateHDRMips(texture, std::move(image), target, abortProcessing, face); + if (image.hasFloatFormat()) { + convertImageToHDRTexture(texture, std::move(image), target, baseMipLevel, buildMips, abortProcessing, face); } else { - generateLDRMips(texture, std::move(image), target, abortProcessing, face); + convertImageToLDRTexture(texture, std::move(image), target, baseMipLevel, buildMips, abortProcessing, face); } } -#else - texture->setAutoGenerateMips(true); -#endif +} + +void convertToTextureWithMips(gpu::Texture* texture, Image&& image, BackendTarget target, const std::atomic& abortProcessing, int face) { + convertImageToTexture(texture, image, target, face, 0, true, abortProcessing); +} + +void convertToTexture(gpu::Texture* texture, Image&& image, BackendTarget target, const std::atomic& abortProcessing, int face, int mipLevel) { + PROFILE_RANGE(resource_parse, "convertToTexture"); + convertImageToTexture(texture, image, target, face, mipLevel, false, abortProcessing); } void processTextureAlpha(const Image& srcImage, bool& validAlpha, bool& alphaAsMask) { @@ -900,7 +946,7 @@ gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(Image&& srcImag theTexture->setUsage(usage.build()); theTexture->setStoredMipFormat(formatMip); theTexture->assignStoredMip(0, image.getByteCount(), image.getBits()); - generateMips(theTexture.get(), std::move(image), target, abortProcessing); + convertToTextureWithMips(theTexture.get(), std::move(image), target, abortProcessing); } return theTexture; @@ -944,14 +990,14 @@ Image processBumpMap(Image&& image) { const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); // surrounding pixels - const QRgb topLeft = localCopy.getPixel(iPrevClamped, jPrevClamped); - const QRgb top = localCopy.getPixel(iPrevClamped, j); - const QRgb topRight = localCopy.getPixel(iPrevClamped, jNextClamped); - const QRgb right = localCopy.getPixel(i, jNextClamped); - const QRgb bottomRight = localCopy.getPixel(iNextClamped, jNextClamped); - const QRgb bottom = localCopy.getPixel(iNextClamped, j); - const QRgb bottomLeft = localCopy.getPixel(iNextClamped, jPrevClamped); - const QRgb left = localCopy.getPixel(i, jPrevClamped); + const QRgb topLeft = localCopy.getPackedPixel(iPrevClamped, jPrevClamped); + const QRgb top = localCopy.getPackedPixel(iPrevClamped, j); + const QRgb topRight = localCopy.getPackedPixel(iPrevClamped, jNextClamped); + const QRgb right = localCopy.getPackedPixel(i, jNextClamped); + const QRgb bottomRight = localCopy.getPackedPixel(iNextClamped, jNextClamped); + const QRgb bottom = localCopy.getPackedPixel(iNextClamped, j); + const QRgb bottomLeft = localCopy.getPackedPixel(iNextClamped, jPrevClamped); + const QRgb left = localCopy.getPackedPixel(i, jPrevClamped); // take their gray intensities // since it's a grayscale image, the value of each component RGB is the same @@ -974,12 +1020,13 @@ Image processBumpMap(Image&& image) { // convert to rgb from the value obtained computing the filter QRgb qRgbValue = qRgba(mapComponent(v.z), mapComponent(v.y), mapComponent(v.x), 1.0); - result.setPixel(i, j, qRgbValue); + result.setPackedPixel(i, j, qRgbValue); } } return result; } + gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(Image&& srcImage, const std::string& srcImageName, bool compress, BackendTarget target, bool isBumpMap, const std::atomic& abortProcessing) { @@ -1014,7 +1061,7 @@ gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(Image&& src theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); theTexture->assignStoredMip(0, image.getByteCount(), image.getBits()); - generateMips(theTexture.get(), std::move(image), target, abortProcessing); + convertToTextureWithMips(theTexture.get(), std::move(image), target, abortProcessing); } return theTexture; @@ -1054,7 +1101,7 @@ gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(Image&& src theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); theTexture->assignStoredMip(0, image.getByteCount(), image.getBits()); - generateMips(theTexture.get(), std::move(image), target, abortProcessing); + convertToTextureWithMips(theTexture.get(), std::move(image), target, abortProcessing); } return theTexture; @@ -1416,8 +1463,41 @@ Image convertToHDRFormat(Image&& srcImage, gpu::Element format) { return hdrImage; } +static bool isLinearTextureFormat(gpu::Element format) { + return !((format == gpu::Element::COLOR_SRGBA_32) + || (format == gpu::Element::COLOR_SBGRA_32) + || (format == gpu::Element::COLOR_SR_8) + || (format == gpu::Element::COLOR_COMPRESSED_BCX_SRGB) + || (format == gpu::Element::COLOR_COMPRESSED_BCX_SRGBA_MASK) + || (format == gpu::Element::COLOR_COMPRESSED_BCX_SRGBA) + || (format == gpu::Element::COLOR_COMPRESSED_BCX_SRGBA_HIGH) + || (format == gpu::Element::COLOR_COMPRESSED_ETC2_SRGB) + || (format == gpu::Element::COLOR_COMPRESSED_ETC2_SRGBA) + || (format == gpu::Element::COLOR_COMPRESSED_ETC2_SRGB_PUNCHTHROUGH_ALPHA)); +} + +void convolveForGGX(const std::vector& faces, gpu::Texture* texture, BackendTarget target, const std::atomic& abortProcessing = false) { + PROFILE_RANGE(resource_parse, "convolveForGGX"); + CubeMap source(faces, texture->getNumMips(), abortProcessing); + CubeMap output(texture->getWidth(), texture->getHeight(), texture->getNumMips()); + + if (!faces.front().hasFloatFormat()) { + source.applyGamma(2.2f); + } + source.convolveForGGX(output, abortProcessing); + if (!isLinearTextureFormat(texture->getTexelFormat())) { + output.applyGamma(1.0f/2.2f); + } + + for (int face = 0; face < 6; face++) { + for (gpu::uint16 mipLevel = 0; mipLevel < output.getMipCount(); mipLevel++) { + convertToTexture(texture, output.getFaceImage(mipLevel, face), target, abortProcessing, face, mipLevel); + } + } +} + gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(Image&& srcImage, const std::string& srcImageName, - bool compress, BackendTarget target, bool generateIrradiance, + bool compress, BackendTarget target, int options, const std::atomic& abortProcessing) { PROFILE_RANGE(resource_parse, "processCubeTextureColorFromImage"); @@ -1491,7 +1571,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(Image&& srcIm theTexture->setStoredMipFormat(formatMip); // Generate irradiance while we are at it - if (generateIrradiance) { + if (options & CUBE_GENERATE_IRRADIANCE) { PROFILE_RANGE(resource_parse, "generateIrradiance"); gpu::Element irradianceFormat; // TODO: we could locally compress the irradiance texture on Android, but we don't need to @@ -1513,9 +1593,16 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(Image&& srcIm auto irradiance = irradianceTexture->getIrradiance(); theTexture->overrideIrradiance(irradiance); } - - for (uint8 face = 0; face < faces.size(); ++face) { - generateMips(theTexture.get(), std::move(faces[face]), target, abortProcessing, face); + + if (options & CUBE_GGX_CONVOLVE) { + // Performs and convolution AND mip map generation + convolveForGGX(faces, theTexture.get(), target, abortProcessing); + } else { + // Create mip maps and compress to final format in one go + for (uint8 face = 0; face < faces.size(); ++face) { + // Force building the mip maps right now on CPU if we are convolving for GGX later on + convertToTextureWithMips(theTexture.get(), std::move(faces[face]), target, abortProcessing, face); + } } } diff --git a/libraries/image/src/image/TextureProcessing.h b/libraries/image/src/image/TextureProcessing.h index 72e2400721..b4036ddd9f 100644 --- a/libraries/image/src/image/TextureProcessing.h +++ b/libraries/image/src/image/TextureProcessing.h @@ -17,11 +17,16 @@ #include #include "Image.h" +#include namespace image { std::function getHDRPackingFunction(); std::function getHDRUnpackingFunction(); + void convertToFloatFromPacked(const unsigned char* source, int width, int height, size_t srcLineByteStride, gpu::Element sourceFormat, + glm::vec4* output, size_t outputLinePixelStride); + void convertToPackedFromFloat(unsigned char* output, int width, int height, size_t outputLineByteStride, gpu::Element outputFormat, + const glm::vec4* source, size_t srcLinePixelStride); namespace TextureUsage { @@ -62,7 +67,8 @@ enum Type { ROUGHNESS_TEXTURE, GLOSS_TEXTURE, EMISSIVE_TEXTURE, - CUBE_TEXTURE, + SKY_TEXTURE, + AMBIENT_TEXTURE, OCCLUSION_TEXTURE, SCATTERING_TEXTURE = OCCLUSION_TEXTURE, LIGHTMAP_TEXTURE, @@ -70,7 +76,7 @@ enum Type { }; using TextureLoader = std::function&)>; -TextureLoader getTextureLoaderForType(Type type, const QVariantMap& options = QVariantMap()); +TextureLoader getTextureLoaderForType(Type type); gpu::TexturePointer create2DTextureFromImage(Image&& image, const std::string& srcImageName, bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); @@ -92,8 +98,8 @@ gpu::TexturePointer createMetallicTextureFromImage(Image&& image, const std::str bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); gpu::TexturePointer createCubeTextureFromImage(Image&& image, const std::string& srcImageName, bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); -gpu::TexturePointer createCubeTextureFromImageWithoutIrradiance(Image&& image, const std::string& srcImageName, - bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); +gpu::TexturePointer createAmbientCubeTextureAndIrradianceFromImage(Image&& image, const std::string& srcImageName, + bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); gpu::TexturePointer createLightmapTextureFromImage(Image&& image, const std::string& srcImageName, bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing); gpu::TexturePointer process2DTextureColorFromImage(Image&& srcImage, const std::string& srcImageName, bool compress, @@ -102,9 +108,14 @@ gpu::TexturePointer process2DTextureNormalMapFromImage(Image&& srcImage, const s gpu::BackendTarget target, bool isBumpMap, const std::atomic& abortProcessing); gpu::TexturePointer process2DTextureGrayscaleFromImage(Image&& srcImage, const std::string& srcImageName, bool compress, gpu::BackendTarget target, bool isInvertedPixels, const std::atomic& abortProcessing); -gpu::TexturePointer processCubeTextureColorFromImage(Image&& srcImage, const std::string& srcImageName, bool compress, - gpu::BackendTarget target, bool generateIrradiance, const std::atomic& abortProcessing); +enum CubeTextureOptions { + CUBE_DEFAULT = 0x0, + CUBE_GENERATE_IRRADIANCE = 0x1, + CUBE_GGX_CONVOLVE = 0x2 +}; +gpu::TexturePointer processCubeTextureColorFromImage(Image&& srcImage, const std::string& srcImageName, bool compress, + gpu::BackendTarget target, int option, const std::atomic& abortProcessing); } // namespace TextureUsage const QStringList getSupportedFormats(); @@ -113,6 +124,9 @@ gpu::TexturePointer processImage(std::shared_ptr content, const std:: int maxNumPixels, TextureUsage::Type textureType, bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing = false); +void convertToTextureWithMips(gpu::Texture* texture, Image&& image, gpu::BackendTarget target, const std::atomic& abortProcessing = false, int face = -1); +void convertToTexture(gpu::Texture* texture, Image&& image, gpu::BackendTarget target, const std::atomic& abortProcessing = false, int face = -1, int mipLevel = 0); + } // namespace image #endif // hifi_image_TextureProcessing_h diff --git a/libraries/material-networking/src/material-networking/MaterialCache.cpp b/libraries/material-networking/src/material-networking/MaterialCache.cpp index 6561fc697e..745504fb3d 100644 --- a/libraries/material-networking/src/material-networking/MaterialCache.cpp +++ b/libraries/material-networking/src/material-networking/MaterialCache.cpp @@ -177,6 +177,8 @@ std::pair> NetworkMaterialResource material->setModel(modelString); } + std::array texcoordTransforms; + if (modelString == HIFI_PBR) { const QString FALLTHROUGH("fallthrough"); for (auto& key : materialJSON.keys()) { @@ -184,6 +186,7 @@ std::pair> NetworkMaterialResource auto nameJSON = materialJSON.value(key); if (nameJSON.isString()) { name = nameJSON.toString().toStdString(); + material->setName(name); } } else if (key == "model") { auto modelJSON = materialJSON.value(key); @@ -371,8 +374,11 @@ std::pair> NetworkMaterialResource if (valueString == FALLTHROUGH) { material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::TEXCOORDTRANSFORM0); } + } else if (value.isObject()) { + auto valueVariant = value.toVariant(); + glm::mat4 transform = mat4FromVariant(valueVariant); + texcoordTransforms[0] = transform; } - // TODO: implement texCoordTransform0 } else if (key == "texCoordTransform1") { auto value = materialJSON.value(key); if (value.isString()) { @@ -380,8 +386,11 @@ std::pair> NetworkMaterialResource if (valueString == FALLTHROUGH) { material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::TEXCOORDTRANSFORM1); } + } else if (value.isObject()) { + auto valueVariant = value.toVariant(); + glm::mat4 transform = mat4FromVariant(valueVariant); + texcoordTransforms[1] = transform; } - // TODO: implement texCoordTransform1 } else if (key == "lightmapParams") { auto value = materialJSON.value(key); if (value.isString()) { @@ -408,6 +417,15 @@ std::pair> NetworkMaterialResource } } } + + // Do this after the texture maps are defined, so it overrides the default transforms + for (int i = 0; i < graphics::Material::NUM_TEXCOORD_TRANSFORMS; i++) { + mat4 newTransform = texcoordTransforms[i]; + if (newTransform != mat4() || newTransform != material->getTexCoordTransform(i)) { + material->setTexCoordTransform(i, newTransform); + } + } + return std::pair>(name, material); } @@ -559,8 +577,7 @@ void NetworkMaterial::setLightmapMap(const QUrl& url) { } NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) : - graphics::Material(*material._material), - _textures(MapChannel::NUM_MAP_CHANNELS) + graphics::Material(*material._material) { _name = material.name.toStdString(); if (!material.albedoTexture.filename.isEmpty()) { @@ -709,7 +726,7 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { bool NetworkMaterial::isMissingTexture() { for (auto& networkTexture : _textures) { - auto& texture = networkTexture.texture; + auto& texture = networkTexture.second.texture; if (!texture) { continue; } diff --git a/libraries/material-networking/src/material-networking/MaterialCache.h b/libraries/material-networking/src/material-networking/MaterialCache.h index d327aedb22..7ed0453187 100644 --- a/libraries/material-networking/src/material-networking/MaterialCache.h +++ b/libraries/material-networking/src/material-networking/MaterialCache.h @@ -36,15 +36,21 @@ public: bool isMissingTexture(); void checkResetOpacityMap(); -protected: - friend class Geometry; - class Texture { public: QString name; NetworkTexturePointer texture; }; - using Textures = std::vector; + struct MapChannelHash { + std::size_t operator()(MapChannel mapChannel) const { + return static_cast(mapChannel); + } + }; + using Textures = std::unordered_map; + Textures getTextures() { return _textures; } + +protected: + friend class Geometry; Textures _textures; @@ -102,6 +108,7 @@ private: using NetworkMaterialResourcePointer = QSharedPointer; using MaterialMapping = std::vector>; +Q_DECLARE_METATYPE(MaterialMapping) class MaterialCache : public ResourceCache { public: diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 6af59930fa..b3192eac6e 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -224,10 +224,14 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs return getResourceTexture(url); } auto modifiedUrl = url; - if (type == image::TextureUsage::CUBE_TEXTURE) { + if (type == image::TextureUsage::SKY_TEXTURE) { QUrlQuery query { url.query() }; query.addQueryItem("skybox", ""); modifiedUrl.setQuery(query.toString()); + } else if (type == image::TextureUsage::AMBIENT_TEXTURE) { + QUrlQuery query{ url.query() }; + query.addQueryItem("ambient", ""); + modifiedUrl.setQuery(query.toString()); } TextureExtra extra = { type, content, maxNumPixels, sourceChannel }; return ResourceCache::getResource(modifiedUrl, QUrl(), &extra, std::hash()(extra)).staticCast(); @@ -283,7 +287,8 @@ gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) { case image::TextureUsage::BUMP_TEXTURE: case image::TextureUsage::SPECULAR_TEXTURE: case image::TextureUsage::GLOSS_TEXTURE: - case image::TextureUsage::CUBE_TEXTURE: + case image::TextureUsage::SKY_TEXTURE: + case image::TextureUsage::AMBIENT_TEXTURE: case image::TextureUsage::STRICT_TEXTURE: default: break; @@ -306,13 +311,13 @@ gpu::BackendTarget getBackendTarget() { } /// Returns a texture version of an image file -gpu::TexturePointer TextureCache::getImageTexture(const QString& path, image::TextureUsage::Type type, QVariantMap options) { +gpu::TexturePointer TextureCache::getImageTexture(const QString& path, image::TextureUsage::Type type) { QImage image = QImage(path); if (image.isNull()) { qCWarning(networking) << "Unable to load required resource texture" << path; return nullptr; } - auto loader = image::TextureUsage::getTextureLoaderForType(type, options); + auto loader = image::TextureUsage::getTextureLoaderForType(type); #ifdef USE_GLES constexpr bool shouldCompress = true; @@ -408,7 +413,7 @@ void NetworkTexture::setExtra(void* extra) { _shouldFailOnRedirect = _currentlyLoadingResourceType != ResourceType::KTX; - if (_type == image::TextureUsage::CUBE_TEXTURE) { + if (_type == image::TextureUsage::SKY_TEXTURE) { setLoadPriority(this, SKYBOX_LOAD_PRIORITY); } else if (_currentlyLoadingResourceType == ResourceType::KTX) { setLoadPriority(this, HIGH_MIPS_LOAD_PRIORITY); @@ -630,11 +635,9 @@ void NetworkTexture::makeLocalRequest() { } bool NetworkTexture::handleFailedRequest(ResourceRequest::Result result) { - if (_currentlyLoadingResourceType != ResourceType::KTX - && result == ResourceRequest::Result::RedirectFail) { - + if (_shouldFailOnRedirect && result == ResourceRequest::Result::RedirectFail) { auto newPath = _request->getRelativePathUrl(); - if (newPath.fileName().endsWith(".ktx")) { + if (newPath.fileName().toLower().endsWith(".ktx")) { _currentlyLoadingResourceType = ResourceType::KTX; _activeUrl = newPath; _shouldFailOnRedirect = false; diff --git a/libraries/material-networking/src/material-networking/TextureCache.h b/libraries/material-networking/src/material-networking/TextureCache.h index f8ddd77412..a328622885 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.h +++ b/libraries/material-networking/src/material-networking/TextureCache.h @@ -176,7 +176,7 @@ public: const gpu::TexturePointer& getBlackTexture(); /// Returns a texture version of an image file - static gpu::TexturePointer getImageTexture(const QString& path, image::TextureUsage::Type type = image::TextureUsage::DEFAULT_TEXTURE, QVariantMap options = QVariantMap()); + static gpu::TexturePointer getImageTexture(const QString& path, image::TextureUsage::Type type = image::TextureUsage::DEFAULT_TEXTURE); /// Loads a texture from the specified URL. NetworkTexturePointer getTexture(const QUrl& url, image::TextureUsage::Type type = image::TextureUsage::DEFAULT_TEXTURE, diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 2e378965de..25a45cefe5 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -52,7 +52,7 @@ std::vector createMaterialList(const hfm::Mesh& mesh) { } std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { - Q_ASSERT(normals.size() == 0 || normals.size() == mesh.vertices.size()); + Q_ASSERT(normals.size() == 0 || (int)normals.size() == mesh.vertices.size()); Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index acb2bdc1c5..17b62d0915 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -10,6 +10,62 @@ #include "ModelBakerLogging.h" +#include + +void processMaterialMapping(MaterialMapping& materialMapping, const QJsonObject& materialMap, const hifi::URL& url) { + 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()); + materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url); + 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(url.resolved(mappingValue)))); + } + } +} + void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { const auto& mapping = input.get0(); const auto& url = input.get1(); @@ -18,56 +74,18 @@ void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, con auto mappingIter = mapping.find("materialMap"); if (mappingIter != mapping.end()) { QByteArray materialMapValue = mappingIter.value().toByteArray(); - QJsonObject materialMap = QJsonDocument::fromJson(materialMapValue).object(); - if (materialMap.isEmpty()) { + QJsonDocument materialMapJSON = QJsonDocument::fromJson(materialMapValue); + if (materialMapJSON.isEmpty()) { qCDebug(model_baker) << "Material Map found but did not produce valid JSON:" << materialMapValue; + } else if (materialMapJSON.isObject()) { + QJsonObject materialMap = materialMapJSON.object(); + processMaterialMapping(materialMapping, materialMap, url); } 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()); - materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url); - 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(url.resolved(mappingValue)))); + QJsonArray materialMapArray = materialMapJSON.array(); + for (auto materialMapIter : materialMapArray) { + if (materialMapIter.isObject()) { + QJsonObject materialMap = materialMapIter.toObject(); + processMaterialMapping(materialMapping, materialMap, url); } } } diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 4cf7609ee9..26bd20d967 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -31,8 +31,6 @@ Q_LOGGING_CATEGORY(trace_resource_parse_geometry, "trace.resource.parse.geometry") -class GeometryReader; - class GeometryExtra { public: const GeometryMappingPair& mapping; @@ -87,113 +85,6 @@ namespace std { }; } -QUrl resolveTextureBaseUrl(const QUrl& url, const QUrl& textureBaseUrl) { - return textureBaseUrl.isValid() ? textureBaseUrl : url; -} - -class GeometryMappingResource : public GeometryResource { - Q_OBJECT -public: - GeometryMappingResource(const QUrl& url) : GeometryResource(url) {}; - - QString getType() const override { return "GeometryMapping"; } - - virtual void downloadFinished(const QByteArray& data) override; - -private slots: - void onGeometryMappingLoaded(bool success); - -private: - GeometryResource::Pointer _geometryResource; - QMetaObject::Connection _connection; -}; - -void GeometryMappingResource::downloadFinished(const QByteArray& data) { - PROFILE_ASYNC_BEGIN(resource_parse_geometry, "GeometryMappingResource::downloadFinished", _url.toString(), - { { "url", _url.toString() } }); - - // store parsed contents of FST file - _mapping = FSTReader::readMapping(data); - - QString filename = _mapping.value("filename").toString(); - - if (filename.isNull()) { - finishedLoading(false); - } else { - const QString baseURL = _mapping.value("baseURL").toString(); - const QUrl base = _effectiveBaseURL.resolved(baseURL); - QUrl url = base.resolved(filename); - - QString texdir = _mapping.value(TEXDIR_FIELD).toString(); - if (!texdir.isNull()) { - if (!texdir.endsWith('/')) { - texdir += '/'; - } - _textureBaseUrl = resolveTextureBaseUrl(url, base.resolved(texdir)); - } else { - _textureBaseUrl = url.resolved(QUrl(".")); - } - - auto scripts = FSTReader::getScripts(base, _mapping); - if (scripts.size() > 0) { - _mapping.remove(SCRIPT_FIELD); - for (auto &scriptPath : scripts) { - _mapping.insertMulti(SCRIPT_FIELD, scriptPath); - } - } - - auto animGraphVariant = _mapping.value("animGraphUrl"); - - if (animGraphVariant.isValid()) { - QUrl fstUrl(animGraphVariant.toString()); - if (fstUrl.isValid()) { - _animGraphOverrideUrl = base.resolved(fstUrl); - } else { - _animGraphOverrideUrl = QUrl(); - } - } else { - _animGraphOverrideUrl = QUrl(); - } - - auto modelCache = DependencyManager::get(); - GeometryExtra extra { GeometryMappingPair(base, _mapping), _textureBaseUrl, false }; - - // Get the raw GeometryResource - _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast(); - // Avoid caching nested resources - their references will be held by the parent - _geometryResource->_isCacheable = false; - - if (_geometryResource->isLoaded()) { - onGeometryMappingLoaded(!_geometryResource->getURL().isEmpty()); - } else { - if (_connection) { - disconnect(_connection); - } - - _connection = connect(_geometryResource.data(), &Resource::finished, - this, &GeometryMappingResource::onGeometryMappingLoaded); - } - } -} - -void GeometryMappingResource::onGeometryMappingLoaded(bool success) { - if (success && _geometryResource) { - _hfmModel = _geometryResource->_hfmModel; - _materialMapping = _geometryResource->_materialMapping; - _meshParts = _geometryResource->_meshParts; - _meshes = _geometryResource->_meshes; - _materials = _geometryResource->_materials; - - // Avoid holding onto extra references - _geometryResource.reset(); - // Make sure connection will not trigger again - disconnect(_connection); // FIXME Should not have to do this - } - - PROFILE_ASYNC_END(resource_parse_geometry, "GeometryMappingResource::downloadFinished", _url.toString()); - finishedLoading(success); -} - class GeometryReader : public QRunnable { public: GeometryReader(const ModelLoader& modelLoader, QWeakPointer& resource, const QUrl& url, const GeometryMappingPair& mapping, @@ -282,8 +173,16 @@ void GeometryReader::run() { hfmModel->scripts.push_back(script.toString()); } } + + // Do processing on the model + baker::Baker modelBaker(hfmModel, _mapping.second, _mapping.first); + modelBaker.run(); + + auto processedHFMModel = modelBaker.getHFMModel(); + auto materialMapping = modelBaker.getMaterialMapping(); + QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", - Q_ARG(HFMModel::Pointer, hfmModel), Q_ARG(GeometryMappingPair, _mapping)); + Q_ARG(HFMModel::Pointer, processedHFMModel), Q_ARG(MaterialMapping, materialMapping)); } catch (const std::exception&) { auto resource = _resource.toStrongRef(); if (resource) { @@ -300,60 +199,133 @@ void GeometryReader::run() { } } -class GeometryDefinitionResource : public GeometryResource { - Q_OBJECT -public: - GeometryDefinitionResource(const ModelLoader& modelLoader, const QUrl& url) : GeometryResource(url), _modelLoader(modelLoader) {} - GeometryDefinitionResource(const GeometryDefinitionResource& other) : - GeometryResource(other), - _modelLoader(other._modelLoader), - _mapping(other._mapping), - _combineParts(other._combineParts) {} +QUrl resolveTextureBaseUrl(const QUrl& url, const QUrl& textureBaseUrl) { + return textureBaseUrl.isValid() ? textureBaseUrl : url; +} - QString getType() const override { return "GeometryDefinition"; } +GeometryResource::GeometryResource(const GeometryResource& other) : + Resource(other), + Geometry(other), + _modelLoader(other._modelLoader), + _mappingPair(other._mappingPair), + _textureBaseURL(other._textureBaseURL), + _combineParts(other._combineParts), + _isCacheable(other._isCacheable) +{ + if (other._geometryResource) { + _startedLoading = false; + } +} - virtual void downloadFinished(const QByteArray& data) override; +void GeometryResource::downloadFinished(const QByteArray& data) { + if (_effectiveBaseURL.fileName().toLower().endsWith(".fst")) { + PROFILE_ASYNC_BEGIN(resource_parse_geometry, "GeometryResource::downloadFinished", _url.toString(), { { "url", _url.toString() } }); - void setExtra(void* extra) override; + // store parsed contents of FST file + _mapping = FSTReader::readMapping(data); -protected: - Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping); + QString filename = _mapping.value("filename").toString(); -private: - ModelLoader _modelLoader; - GeometryMappingPair _mapping; - bool _combineParts; -}; + if (filename.isNull()) { + finishedLoading(false); + } else { + const QString baseURL = _mapping.value("baseURL").toString(); + const QUrl base = _effectiveBaseURL.resolved(baseURL); + QUrl url = base.resolved(filename); -void GeometryDefinitionResource::setExtra(void* extra) { + QString texdir = _mapping.value(TEXDIR_FIELD).toString(); + if (!texdir.isNull()) { + if (!texdir.endsWith('/')) { + texdir += '/'; + } + _textureBaseURL = resolveTextureBaseUrl(url, base.resolved(texdir)); + } else { + _textureBaseURL = url.resolved(QUrl(".")); + } + + auto scripts = FSTReader::getScripts(base, _mapping); + if (scripts.size() > 0) { + _mapping.remove(SCRIPT_FIELD); + for (auto &scriptPath : scripts) { + _mapping.insertMulti(SCRIPT_FIELD, scriptPath); + } + } + + auto animGraphVariant = _mapping.value("animGraphUrl"); + + if (animGraphVariant.isValid()) { + QUrl fstUrl(animGraphVariant.toString()); + if (fstUrl.isValid()) { + _animGraphOverrideUrl = base.resolved(fstUrl); + } else { + _animGraphOverrideUrl = QUrl(); + } + } else { + _animGraphOverrideUrl = QUrl(); + } + + auto modelCache = DependencyManager::get(); + GeometryExtra extra { GeometryMappingPair(base, _mapping), _textureBaseURL, false }; + + // Get the raw GeometryResource + _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast(); + // Avoid caching nested resources - their references will be held by the parent + _geometryResource->_isCacheable = false; + + if (_geometryResource->isLoaded()) { + onGeometryMappingLoaded(!_geometryResource->getURL().isEmpty()); + } else { + if (_connection) { + disconnect(_connection); + } + + _connection = connect(_geometryResource.data(), &Resource::finished, this, &GeometryResource::onGeometryMappingLoaded); + } + } + } else { + if (_url != _effectiveBaseURL) { + _url = _effectiveBaseURL; + _textureBaseURL = _effectiveBaseURL; + } + QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mappingPair, data, _combineParts, _request->getWebMediaType())); + } +} + +void GeometryResource::onGeometryMappingLoaded(bool success) { + if (success && _geometryResource) { + _hfmModel = _geometryResource->_hfmModel; + _materialMapping = _geometryResource->_materialMapping; + _meshParts = _geometryResource->_meshParts; + _meshes = _geometryResource->_meshes; + _materials = _geometryResource->_materials; + + // Avoid holding onto extra references + _geometryResource.reset(); + // Make sure connection will not trigger again + disconnect(_connection); // FIXME Should not have to do this + } + + PROFILE_ASYNC_END(resource_parse_geometry, "GeometryResource::downloadFinished", _url.toString()); + finishedLoading(success); +} + +void GeometryResource::setExtra(void* extra) { const GeometryExtra* geometryExtra = static_cast(extra); - _mapping = geometryExtra ? geometryExtra->mapping : GeometryMappingPair(QUrl(), QVariantHash()); - _textureBaseUrl = geometryExtra ? resolveTextureBaseUrl(_url, geometryExtra->textureBaseUrl) : QUrl(); + _mappingPair = geometryExtra ? geometryExtra->mapping : GeometryMappingPair(QUrl(), QVariantHash()); + _textureBaseURL = geometryExtra ? resolveTextureBaseUrl(_url, geometryExtra->textureBaseUrl) : QUrl(); _combineParts = geometryExtra ? geometryExtra->combineParts : true; } -void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { - if (_url != _effectiveBaseURL) { - _url = _effectiveBaseURL; - _textureBaseUrl = _effectiveBaseURL; - } - QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mapping, data, _combineParts, _request->getWebMediaType())); -} - -void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping) { - // Do processing on the model - baker::Baker modelBaker(hfmModel, mapping.second, mapping.first); - modelBaker.run(); - +void GeometryResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const MaterialMapping& materialMapping) { // Assume ownership of the processed HFMModel - _hfmModel = modelBaker.getHFMModel(); - _materialMapping = modelBaker.getMaterialMapping(); + _hfmModel = hfmModel; + _materialMapping = materialMapping; // Copy materials QHash materialIDAtlas; for (const HFMMaterial& material : _hfmModel->materials) { materialIDAtlas[material.materialID] = _materials.size(); - _materials.push_back(std::make_shared(material, _textureBaseUrl)); + _materials.push_back(std::make_shared(material, _textureBaseURL)); } std::shared_ptr meshes = std::make_shared(); @@ -376,6 +348,23 @@ void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmMode finishedLoading(true); } +void GeometryResource::deleter() { + resetTextures(); + Resource::deleter(); +} + +void GeometryResource::setTextures() { + if (_hfmModel) { + for (const HFMMaterial& material : _hfmModel->materials) { + _materials.push_back(std::make_shared(material, _textureBaseURL)); + } + } +} + +void GeometryResource::resetTextures() { + _materials.clear(); +} + ModelCache::ModelCache() { const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE); @@ -388,26 +377,14 @@ ModelCache::ModelCache() { } QSharedPointer ModelCache::createResource(const QUrl& url) { - Resource* resource = nullptr; - if (url.path().toLower().endsWith(".fst")) { - resource = new GeometryMappingResource(url); - } else { - resource = new GeometryDefinitionResource(_modelLoader, url); - } - - return QSharedPointer(resource, &Resource::deleter); + return QSharedPointer(new GeometryResource(url, _modelLoader), &GeometryResource::deleter); } QSharedPointer ModelCache::createResourceCopy(const QSharedPointer& resource) { - if (resource->getURL().path().toLower().endsWith(".fst")) { - return QSharedPointer(new GeometryMappingResource(*resource.staticCast()), &Resource::deleter); - } else { - return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast()), &Resource::deleter); - } + return QSharedPointer(new GeometryResource(*resource.staticCast()), &GeometryResource::deleter); } -GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url, - const GeometryMappingPair& mapping, const QUrl& textureBaseUrl) { +GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url, const GeometryMappingPair& mapping, const QUrl& textureBaseUrl) { bool combineParts = true; GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts }; GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra)).staticCast(); @@ -437,8 +414,8 @@ const QVariantMap Geometry::getTextures() const { QVariantMap textures; for (const auto& material : _materials) { for (const auto& texture : material->_textures) { - if (texture.texture) { - textures[texture.name] = texture.texture->getURL(); + if (texture.second.texture) { + textures[texture.second.name] = texture.second.texture->getURL(); } } } @@ -467,7 +444,7 @@ void Geometry::setTextures(const QVariantMap& textureMap) { for (auto& material : _materials) { // Check if any material textures actually changed if (std::any_of(material->_textures.cbegin(), material->_textures.cend(), - [&textureMap](const NetworkMaterial::Textures::value_type& it) { return it.texture && textureMap.contains(it.name); })) { + [&textureMap](const NetworkMaterial::Textures::value_type& it) { return it.second.texture && textureMap.contains(it.second.name); })) { // FIXME: The Model currently caches the materials (waste of space!) // so they must be copied in the Geometry copy-ctor @@ -498,6 +475,20 @@ bool Geometry::areTexturesLoaded() const { material->checkResetOpacityMap(); } + for (auto& materialMapping : _materialMapping) { + if (materialMapping.second) { + for (auto& materialPair : materialMapping.second->parsedMaterials.networkMaterials) { + if (materialPair.second) { + if (materialPair.second->isMissingTexture()) { + return false; + } + + materialPair.second->checkResetOpacityMap(); + } + } + } + } + _areTexturesLoaded = true; } return true; @@ -513,23 +504,6 @@ const std::shared_ptr Geometry::getShapeMaterial(int partID) co return nullptr; } -void GeometryResource::deleter() { - resetTextures(); - Resource::deleter(); -} - -void GeometryResource::setTextures() { - if (_hfmModel) { - for (const HFMMaterial& material : _hfmModel->materials) { - _materials.push_back(std::make_shared(material, _textureBaseUrl)); - } - } -} - -void GeometryResource::resetTextures() { - _materials.clear(); -} - void GeometryResourceWatcher::startWatching() { connect(_resource.data(), &Resource::finished, this, &GeometryResourceWatcher::resourceFinished); connect(_resource.data(), &Resource::onRefresh, this, &GeometryResourceWatcher::resourceRefreshed); diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index ca1ceaff16..f9ae2dccd6 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -24,8 +24,6 @@ class MeshPart; -class GeometryMappingResource; - using GeometryMappingPair = std::pair; Q_DECLARE_METATYPE(GeometryMappingPair) @@ -60,8 +58,6 @@ public: const QVariantHash& getMapping() const { return _mapping; } protected: - friend class GeometryMappingResource; - // Shared across all geometries, constant throughout lifetime std::shared_ptr _hfmModel; MaterialMapping _materialMapping; @@ -80,23 +76,29 @@ private: /// A geometry loaded from the network. class GeometryResource : public Resource, public Geometry { + Q_OBJECT public: using Pointer = QSharedPointer; - GeometryResource(const QUrl& url) : Resource(url) {} - GeometryResource(const GeometryResource& other) : - Resource(other), - Geometry(other), - _textureBaseUrl(other._textureBaseUrl), - _isCacheable(other._isCacheable) {} + GeometryResource(const QUrl& url, const ModelLoader& modelLoader) : Resource(url), _modelLoader(modelLoader) {} + GeometryResource(const GeometryResource& other); - virtual bool areTexturesLoaded() const override { return isLoaded() && Geometry::areTexturesLoaded(); } + QString getType() const override { return "Geometry"; } virtual void deleter() override; + virtual void downloadFinished(const QByteArray& data) override; + void setExtra(void* extra) override; + + virtual bool areTexturesLoaded() const override { return isLoaded() && Geometry::areTexturesLoaded(); } + +private slots: + void onGeometryMappingLoaded(bool success); + protected: friend class ModelCache; - friend class GeometryMappingResource; + + Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, const MaterialMapping& materialMapping); // Geometries may not hold onto textures while cached - that is for the texture cache // Instead, these methods clear and reset textures from the geometry when caching/loading @@ -104,10 +106,18 @@ protected: void setTextures(); void resetTextures(); - QUrl _textureBaseUrl; - virtual bool isCacheable() const override { return _loaded && _isCacheable; } - bool _isCacheable { true }; + +private: + ModelLoader _modelLoader; + GeometryMappingPair _mappingPair; + QUrl _textureBaseURL; + bool _combineParts; + + GeometryResource::Pointer _geometryResource; + QMetaObject::Connection _connection; + + bool _isCacheable{ true }; }; class GeometryResourceWatcher : public QObject { @@ -158,7 +168,7 @@ public: const QUrl& textureBaseUrl = QUrl()); protected: - friend class GeometryMappingResource; + friend class GeometryResource; virtual QSharedPointer createResource(const QUrl& url) override; QSharedPointer createResourceCopy(const QSharedPointer& resource) override; diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 18a180ad79..82f3459c15 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -588,6 +588,8 @@ void LimitedNodeList::eraseAllNodes() { foreach(const SharedNodePointer& killedNode, killedNodes) { handleNodeKill(killedNode); } + + _delayedNodeAdds.clear(); } void LimitedNodeList::reset() { @@ -755,7 +757,7 @@ void LimitedNodeList::delayNodeAdd(NewNodeInfo info) { } void LimitedNodeList::removeDelayedAdd(QUuid nodeUUID) { - auto it = std::find_if(_delayedNodeAdds.begin(), _delayedNodeAdds.end(), [&](auto info) { + auto it = std::find_if(_delayedNodeAdds.begin(), _delayedNodeAdds.end(), [&](const auto& info) { return info.uuid == nodeUUID; }); if (it != _delayedNodeAdds.end()) { @@ -764,7 +766,7 @@ void LimitedNodeList::removeDelayedAdd(QUuid nodeUUID) { } bool LimitedNodeList::isDelayedNode(QUuid nodeUUID) { - auto it = std::find_if(_delayedNodeAdds.begin(), _delayedNodeAdds.end(), [&](auto info) { + auto it = std::find_if(_delayedNodeAdds.begin(), _delayedNodeAdds.end(), [&](const auto& info) { return info.uuid == nodeUUID; }); return it != _delayedNodeAdds.end(); diff --git a/libraries/networking/src/MessagesClient.h b/libraries/networking/src/MessagesClient.h index 255487f0bb..f3f9387566 100644 --- a/libraries/networking/src/MessagesClient.h +++ b/libraries/networking/src/MessagesClient.h @@ -24,11 +24,11 @@ #include "ReceivedMessage.h" /**jsdoc - *

The Messages API enables text and data to be sent between scripts over named "channels". A channel can have an arbitrary - * name to help separate messaging between different sets of scripts.

+ *

The Messages API enables text and data to be sent between scripts over named "channels". A channel can have + * an arbitrary name to help separate messaging between different sets of scripts.

* - *

Note: If you want to call a function in another script, you should use one of the following rather than - * sending a message:

+ *

Note: To call a function in another script, you should use one of the following rather than sending a + * message:

*