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/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/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/externals/LibOVR/CMakeLists.txt b/cmake/externals/LibOVR/CMakeLists.txt index ae4cf6320e..481753f7e0 100644 --- a/cmake/externals/LibOVR/CMakeLists.txt +++ b/cmake/externals/LibOVR/CMakeLists.txt @@ -17,8 +17,8 @@ if (WIN32) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://public.highfidelity.com/dependencies/ovr_sdk_win_1.26.0_public.zip - URL_MD5 06804ff9727b910dcd04a37c800053b5 + URL https://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.35.0.zip + URL_MD5 1e3e8b2101387af07ff9c841d0ea285e CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LibOVRCMakeLists.txt" /CMakeLists.txt LOG_DOWNLOAD 1 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/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/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/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 5f8cc2eefb..65453ba21d 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -952,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 d1add60647..2fdf7b6960 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" @@ -820,7 +822,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { audioDLLPath += "/audioWin7"; } QCoreApplication::addLibraryPath(audioDLLPath); -#endif +#endif DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); @@ -1813,6 +1815,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 @@ -1861,12 +1865,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(); } { @@ -2337,7 +2335,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; @@ -2365,7 +2363,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); @@ -2405,7 +2403,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 @@ -2443,6 +2441,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(); @@ -2619,6 +2625,8 @@ void Application::onAboutToQuit() { _aboutToQuit = true; cleanupBeforeQuit(); + + getRefreshRateManager().setRefreshRateRegime(RefreshRateManager::RefreshRateRegime::SHUTDOWN); } void Application::cleanupBeforeQuit() { @@ -3228,6 +3236,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); @@ -3376,6 +3385,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 @@ -4047,6 +4057,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; @@ -4108,6 +4121,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; } @@ -4394,6 +4413,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) { @@ -4402,6 +4428,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(); @@ -5569,6 +5598,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) { @@ -5757,9 +5788,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 } @@ -5775,12 +5803,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; } @@ -5824,14 +5846,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); - } } } @@ -6746,6 +6760,11 @@ void Application::updateRenderArgs(float deltaTime) { } } + appRenderArgs._renderArgs._stencilMaskMode = getActiveDisplayPlugin()->getStencilMaskMode(); + if (appRenderArgs._renderArgs._stencilMaskMode == StencilMaskMode::MESH) { + appRenderArgs._renderArgs._stencilMaskOperator = getActiveDisplayPlugin()->getStencilMaskMeshOperator(); + } + { QMutexLocker viewLocker(&_viewMutex); _myCamera.loadViewFrustum(_displayViewFrustum); @@ -7363,6 +7382,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()); @@ -8280,19 +8300,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(), @@ -8433,11 +8440,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) { @@ -8446,19 +8465,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) { @@ -8763,6 +8783,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) { @@ -8852,6 +8873,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(); @@ -9161,7 +9190,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)); } } @@ -9278,6 +9307,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(); @@ -9320,6 +9387,3 @@ void Application::toggleAwayMode(){ #endif - - -#include "Application.moc" diff --git a/interface/src/Application.h b/interface/src/Application.h index e0e8821037..300769b349 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -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" @@ -203,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(); } @@ -345,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); @@ -404,8 +412,6 @@ public slots: static void packageModel(); - void openUrl(const QUrl& url) const; - void resetSensors(bool andReload = false); void setActiveFaceTracker() const; @@ -472,6 +478,9 @@ public slots: QString getGraphicsCardType(); + bool gpuTextureMemSizeStable(); + void showUrlHandler(const QUrl& url); + private slots: void onDesktopRootItemCreated(QQuickItem* qmlContext); void onDesktopRootContextCreated(QQmlContext* qmlContext); @@ -566,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); @@ -717,6 +725,7 @@ private: QUuid _loginDialogID; QUuid _avatarInputsBarID; LoginStateManager _loginStateManager; + RefreshRateManager _refreshRateManager; quint64 _lastFaceTrackerUpdate; @@ -789,6 +798,9 @@ private: AudioInjectorPointer _snapshotSoundInjector; SharedSoundPointer _snapshotSound; SharedSoundPointer _sampleSound; + std::mutex _snapshotMutex; + std::queue _snapshotOperators; + bool _hasPrimarySnapshot { false }; DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin; QString _autoSwitchDisplayModeSupportedHMDPluginName; @@ -811,5 +823,6 @@ private: bool _resumeAfterLoginDialogActionTaken_WasPostponed { false }; bool _resumeAfterLoginDialogActionTaken_SafeToRun { false }; + bool _startUpFinished { false }; }; #endif // hifi_Application_h 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 cf4b377642..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 @@ -498,8 +505,10 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar // on the creation of entities for that avatar instance and the deletion of entities for this instance avatar->removeAvatarEntitiesFromTree(); if (removalReason != KillAvatarReason::AvatarDisconnected) { - emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID()); - emit DependencyManager::get()->enteredIgnoreRadius(); + if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { + emit AvatarInputs::getInstance()->avatarEnteredIgnoreRadius(avatar->getSessionUUID()); + emit DependencyManager::get()->enteredIgnoreRadius(); + } workload::Transaction workloadTransaction; workloadTransaction.remove(avatar->getSpaceIndex()); @@ -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; @@ -940,7 +949,8 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV * 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. + * @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. */ diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 1bddaedc42..98deebe919 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -262,6 +262,15 @@ public slots: */ 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; @@ -299,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 c0cf63d7e4..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), @@ -948,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); } @@ -1041,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. @@ -1199,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"); @@ -1207,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; @@ -1288,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()); @@ -1613,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); + } }); } } @@ -1890,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()); @@ -3172,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) { @@ -4798,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) { @@ -5766,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/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 f8adbd5c12..9701c911b8 100644 --- a/interface/src/scripting/ControllerScriptingInterface.h +++ b/interface/src/scripting/ControllerScriptingInterface.h @@ -284,7 +284,7 @@ public slots: * Disable 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); @@ -293,7 +293,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/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/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/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/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b86c56bb0c..204ed79660 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1469,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); @@ -1535,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 3d14418157..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 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 f460881a45..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") { @@ -1547,6 +1557,7 @@ float AvatarData::getDataRate(const QString& rateName) const { * * "globalPosition"Global position. * "localPosition"Local position. + * "handControllers"Hand controller positions and orientations. * "avatarBoundingBox"Avatar bounding box. * "avatarOrientation"Avatar orientation. * "avatarScale"Avatar scale. @@ -1571,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") { @@ -1633,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; @@ -1987,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); @@ -2027,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; @@ -2048,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); } } @@ -2110,7 +2221,6 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { } _skeletonModelURL = expanded; - if (_clientTraitsHandler) { _clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonModelURL); } @@ -3008,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 79c82d4f29..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 @@ -1076,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; @@ -1087,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); @@ -1168,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++) { @@ -1186,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(); @@ -1223,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", @@ -1254,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()); @@ -1264,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()); @@ -1420,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: @@ -1598,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. @@ -1671,6 +1726,7 @@ protected: RateCounter<> _parseBufferRate; RateCounter<> _globalPositionRate; RateCounter<> _localPositionRate; + RateCounter<> _handControllersRate; RateCounter<> _avatarBoundingBoxRate; RateCounter<> _avatarOrientationRate; RateCounter<> _avatarScaleRate; @@ -1688,6 +1744,7 @@ protected: RateCounter<> _parseBufferUpdateRate; RateCounter<> _globalPositionUpdateRate; RateCounter<> _localPositionUpdateRate; + RateCounter<> _handControllersUpdateRate; RateCounter<> _avatarBoundingBoxUpdateRate; RateCounter<> _avatarOrientationUpdateRate; RateCounter<> _avatarScaleUpdateRate; @@ -1720,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() }; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index a8f1448a3f..29a40c5b6b 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -332,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); @@ -344,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)); @@ -360,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); @@ -367,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); @@ -379,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/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 7e33618ba9..9c5c2c6918 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -40,13 +40,14 @@ * @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. + * @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: 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. * * @property {number} audioLoudness - The instantaneous loudness of the audio input that the avatar is injecting into the diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index b7eb56c921..eb02ac2241 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -31,7 +31,6 @@ #include #include "ModelBakingLoggingCategory.h" -#include "TextureBaker.h" FBXBaker::FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : ModelBaker(inputModelURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { @@ -104,13 +103,15 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const int meshIndex = 0; for (FBXNode& rootChild : _rootNode.children) { if (rootChild.name == "Objects") { - for (auto object = rootChild.children.begin(); object != rootChild.children.end(); object++) { + 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]); meshIndex++; } + object++; } else if (object->name == "Model") { for (FBXNode& modelChild : object->children) { if (modelChild.name == "Properties60" || modelChild.name == "Properties70") { @@ -136,9 +137,33 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const 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()) { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 5daf8a8adf..a528de512d 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -18,15 +18,11 @@ #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 diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 9fcd7d0354..e23b76f73a 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -27,9 +27,10 @@ std::function MaterialBaker::_getNextOvenWorkerThreadOperator; static int materialNum = 0; -MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) : +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++)) { @@ -132,19 +133,24 @@ void MaterialBaker::processMaterial() { QString extension = idx >= 0 ? cleanURL.mid(idx + 1).toLower() : ""; if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { - QPair textureKey(textureURL, type); + TextureKey textureKey(textureURL, type); if (!_textureBakers.contains(textureKey)) { auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), type); QSharedPointer textureBaker { - new TextureBaker(textureURL, type, _textureOutputDir, "", baseTextureFileName, content), + 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 { @@ -164,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(); @@ -172,6 +178,10 @@ 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(relativeURL); @@ -266,4 +276,8 @@ void MaterialBaker::setMaterials(const QHash& materials, 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 ab2a0a5901..04782443f0 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -21,16 +21,21 @@ 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); + 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; } @@ -51,11 +56,12 @@ 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; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index e58ec00afa..9de32158c5 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -167,6 +167,10 @@ void ModelBaker::saveSourceModel() { connect(networkReply, &QNetworkReply::finished, this, &ModelBaker::handleModelNetworkReply); } + + if (_mappingURL.isEmpty()) { + outputUnbakedFST(); + } } void ModelBaker::handleModelNetworkReply() { @@ -237,14 +241,12 @@ 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(); _hfmModel = baker.getHFMModel(); + _materialMapping = baker.getMaterialMapping(); dracoMeshes = baker.getDracoMeshes(); dracoMaterialLists = baker.getDracoMaterialLists(); } @@ -256,7 +258,7 @@ void ModelBaker::bakeSourceCopy() { return; } - if (_hfmModel->materials.size() > 0) { + if (!_hfmModel->materials.isEmpty()) { _materialBaker = QSharedPointer( new MaterialBaker(_modelURL.fileName(), true, _bakedOutputDir), &MaterialBaker::deleteLater @@ -265,7 +267,7 @@ void ModelBaker::bakeSourceCopy() { connect(_materialBaker.data(), &MaterialBaker::finished, this, &ModelBaker::handleFinishedMaterialBaker); _materialBaker->bake(); } else { - outputBakedFST(); + bakeMaterialMap(); } } @@ -281,26 +283,14 @@ void ModelBaker::handleFinishedMaterialBaker() { auto baseName = relativeBakedMaterialURL.left(relativeBakedMaterialURL.lastIndexOf('.')); relativeBakedMaterialURL = baseName + BAKED_MATERIAL_EXTENSION; - // First we add the materials in the model - QJsonArray materialMapping; - for (auto material : _hfmModel->materials) { - QJsonObject json; - json["mat::" + material.name] = relativeBakedMaterialURL + "#" + material.name; - materialMapping.push_back(json); - } - - // The we add any existing mappings from the mapping - if (_mapping.contains(MATERIAL_MAPPING_FIELD)) { - QByteArray materialMapValue = _mapping[MATERIAL_MAPPING_FIELD].toByteArray(); - QJsonObject oldMaterialMapping = QJsonDocument::fromJson(materialMapValue).object(); - for (auto key : oldMaterialMapping.keys()) { + auto materialResource = baker->getNetworkMaterialResource(); + if (materialResource) { + for (auto materialName : materialResource->parsedMaterials.names) { QJsonObject json; - json[key] = oldMaterialMapping[key]; - materialMapping.push_back(json); + json[QString("mat::" + QString(materialName.c_str()))] = relativeBakedMaterialURL + "#" + materialName.c_str(); + _materialMappingJSON.push_back(json); } } - - _mapping[MATERIAL_MAPPING_FIELD] = QJsonDocument(materialMapping).toJson(QJsonDocument::Compact); } 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 @@ -310,7 +300,93 @@ void ModelBaker::handleFinishedMaterialBaker() { handleWarning("Failed to bake the materials for model with URL " + _modelURL.toString()); } - outputBakedFST(); + 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() { @@ -327,6 +403,10 @@ void ModelBaker::outputBakedFST() { outputMapping[FST_VERSION_FIELD] = FST_VERSION; outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName(); 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 }; diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index d612a0a43a..b98d9716e1 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "Baker.h" #include "MaterialBaker.h" @@ -80,13 +81,19 @@ protected slots: void handleModelNetworkReply(); virtual void bakeSourceCopy(); void handleFinishedMaterialBaker(); + void handleFinishedMaterialMapBaker(); private: + void outputUnbakedFST(); void outputBakedFST(); + void bakeMaterialMap(); bool _hasBeenBaked { false }; hfm::Model::Pointer _hfmModel; + MaterialMapping _materialMapping; + int _materialMapIndex { 0 }; + QJsonArray _materialMappingJSON; QSharedPointer _materialBaker; }; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 9bd1431d28..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 { 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/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 57be2f788b..e34ce277e2 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,86 +235,108 @@ 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. - * TOGGLE_PUSHTOTALKnumbernumberDeprecated: Use - * TogglePushToTalk 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 157730e7c6..156fc1af7c 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -76,7 +76,7 @@ namespace controller { * Get 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 a91a952bb4..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 @@ -182,10 +186,11 @@ public: { PROFILE_RANGE(render, "PluginPresent") gl::globalLock(); - currentPlugin->present(); + currentPlugin->present(_refreshRateController); gl::globalRelease(false); CHECK_GL_ERROR(); } + _refreshRateController->sleepThreadIfNeeded(this, currentPlugin->isHmd()); } _context->doneCurrent(); @@ -234,6 +239,7 @@ private: bool _finishedOtherThreadOperation { false }; std::queue _newPluginQueue; gl::Context* _context { nullptr }; + std::shared_ptr _refreshRateController { nullptr }; }; bool OpenGLDisplayPlugin::activate() { @@ -687,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(); @@ -721,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(); @@ -729,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)); } @@ -746,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>(); @@ -785,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; @@ -801,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 d8c0ce8e1d..e952b1e8db 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -48,6 +48,8 @@ public: void pluginUpdate() override {}; + virtual StencilMaskMode getStencilMaskMode() const override { return StencilMaskMode::PAINT; } + signals: void hmdMountedChanged(); void hmdVisibleChanged(bool visible); diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 01d1098daa..9a634a85ad 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -313,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++; } 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 a532064b6c..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; } 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/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 2b13bf3078..3945fe1d8b 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -33,6 +33,7 @@ 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/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/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/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 9eef89d5c9..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); } diff --git a/libraries/material-networking/src/material-networking/MaterialCache.h b/libraries/material-networking/src/material-networking/MaterialCache.h index 4894054de4..7ed0453187 100644 --- a/libraries/material-networking/src/material-networking/MaterialCache.h +++ b/libraries/material-networking/src/material-networking/MaterialCache.h @@ -108,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-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 23b365dd03..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(); @@ -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:

*