diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 25dc547331..779307c19d 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -835,41 +835,40 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { if (audioEnvGroupObject[AUDIO_ZONES].isObject()) { const QJsonObject& zones = audioEnvGroupObject[AUDIO_ZONES].toObject(); - const QString X_RANGE = "x_range"; - const QString Y_RANGE = "y_range"; - const QString Z_RANGE = "z_range"; + const QString X_MIN = "x_min"; + const QString X_MAX = "x_max"; + const QString Y_MIN = "y_min"; + const QString Y_MAX = "y_max"; + const QString Z_MIN = "z_min"; + const QString Z_MAX = "z_max"; foreach (const QString& zone, zones.keys()) { QJsonObject zoneObject = zones[zone].toObject(); - if (zoneObject.contains(X_RANGE) && zoneObject.contains(Y_RANGE) && zoneObject.contains(Z_RANGE)) { - QStringList xRange = zoneObject.value(X_RANGE).toString().split("-", QString::SkipEmptyParts); - QStringList yRange = zoneObject.value(Y_RANGE).toString().split("-", QString::SkipEmptyParts); - QStringList zRange = zoneObject.value(Z_RANGE).toString().split("-", QString::SkipEmptyParts); + if (zoneObject.contains(X_MIN) && zoneObject.contains(X_MAX) && zoneObject.contains(Y_MIN) && + zoneObject.contains(Y_MAX) && zoneObject.contains(Z_MIN) && zoneObject.contains(Z_MAX)) { - if (xRange.size() == 2 && yRange.size() == 2 && zRange.size() == 2) { - float xMin, xMax, yMin, yMax, zMin, zMax; - bool ok, allOk = true; - xMin = xRange[0].toFloat(&ok); - allOk &= ok; - xMax = xRange[1].toFloat(&ok); - allOk &= ok; - yMin = yRange[0].toFloat(&ok); - allOk &= ok; - yMax = yRange[1].toFloat(&ok); - allOk &= ok; - zMin = zRange[0].toFloat(&ok); - allOk &= ok; - zMax = zRange[1].toFloat(&ok); - allOk &= ok; + float xMin, xMax, yMin, yMax, zMin, zMax; + bool ok, allOk = true; + xMin = zoneObject.value(X_MIN).toString().toFloat(&ok); + allOk &= ok; + xMax = zoneObject.value(X_MAX).toString().toFloat(&ok); + allOk &= ok; + yMin = zoneObject.value(Y_MIN).toString().toFloat(&ok); + allOk &= ok; + yMax = zoneObject.value(Y_MAX).toString().toFloat(&ok); + allOk &= ok; + zMin = zoneObject.value(Z_MIN).toString().toFloat(&ok); + allOk &= ok; + zMax = zoneObject.value(Z_MAX).toString().toFloat(&ok); + allOk &= ok; - if (allOk) { - glm::vec3 corner(xMin, yMin, zMin); - glm::vec3 dimensions(xMax - xMin, yMax - yMin, zMax - zMin); - AABox zoneAABox(corner, dimensions); - _audioZones.insert(zone, zoneAABox); - qDebug() << "Added zone:" << zone << "(corner:" << corner - << ", dimensions:" << dimensions << ")"; - } + if (allOk) { + glm::vec3 corner(xMin, yMin, zMin); + glm::vec3 dimensions(xMax - xMin, yMax - yMin, zMax - zMin); + AABox zoneAABox(corner, dimensions); + _audioZones.insert(zone, zoneAABox); + qDebug() << "Added zone:" << zone << "(corner:" << corner + << ", dimensions:" << dimensions << ")"; } } } diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 31cab68cdf..aedf451924 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -236,10 +236,6 @@ OctreeServer::OctreeServer(ReceivedMessage& message) : { _averageLoopTime.updateAverage(0); qDebug() << "Octree server starting... [" << this << "]"; - - // make sure the AccountManager has an Auth URL for payment redemptions - - AccountManager::getInstance().setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); } OctreeServer::~OctreeServer() { diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index b5699cb3b3..ee59f4a3ac 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -271,8 +271,9 @@ FunctionEnd @CPACK_NSIS_PAGE_COMPONENTS@ + Page custom PostInstallOptionsPage ReadPostInstallOptions + !insertmacro MUI_PAGE_INSTFILES - Page custom PostInstallOptionsPage HandlePostInstallOptions !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES @@ -341,6 +342,227 @@ FunctionEnd ;Only for solid compression (by default, solid compression is enabled for BZIP2 and LZMA) ReserveFile "@POST_INSTALL_OPTIONS_PATH@" + ; Make sure nsDialogs is included before we use it + !include "nsdialogs.nsh" + +;-------------------------------- +; Post Install Options + +Var PostInstallDialog +Var DesktopClientCheckbox +Var DesktopServerCheckbox +Var ServerStartupCheckbox +Var LaunchNowCheckbox +Var CurrentOffset +Var OffsetUnits +Var CopyFromProductionCheckbox + +!macro SetPostInstallOption Checkbox OptionName Default + ; reads the value for the given post install option to the registry + ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" + + ${If} $0 == "NO" + ; the value in the registry says it should not be checked + ${NSD_SetState} ${Checkbox} ${BST_UNCHECKED} + ${ElseIf} $0 == "YES" + ; the value in the registry says it should be checked + ${NSD_SetState} ${Checkbox} ${BST_CHECKED} + ${Else} + ; the value in the registry was not in the expected format, use default + ${NSD_SetState} ${Checkbox} ${Default} + ${EndIf} +!macroend + +Function PostInstallOptionsPage + !insertmacro MUI_HEADER_TEXT "Setup Options" "" + + nsDialogs::Create 1018 + Pop $PostInstallDialog + + ${If} $PostInstallDialog == error + Abort + ${EndIf} + + StrCpy $CurrentOffset 0 + StrCpy $OffsetUnits u + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_SHORTCUT_NAME@" + Pop $DesktopClientCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED} + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for High Fidelity @CONSOLE_SHORTCUT_NAME@" + Pop $DesktopServerCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED} + + IntOp $CurrentOffset $CurrentOffset + 15 + + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity @CONSOLE_SHORTCUT_NAME@ on startup" + Pop $ServerStartupCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} + + IntOp $CurrentOffset $CurrentOffset + 15 + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Server Console after install" + ${Else} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity after install" + ${EndIf} + + Pop $LaunchNowCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} + + ${If} @PR_BUILD@ == 1 + ; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED} + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED} + ${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED} + ${EndIf} + + ; push the offset + IntOp $CurrentOffset $CurrentOffset + 15 + + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install" + Pop $CopyFromProductionCheckbox + + ${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED} + ${EndIf} + + nsDialogs::Show +FunctionEnd + +!macro WritePostInstallOption OptionName Option + ; writes the value for the given post install option to the registry + WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option} +!macroend + +Var DesktopClientState +Var DesktopServerState +Var ServerStartupState +Var LaunchNowState +Var CopyFromProductionState + +Function ReadPostInstallOptions + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to High Fidelity + ${NSD_GetState} $DesktopClientCheckbox $DesktopClientState + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to Server Console + ${NSD_GetState} $DesktopServerCheckbox $DesktopServerState + + ; check if the user asked to have Server Console launched every startup + ${NSD_GetState} $ServerStartupCheckbox $ServerStartupState + ${EndIf} + + ${If} @PR_BUILD@ == 1 + ; check if we need to copy settings/content from production for this PR build + ${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState + ${EndIf} + + ; check if we need to launch an application post-install + ${NSD_GetState} $LaunchNowCheckbox $LaunchNowState +FunctionEnd + +Function HandlePostInstallOptions + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to High Fidelity + ${If} $DesktopClientState == ${BST_CHECKED} + CreateShortCut "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@" + !insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES + ${Else} + !insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO + ${EndIf} + + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to Server Console + ${If} $DesktopServerState == ${BST_CHECKED} + CreateShortCut "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" + !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES + ${Else} + !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO + ${EndIf} + + ; check if the user asked to have Server Console launched every startup + ${If} $ServerStartupState == ${BST_CHECKED} + ; in case we added a shortcut in the global context, pull that now + SetShellVarContext all + Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" + + ; make a startup shortcut in this user's current context + SetShellVarContext current + CreateShortCut "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" + + ; reset the shell var context back + SetShellVarContext all + + !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES + ${Else} + !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO + ${EndIf} + ${EndIf} + + ${If} @PR_BUILD@ == 1 + + ; check if we need to copy settings/content from production for this PR build + ${If} $CopyFromProductionState == ${BST_CHECKED} + SetShellVarContext current + + StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@" + + ; we need to copy whatever is in the data folder for production build to the data folder for this build + CreateDirectory $0 + + ClearErrors + + ; copy the data from production build to this PR build + CopyFiles "$APPDATA\High Fidelity\*" $0 + + ; handle an error in copying files + IfErrors 0 NoError + + MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \ + "There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually." + + NoError: + + SetShellVarContext all + ${EndIf} + ${EndIf} + + ${If} $LaunchNowState == ${BST_CHECKED} + !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES + + ; both launches use the explorer trick in case the user has elevated permissions for the installer + ; it won't be possible to use this approach if either application should be launched with a command line param + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"' + ${Else} + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"' + ${EndIf} + ${Else} + !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO + ${EndIf} +FunctionEnd ;-------------------------------- ;Installer Sections @@ -469,233 +691,11 @@ Section "-Core installation" @CPACK_NSIS_EXTRA_INSTALL_COMMANDS@ + ; Handle whichever post install options were set + Call HandlePostInstallOptions + SectionEnd -; Make sure nsDialogs is included before we use it -!include "nsdialogs.nsh" - -Var PostInstallDialog -Var OptionsLabel -Var DesktopClientCheckbox -Var DesktopServerCheckbox -Var ServerStartupCheckbox -Var LaunchNowCheckbox -Var CurrentOffset -Var OffsetUnits -Var CopyFromProductionCheckbox - -!macro SetPostInstallOption Checkbox OptionName Default - ; reads the value for the given post install option to the registry - ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" - - ${If} $0 == "NO" - ; the value in the registry says it should not be checked - ${NSD_SetState} ${Checkbox} ${BST_UNCHECKED} - ${ElseIf} $0 == "YES" - ; the value in the registry says it should be checked - ${NSD_SetState} ${Checkbox} ${BST_CHECKED} - ${Else} - ; the value in the registry was not in the expected format, use default - ${NSD_SetState} ${Checkbox} ${Default} - ${EndIf} -!macroend - -Function PostInstallOptionsPage - ; Set the text on the dialog button to match finish, hide the back and cancel buttons - GetDlgItem $R1 $hwndparent 1 - SendMessage $R1 ${WM_SETTEXT} 0 "STR:&Finish" - - GetDlgItem $R3 $hwndparent 3 - ShowWindow $R3 0 - - nsDialogs::Create 1018 - Pop $PostInstallDialog - - ${If} $PostInstallDialog == error - Abort - ${EndIf} - - ${NSD_CreateLabel} 0 0 100% 12u "Setup Options" - Pop $OptionsLabel - - ; Set label to bold - CreateFont $R2 "Arial" 10 700 - SendMessage $OptionsLabel ${WM_SETFONT} $R2 0 - - ; Force label redraw - ShowWindow $OptionsLabel ${SW_HIDE} - ShowWindow $OptionsLabel ${SW_SHOW} - - StrCpy $CurrentOffset 15 - StrCpy $OffsetUnits u - - ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_SHORTCUT_NAME@" - Pop $DesktopClientCheckbox - IntOp $CurrentOffset $CurrentOffset + 15 - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED} - ${EndIf} - - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for High Fidelity @CONSOLE_SHORTCUT_NAME@" - Pop $DesktopServerCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 - - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity @CONSOLE_SHORTCUT_NAME@ on startup" - Pop $ServerStartupCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 - ${EndIf} - - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Server Console Now" - ${Else} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Now" - ${EndIf} - - Pop $LaunchNowCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} - - ${If} @PR_BUILD@ == 1 - ; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked - ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED} - ${EndIf} - - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED} - ${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED} - ${EndIf} - - ; push the offset - IntOp $CurrentOffset $CurrentOffset + 15 - - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install" - Pop $CopyFromProductionCheckbox - - ${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED} - ${EndIf} - - nsDialogs::Show -FunctionEnd - -!macro WritePostInstallOption OptionName Option - ; writes the value for the given post install option to the registry - WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option} -!macroend - -Var DesktopClientState -Var DesktopServerState -Var ServerStartupState -Var LaunchNowState -Var CopyFromProductionState - -Function HandlePostInstallOptions - ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ; check if the user asked for a desktop shortcut to High Fidelity - ${NSD_GetState} $DesktopClientCheckbox $DesktopClientState - - ${If} $DesktopClientState == ${BST_CHECKED} - CreateShortCut "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@" - !insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES - ${Else} - !insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO - ${EndIf} - - ${EndIf} - - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ; check if the user asked for a desktop shortcut to Server Console - ${NSD_GetState} $DesktopServerCheckbox $DesktopServerState - - ${If} $DesktopServerState == ${BST_CHECKED} - CreateShortCut "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" - !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES - ${Else} - !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO - ${EndIf} - - ; check if the user asked to have Server Console launched every startup - ${NSD_GetState} $ServerStartupCheckbox $ServerStartupState - - ${If} $ServerStartupState == ${BST_CHECKED} - ; in case we added a shortcut in the global context, pull that now - SetShellVarContext all - Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" - - ; make a startup shortcut in this user's current context - SetShellVarContext current - CreateShortCut "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" - - ; reset the shell var context back - SetShellVarContext all - - !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES - ${Else} - !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO - ${EndIf} - ${EndIf} - - ${If} @PR_BUILD@ == 1 - - ; check if we need to copy settings/content from production for this PR build - ${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState - - ${If} $CopyFromProductionState == ${BST_CHECKED} - SetShellVarContext current - - StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@" - - ; we need to copy whatever is in the data folder for production build to the data folder for this build - CreateDirectory $0 - - ClearErrors - - ; copy the data from production build to this PR build - CopyFiles "$APPDATA\High Fidelity\*" $0 - - ; handle an error in copying files - IfErrors 0 NoError - - MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \ - "There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually." - - NoError: - - SetShellVarContext all - ${EndIf} - ${EndIf} - - ; check if we need to launch an application post-install - ${NSD_GetState} $LaunchNowCheckbox $LaunchNowState - - ${If} $LaunchNowState == ${BST_CHECKED} - !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES - - ; both launches use the explorer trick in case the user has elevated permissions for the installer - ; it won't be possible to use this approach if either application should be launched with a command line param - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"' - ${Else} - Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"' - ${EndIf} - ${Else} - !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO - ${EndIf} - -FunctionEnd - !include nsProcess.nsh !macro PromptForRunningApplication applicationName displayName action prompter diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 014d9f3767..cde398e01f 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -230,22 +230,40 @@ }, "columns": [ { - "name": "x_range", - "label": "X range", + "name": "x_min", + "label": "X start", "can_set": true, - "placeholder": "0-16384" + "placeholder": "-16384.0" }, { - "name": "y_range", - "label": "Y range", + "name": "x_max", + "label": "X end", "can_set": true, - "placeholder": "0-16384" + "placeholder": "16384.0" + }, + { + "name": "y_min", + "label": "Y start", + "can_set": true, + "placeholder": "-16384.0" }, { - "name": "z_range", - "label": "Z range", + "name": "y_max", + "label": "Y end", "can_set": true, - "placeholder": "0-16384" + "placeholder": "16384.0" + }, + { + "name": "z_min", + "label": "Z start", + "can_set": true, + "placeholder": "-16384.0" + }, + { + "name": "z_max", + "label": "Z end", + "can_set": true, + "placeholder": "16384.0" } ] }, diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 7dc94421be..fae07ace45 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -778,7 +778,7 @@ function chooseFromHighFidelityDomains(clickedButton) { function createTemporaryDomain() { swal({ title: 'Create temporary place name', - text: "This will create a temporary place name and domain ID (valid for 30 days)" + text: "This will create a temporary place name and domain ID" + " so other users can easily connect to your domain.

" + "In order to make your domain reachable, this will also enable full automatic networking.", showCancelButton: true, diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 3e4ee7b758..f385f5c489 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -331,7 +331,6 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, QCryptographicHash::Sha256); if (rsaPublicKey) { - QByteArray decryptedArray(RSA_size(rsaPublicKey), 0); int decryptResult = RSA_verify(NID_sha256, reinterpret_cast(usernameWithToken.constData()), usernameWithToken.size(), diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 9e13c8e6fa..f0df67a6f6 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -96,7 +96,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : // make sure we hear about newly connected nodes from our gatekeeper connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); - if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) { + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to @@ -198,7 +198,6 @@ bool DomainServer::optionallySetupOAuth() { } AccountManager& accountManager = AccountManager::getInstance(); - accountManager.disableSettingsFilePersistence(); accountManager.setAuthURL(_oauthProviderURL); _oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString(); @@ -372,20 +371,12 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { packetReceiver.registerListener(PacketType::ICEPing, &_gatekeeper, "processICEPingPacket"); packetReceiver.registerListener(PacketType::ICEPingReply, &_gatekeeper, "processICEPingReplyPacket"); packetReceiver.registerListener(PacketType::ICEServerPeerInformation, &_gatekeeper, "processICEPeerInformationPacket"); + packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket"); // add whatever static assignments that have been parsed to the queue addStaticAssignmentsToQueue(); } -bool DomainServer::didSetupAccountManagerWithAccessToken() { - if (AccountManager::getInstance().hasValidAccessToken()) { - // we already gave the account manager a valid access token - return true; - } - - return resetAccountManagerAccessToken(); -} - const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; bool DomainServer::resetAccountManagerAccessToken() { @@ -401,9 +392,13 @@ bool DomainServer::resetAccountManagerAccessToken() { if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) { accessToken = accessTokenVariant->toString(); } else { - qDebug() << "A domain-server feature that requires authentication is enabled but no access token is present." - << "Set an access token via the web interface, in your user or master config" + qDebug() << "A domain-server feature that requires authentication is enabled but no access token is present."; + qDebug() << "Set an access token via the web interface, in your user or master config" << "at keypath metaverse.access_token or in your ENV at key DOMAIN_SERVER_ACCESS_TOKEN"; + + // clear any existing access token from AccountManager + AccountManager::getInstance().setAccessTokenForCurrentAuthURL(QString()); + return false; } } else { @@ -429,34 +424,6 @@ bool DomainServer::resetAccountManagerAccessToken() { } } -bool DomainServer::optionallySetupAssignmentPayment() { - const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments"; - const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - - if (settingsMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) && - settingsMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() && - didSetupAccountManagerWithAccessToken()) { - - qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString()); - - // assume that the fact we are authing against HF data server means we will pay for assignments - // setup a timer to send transactions to pay assigned nodes every 30 seconds - QTimer* creditSetupTimer = new QTimer(this); - connect(creditSetupTimer, &QTimer::timeout, this, &DomainServer::setupPendingAssignmentCredits); - - const qint64 CREDIT_CHECK_INTERVAL_MSECS = 5 * 1000; - creditSetupTimer->start(CREDIT_CHECK_INTERVAL_MSECS); - - QTimer* nodePaymentTimer = new QTimer(this); - connect(nodePaymentTimer, &QTimer::timeout, this, &DomainServer::sendPendingTransactionsToServer); - - const qint64 TRANSACTION_SEND_INTERVAL_MSECS = 30 * 1000; - nodePaymentTimer->start(TRANSACTION_SEND_INTERVAL_MSECS); - } - - return true; -} - void DomainServer::setupAutomaticNetworking() { auto nodeList = DependencyManager::get(); @@ -467,9 +434,9 @@ void DomainServer::setupAutomaticNetworking() { setupICEHeartbeatForFullNetworking(); } - if (!didSetupAccountManagerWithAccessToken()) { - qDebug() << "Cannot send heartbeat to data server without an access token."; - qDebug() << "Add an access token to your config file or via the web interface."; + if (!resetAccountManagerAccessToken()) { + qDebug() << "Will not send heartbeat to Metaverse API without an access token."; + qDebug() << "If this is not a temporary domain add an access token to your config file or via the web interface."; return; } @@ -526,6 +493,19 @@ void DomainServer::setupICEHeartbeatForFullNetworking() { // we need this DS to know what our public IP is - start trying to figure that out now limitedNodeList->startSTUNPublicSocketUpdate(); + // to send ICE heartbeats we'd better have a private key locally with an uploaded public key + auto& accountManager = AccountManager::getInstance(); + auto domainID = accountManager.getAccountInfo().getDomainID(); + + // if we have an access token and we don't have a private key or the current domain ID has changed + // we should generate a new keypair + if (!accountManager.getAccountInfo().hasPrivateKey() || domainID != limitedNodeList->getSessionUUID()) { + accountManager.generateNewDomainKeypair(limitedNodeList->getSessionUUID()); + } + + // hookup to the signal from account manager that tells us when keypair is available + connect(&accountManager, &AccountManager::newKeypair, this, &DomainServer::handleKeypairChange); + if (!_iceHeartbeatTimer) { // setup a timer to heartbeat with the ice-server every so often _iceHeartbeatTimer = new QTimer { this }; @@ -1082,11 +1062,76 @@ void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) { domainUpdateJSON.toUtf8()); } -// TODO: have data-web respond with ice-server hostname to use - void DomainServer::sendHeartbeatToIceServer() { if (!_iceServerSocket.getAddress().isNull()) { - DependencyManager::get()->sendHeartbeatToIceServer(_iceServerSocket); + + auto& accountManager = AccountManager::getInstance(); + auto limitedNodeList = DependencyManager::get(); + + if (!accountManager.getAccountInfo().hasPrivateKey()) { + qWarning() << "Cannot send an ice-server heartbeat without a private key for signature."; + qWarning() << "Waiting for keypair generation to complete before sending ICE heartbeat."; + + if (!limitedNodeList->getSessionUUID().isNull()) { + accountManager.generateNewDomainKeypair(limitedNodeList->getSessionUUID()); + } else { + qWarning() << "Attempting to send ICE server heartbeat with no domain ID. This is not supported"; + } + + return; + } + + // NOTE: I'd love to specify the correct size for the packet here, but it's a little trickey with + // QDataStream and the possibility of IPv6 address for the sockets. + if (!_iceServerHeartbeatPacket) { + _iceServerHeartbeatPacket = NLPacket::create(PacketType::ICEServerHeartbeat); + } + + bool shouldRecreatePacket = false; + + if (_iceServerHeartbeatPacket->getPayloadSize() > 0) { + // if either of our sockets have changed we need to re-sign the heartbeat + // first read the sockets out from the current packet + _iceServerHeartbeatPacket->seek(0); + QDataStream heartbeatStream(_iceServerHeartbeatPacket.get()); + + QUuid senderUUID; + HifiSockAddr publicSocket, localSocket; + heartbeatStream >> senderUUID >> publicSocket >> localSocket; + + if (senderUUID != limitedNodeList->getSessionUUID() + || publicSocket != limitedNodeList->getPublicSockAddr() + || localSocket != limitedNodeList->getLocalSockAddr()) { + shouldRecreatePacket = true; + } + } else { + shouldRecreatePacket = true; + } + + if (shouldRecreatePacket) { + // either we don't have a heartbeat packet yet or some combination of sockets, ID and keypair have changed + // and we need to make a new one + + // reset the position in the packet before writing + _iceServerHeartbeatPacket->reset(); + + // write our plaintext data to the packet + QDataStream heartbeatDataStream(_iceServerHeartbeatPacket.get()); + heartbeatDataStream << limitedNodeList->getSessionUUID() + << limitedNodeList->getPublicSockAddr() << limitedNodeList->getLocalSockAddr(); + + // setup a QByteArray that points to the plaintext data + auto plaintext = QByteArray::fromRawData(_iceServerHeartbeatPacket->getPayload(), _iceServerHeartbeatPacket->getPayloadSize()); + + // generate a signature for the plaintext data in the packet + auto signature = accountManager.getAccountInfo().signPlaintext(plaintext); + + // pack the signature with the data + heartbeatDataStream << signature; + } + + // send the heartbeat packet to the ice server now + limitedNodeList->sendUnreliablePacket(*_iceServerHeartbeatPacket, _iceServerSocket); } } @@ -1970,3 +2015,31 @@ void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer message) { + static const int NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN = 3; + + static int numHeartbeatDenials = 0; + if (++numHeartbeatDenials > NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN) { + qDebug() << "Received" << NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN << "heartbeat denials from ice-server" + << "- re-generating keypair now"; + + // we've hit our threshold of heartbeat denials, trigger a keypair re-generation + auto limitedNodeList = DependencyManager::get(); + AccountManager::getInstance().generateNewDomainKeypair(limitedNodeList->getSessionUUID()); + + // reset our number of heartbeat denials + numHeartbeatDenials = 0; + } +} + +void DomainServer::handleKeypairChange() { + if (_iceServerHeartbeatPacket) { + // reset the payload size of the ice-server heartbeat packet - this causes the packet to be re-generated + // the next time we go to send an ice-server heartbeat + _iceServerHeartbeatPacket->setPayloadSize(0); + + // send a heartbeat to the ice server immediately + sendHeartbeatToIceServer(); + } +} diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 326ca3e1a8..3a83e8696b 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -61,6 +61,7 @@ public slots: void processNodeJSONStatsPacket(QSharedPointer packetList, SharedNodePointer sendingNode); void processPathQueryPacket(QSharedPointer packet); void processNodeDisconnectRequestPacket(QSharedPointer message); + void processICEServerHeartbeatDenialPacket(QSharedPointer message); private slots: void aboutToQuit(); @@ -78,16 +79,16 @@ private slots: void handleTempDomainError(QNetworkReply& requestReply); void queuedQuit(QString quitMessage, int exitCode); + + void handleKeypairChange(); private: void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid()); bool optionallySetupOAuth(); bool optionallyReadX509KeyAndCertificate(); - bool optionallySetupAssignmentPayment(); void optionallyGetTemporaryName(const QStringList& arguments); - bool didSetupAccountManagerWithAccessToken(); bool resetAccountManagerAccessToken(); void setupAutomaticNetworking(); @@ -153,6 +154,7 @@ private: DomainServerSettingsManager _settingsManager; HifiSockAddr _iceServerSocket; + std::unique_ptr _iceServerHeartbeatPacket; QTimer* _iceHeartbeatTimer { nullptr }; // this looks like it dangles when created but it's parented to the DomainServer diff --git a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js index f7e983d683..2be59365b8 100644 --- a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js +++ b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js @@ -1,218 +1,260 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true*/ +/*global AvatarList, Entities, EntityViewer, Script, SoundCache, Audio, print, randFloat*/ // // ACAudioSearchAndInject.js // audio // -// Created by Eric Levin 2/1/2016 +// Created by Eric Levin and Howard Stearns 2/1/2016 // Copyright 2016 High Fidelity, Inc. - -// This AC script searches for special sound entities nearby avatars and plays those sounds based off information specified in the entity's -// user data field ( see acAudioSearchAndCompatibilityEntitySpawner.js for an example) +// +// Keeps track of all sounds within QUERY_RADIUS of an avatar, where a "sound" is specified in entity userData. +// Inject as many as practical into the audio mixer. +// See acAudioSearchAndCompatibilityEntitySpawner.js. +// +// This implementation takes some precautions to scale well: +// - It doesn't hastle the entity server because it issues at most one octree query every UPDATE_TIME period, regardless of the number of avatars. +// - It does not load itself because it only gathers entities once every UPDATE_TIME period, and only +// checks entity properties for those small number of entities that are currently playing (plus a RECHECK_TIME period examination of all entities). +// This implementation tries to use all the available injectors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -Script.include("https://rawgit.com/highfidelity/hifi/master/examples/libraries/utils.js"); - -var SOUND_DATA_KEY = "soundKey"; - -var QUERY_RADIUS = 50; - -EntityViewer.setKeyholeRadius(QUERY_RADIUS); -Entities.setPacketsPerSecond(6000); - -Agent.isAvatar = true; +var MSEC_PER_SEC = 1000; +var SOUND_DATA_KEY = "io.highfidelity.soundKey"; // Sound data is specified in userData under this key. +var old_sound_data_key = "soundKey"; // For backwards compatibility. +var QUERY_RADIUS = 50; // meters +var UPDATE_TIME = 100; // ms. We'll update just one thing on this period. +var EXPIRATION_TIME = 5 * MSEC_PER_SEC; // ms. Remove sounds that have been out of range for this time. +var RECHECK_TIME = 10 * MSEC_PER_SEC; // ms. Check for new userData properties this often when not currently playing. +// (By not checking most of the time when not playing, we can efficiently go through all entities without getEntityProperties.) +var UPDATES_PER_STATS_LOG = RECHECK_TIME / UPDATE_TIME; // (It's nice to smooth out the results by straddling a recheck.) var DEFAULT_SOUND_DATA = { - volume: 0.5, - loop: false, - playbackGap: 1000, // in ms + volume: 0.5, // userData cannot specify zero volume with our current method of defaulting. + loop: false, // Default must be false with our current method of defaulting, else there's no way to get a false value. + playbackGap: MSEC_PER_SEC, // in ms playbackGapRange: 0 // in ms }; -var MIN_PLAYBACK_GAP = 0; -var UPDATE_TIME = 100; -var EXPIRATION_TIME = 5000; +Script.include("../../libraries/utils.js"); +Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList. +function ignore() {} +function debug() { // Display the arguments not just [Object object]. + //print.apply(null, [].map.call(arguments, JSON.stringify)); +} -var soundEntityMap = {}; -var soundUrls = {}; +EntityViewer.setKeyholeRadius(QUERY_RADIUS); -var avatarPositions = []; - - -function update() { - var avatars = AvatarList.getAvatarIdentifiers(); - for (var i = 0; i < avatars.length; i++) { - var avatar = AvatarList.getAvatar(avatars[i]); - var avatarPosition = avatar.position; - if (!avatarPosition) { - continue; +// ENTITY DATA CACHE +// +var entityCache = {}; // A dictionary of unexpired EntityData objects. +var examinationCount = 0; +function EntityDatum(entityIdentifier) { // Just the data of an entity that we need to know about. + // This data is only use for our sound injection. There is no need to store such info in the replicated entity on everyone's computer. + var that = this; + that.lastUserDataUpdate = 0; // new entity is in need of rechecking user data + // State Transitions: + // no data => no data | sound data | expired + // expired => stop => remove + // sound data => downloading + // downloading => downloading | waiting + // waiting => playing | waiting (if too many already playing) + // playing => update position etc | no data + that.stop = function stop() { + if (!that.sound) { + return; } - EntityViewer.setPosition(avatarPosition); - EntityViewer.queryOctree(); - avatarPositions.push(avatarPosition); + print("stopping sound", entityIdentifier, that.url); + delete that.sound; + delete that.url; + if (!that.injector) { + return; + } + that.injector.stop(); + delete that.injector; + }; + this.update = function stateTransitions(expirationCutoff, userDataCutoff, now) { + if (that.timestamp < expirationCutoff) { // EXPIRED => STOP => REMOVE + that.stop(); // Alternatively, we could fade out and then stop... + delete entityCache[entityIdentifier]; + return; + } + var properties, soundData; // Latest data, pulled from local octree. + // getEntityProperties locks the tree, which competes with the asynchronous processing of queryOctree results. + // Most entity updates are fast and only a very few do getEntityProperties. + function ensureSoundData() { // We only getEntityProperities when we need to. + if (properties) { + return; + } + properties = Entities.getEntityProperties(entityIdentifier, ['userData', 'position']); + examinationCount++; // Collect statistics on how many getEntityProperties we do. + debug("updating", that, properties); + try { + var userData = properties.userData && JSON.parse(properties.userData); + soundData = userData && (userData[SOUND_DATA_KEY] || userData[old_sound_data_key]); // Don't store soundData yet. Let state changes compare. + that.lastUserDataUpdate = now; // But do update these ... + that.url = soundData && soundData.url; + that.playAfter = that.url && now; + } catch (err) { + print(err, properties.userData); + } + } + // Stumbling on big new pile of entities will do a lot of getEntityProperties. Once. + if (that.lastUserDataUpdate < userDataCutoff) { // NO DATA => SOUND DATA + ensureSoundData(); + } + if (!that.url) { // NO DATA => NO DATA + return that.stop(); + } + if (!that.sound) { // SOUND DATA => DOWNLOADING + that.sound = SoundCache.getSound(soundData.url); // SoundCache can manage duplicates better than we can. + } + if (!that.sound.downloaded) { // DOWNLOADING => DOWNLOADING + return; + } + if (that.playAfter > now) { // DOWNLOADING | WAITING => WAITING + return; + } + ensureSoundData(); // We'll try to play/setOptions and will need position, so we might as well get soundData, too. + if (soundData.url !== that.url) { // WAITING => NO DATA (update next time around) + return that.stop(); + } + var options = { + position: properties.position, + loop: soundData.loop || DEFAULT_SOUND_DATA.loop, + volume: soundData.volume || DEFAULT_SOUND_DATA.volume + }; + function repeat() { + return !options.loop && (soundData.playbackGap >= 0); + } + function randomizedNextPlay() { // time of next play or recheck, randomized to distribute the work + var range = soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange, + base = repeat() ? ((that.sound.duration * MSEC_PER_SEC) + (soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap)) : RECHECK_TIME; + return now + base + randFloat(-Math.min(base, range), range); + } + if (!that.injector) { // WAITING => PLAYING | WAITING + debug("starting", that, options); + that.injector = Audio.playSound(that.sound, options); // Might be null if at at injector limit. Will try again later. + if (that.injector) { + print("started", entityIdentifier, that.url); + } else { // Don't hammer ensureSoundData or injector manager. + that.playAfter = randomizedNextPlay(); + } + return; + } + that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC + if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap. + if (repeat()) { // WAITING => PLAYING + // Setup next play just once, now. Changes won't be looked at while we wait. + that.playAfter = randomizedNextPlay(); + // Subtle: if the restart fails b/c we're at injector limit, we won't try again until next playAfter. + that.injector.restart(); + } else { // PLAYING => NO DATA + that.playAfter = Infinity; // was one-shot and we're finished + } + } + }; +} +function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar) { + ignore(avatarPosition, avatar); // We could use avatars and/or avatarPositions to prioritize which ones to play. + var entitySound = entityCache[entityIdentifier]; + if (!entitySound) { + entitySound = entityCache[entityIdentifier] = new EntityDatum(entityIdentifier); } - Script.setTimeout(function() { - avatarPositions.forEach(function(avatarPosition) { - var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS); - handleFoundSoundEntities(entities); + entitySound.timestamp = timestamp; // Might be updated for multiple avatars. That's fine. +} +var nUpdates = UPDATES_PER_STATS_LOG, lastStats = Date.now(); +function updateAllEntityData() { // A fast update of all entities we know about. A few make sounds. + var now = Date.now(), + expirationCutoff = now - EXPIRATION_TIME, + userDataRecheckCutoff = now - RECHECK_TIME; + Object.keys(entityCache).forEach(function (entityIdentifier) { + entityCache[entityIdentifier].update(expirationCutoff, userDataRecheckCutoff, now); + }); + if (nUpdates-- <= 0) { // Report statistics. + // For example, with: + // injector-limit = 40 (in C++ code) + // N_SOUNDS = 1000 (from userData in, e.g., acAudioSearchCompatibleEntitySpawner.js) + // replay-period = 3 + 20 = 23 (seconds, ditto) + // stats-period = UPDATES_PER_STATS_LOG * UPDATE_TIME / MSEC_PER_SEC = 10 seconds + // The log should show between each stats report: + // "start" lines ~= injector-limit * P(finish) = injector-limit * stats-period/replay-period = 17 ? + // total attempts at starting ("start" lines + "could not thread" lines) ~= N_SOUNDS = 1000 ? + // entities > N_SOUNDS * (1+ N_SILENT_ENTITIES_PER_SOUND) = 11000 + whatever was in the scene before running spawner + // sounds = N_SOUNDS = 1000 + // getEntityPropertiesPerUpdate ~= playing + failed-starts/UPDATES_PER_STATS_LOG + other-rechecks-each-update + // = injector-limit + (total attempts - "start" lines)/UPDATES_PER_STATS__LOG + // + (entities - playing - failed-starts/UPDATES_PER_STATS_LOG) * P(recheck-in-update) + // where failed-starts/UPDATES_PER_STATS_LOG = (1000-17)/100 = 10 + // = 40 + 10 + (11000 - 40 - 10)*UPDATE_TIME/RECHECK_TIME + // = 40 + 10 + 10950*0.01 = 159 (mostly proportional to enties/RECHECK_TIME) + // millisecondsPerUpdate ~= UPDATE_TIME = 100 (+ some timer machinery time) + // this assignment client activity monitor < 100% cpu + var stats = { + entities: 0, + sounds: 0, + playing: 0, + getEntityPropertiesPerUpdate: examinationCount / UPDATES_PER_STATS_LOG, + millisecondsPerUpdate: (now - lastStats) / UPDATES_PER_STATS_LOG + }; + nUpdates = UPDATES_PER_STATS_LOG; + lastStats = now; + examinationCount = 0; + Object.keys(entityCache).forEach(function (entityIdentifier) { + var datum = entityCache[entityIdentifier]; + stats.entities++; + if (datum.url) { + stats.sounds++; + if (datum.injector && datum.injector.isPlaying) { + stats.playing++; + } + } }); - //Now wipe list for next query; - avatarPositions = []; - }, UPDATE_TIME); - handleActiveSoundEntities(); -} - -function handleActiveSoundEntities() { - // Go through all our sound entities, if they have passed expiration time, remove them from map - for (var potentialSoundEntity in soundEntityMap) { - if (!soundEntityMap.hasOwnProperty(potentialSoundEntity)) { - // The current property is not a direct property of soundEntityMap so ignore it - continue; - } - var soundEntity = potentialSoundEntity; - var soundProperties = soundEntityMap[soundEntity]; - soundProperties.timeWithoutAvatarInRange += UPDATE_TIME; - if (soundProperties.timeWithoutAvatarInRange > EXPIRATION_TIME && soundProperties.soundInjector) { - // An avatar hasn't been within range of this sound entity recently, so remove it from map - soundProperties.soundInjector.stop(); - delete soundEntityMap[soundEntity]; - } else if (soundProperties.isDownloaded) { - // If this sound hasn't expired yet, we want to potentially play it! - if (soundProperties.readyToPlay) { - var newPosition = Entities.getEntityProperties(soundEntity, "position").position; - if (!soundProperties.soundInjector) { - soundProperties.soundInjector = Audio.playSound(soundProperties.sound, { - volume: soundProperties.volume, - position: newPosition, - loop: soundProperties.loop - }); - } else { - soundProperties.soundInjector.restart(); - } - soundProperties.readyToPlay = false; - } else if (soundProperties.sound && soundProperties.loop === false) { - // We need to check all of our entities that are not looping but have an interval associated with them - // to see if it's time for them to play again - soundProperties.timeSinceLastPlay += UPDATE_TIME; - if (soundProperties.timeSinceLastPlay > soundProperties.clipDuration + soundProperties.currentPlaybackGap) { - soundProperties.readyToPlay = true; - soundProperties.timeSinceLastPlay = 0; - // Now let's get our new current interval - soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange); - soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap); - } - } - } + print(JSON.stringify(stats)); } } - -function handleFoundSoundEntities(entities) { - entities.forEach(function(entity) { - var soundData = getEntityCustomData(SOUND_DATA_KEY, entity); - if (soundData && soundData.url) { - //check sound entities list- if it's not in, add it - if (!soundEntityMap[entity]) { - var soundProperties = { - url: soundData.url, - volume: soundData.volume || DEFAULT_SOUND_DATA.volume, - loop: soundData.loop || DEFAULT_SOUND_DATA.loop, - playbackGap: soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap, - playbackGapRange: soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange, - readyToPlay: false, - position: Entities.getEntityProperties(entity, "position").position, - timeSinceLastPlay: 0, - timeWithoutAvatarInRange: 0, - isDownloaded: false - }; - - - soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange); - soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap); - - - soundEntityMap[entity] = soundProperties; - if (!soundUrls[soundData.url]) { - // We need to download sound before we add it to our map - var sound = SoundCache.getSound(soundData.url); - // Only add it to map once it's downloaded - soundUrls[soundData.url] = sound; - sound.ready.connect(function() { - soundProperties.sound = sound; - soundProperties.readyToPlay = true; - soundProperties.isDownloaded = true; - soundProperties.clipDuration = sound.duration * 1000; - soundEntityMap[entity] = soundProperties; - - }); - } else { - // We already have sound downloaded, so just add it to map right away - soundProperties.sound = soundUrls[soundData.url]; - soundProperties.clipDuration = soundProperties.sound.duration * 1000; - soundProperties.readyToPlay = true; - soundProperties.isDownloaded = true; - soundEntityMap[entity] = soundProperties; - } - } else { - //If this sound is in our map already, we want to reset timeWithoutAvatarInRange - // Also we want to check to see if the entity has been updated with new sound data- if so we want to update! - soundEntityMap[entity].timeWithoutAvatarInRange = 0; - checkForSoundPropertyChanges(soundEntityMap[entity], soundData); - } - } +// Update the set of which EntityData we know about. +// +function updateEntiesForAvatar(avatarIdentifier) { // Just one piece of update work. + // This does at most: + // one queryOctree request of the entity server, and + // one findEntities geometry query of our own octree, and + // a quick internEntityDatum of each of what may be a large number of entityIdentifiers. + // The idea is that this is a nice bounded piece of work that should not be done too frequently. + // However, it means that we won't learn about new entities until, on average (nAvatars * UPDATE_TIME) + query round trip. + var avatar = AvatarList.getAvatar(avatarIdentifier), avatarPosition = avatar && avatar.position; + if (!avatarPosition) { // No longer here. + return; + } + var timestamp = Date.now(); + EntityViewer.setPosition(avatarPosition); + EntityViewer.queryOctree(); // Requests an update, but there's no telling when we'll actually see different results. + var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS); + debug("found", entities.length, "entities near", avatar.name || "unknown", "at", avatarPosition); + entities.forEach(function (entityIdentifier) { + internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar); }); } -function checkForSoundPropertyChanges(currentProps, newProps) { - var needsNewInjector = false; - - if (currentProps.playbackGap !== newProps.playbackGap && !currentProps.loop) { - // playbackGap only applies to non looping sounds - currentProps.playbackGap = newProps.playbackGap; - currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange); - currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap); - currentProps.readyToPlay = true; - } - - if (currentProps.playbackGapRange !== currentProps.playbackGapRange) { - currentProps.playbackGapRange = newProps.playbackGapRange; - currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange); - currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap); - currentProps.readyToPlay = true; - } - if (currentProps.volume !== newProps.volume) { - currentProps.volume = newProps.volume; - needsNewInjector = true; - } - if (currentProps.url !== newProps.url) { - currentProps.url = newProps.url; - currentProps.sound = null; - if (!soundUrls[currentProps.url]) { - var sound = SoundCache.getSound(currentProps.url); - currentProps.isDownloaded = false; - sound.ready.connect(function() { - currentProps.sound = sound; - currentProps.clipDuration = sound.duration * 1000; - currentProps.isDownloaded = true; - }); - } else { - currentProps.sound = sound; - currentProps.clipDuration = sound.duration * 1000; - } - needsNewInjector = true; - } - - if (currentProps.loop !== newProps.loop) { - currentProps.loop = newProps.loop; - needsNewInjector = true; - } - if (needsNewInjector) { - // If we were looping we need to stop that so new changes are applied - currentProps.soundInjector.stop(); - currentProps.soundInjector = null; - currentProps.readyToPlay = true; - } - +// Slowly update the set of data we have to work with. +// +var workQueue = []; +function updateWorkQueueForAvatarsPresent() { // when nothing else to do, fill queue with individual avatar updates + workQueue = AvatarList.getAvatarIdentifiers().map(function (avatarIdentifier) { + return function () { + updateEntiesForAvatar(avatarIdentifier); + }; + }); } - -Script.setInterval(update, UPDATE_TIME); +Script.setInterval(function () { + // There might be thousands of EntityData known to us, but only a few will require any work to update. + updateAllEntityData(); // i.e., this better be pretty fast. + // Each interval, we do no more than one updateEntitiesforAvatar. + if (!workQueue.length) { + workQueue = [updateWorkQueueForAvatarsPresent]; + } + workQueue.pop()(); // There's always one +}, UPDATE_TIME); diff --git a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js index 126635ee7a..2a80a712b6 100644 --- a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js +++ b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js @@ -1,4 +1,6 @@ -// +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true*/ +/*global Entities, Script, Quat, Vec3, Camera, MyAvatar, print, randFloat*/ // acAudioSearchCompatibleEntitySpawner.js // audio/acAudioSearching // @@ -13,6 +15,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +var N_SOUNDS = 1000; +var N_SILENT_ENTITIES_PER_SOUND = 10; +var ADD_PERIOD = 50; // ms between adding 1 sound + N_SILENT_ENTITIES_PER_SOUND, to not overrun entity server. +var SPATIAL_DISTRIBUTION = 10; // meters spread over how far to randomly distribute enties. Script.include("../../libraries/utils.js"); var orientation = Camera.getOrientation(); orientation = Quat.safeEulerAngles(orientation); @@ -20,20 +26,21 @@ orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); // http://hifi-public.s3.amazonaws.com/ryan/demo/0619_Fireplace__Tree_B.L.wav -var SOUND_DATA_KEY = "soundKey"; +var SOUND_DATA_KEY = "io.highfidelity.soundKey"; var userData = { soundKey: { url: "http://hifi-content.s3.amazonaws.com/DomainContent/Junkyard/Sounds/ClothSail/cloth_sail3.L.wav", volume: 0.3, loop: false, - playbackGap: 2000, // In ms - time to wait in between clip plays - playbackGapRange: 500 // In ms - the range to wait in between clip plays + playbackGap: 20000, // In ms - time to wait in between clip plays + playbackGapRange: 5000 // In ms - the range to wait in between clip plays } -} +}; +var userDataString = JSON.stringify(userData); var entityProps = { type: "Box", - position: center, + name: 'audioSearchEntity', color: { red: 200, green: 10, @@ -43,15 +50,41 @@ var entityProps = { x: 0.1, y: 0.1, z: 0.1 - }, - userData: JSON.stringify(userData) + } +}; + +var entities = [], nSounds = 0; +Script.include("../../libraries/utils.js"); +function addOneSet() { + function randomizeDimension(coordinate) { + return coordinate + randFloat(-SPATIAL_DISTRIBUTION / 2, SPATIAL_DISTRIBUTION / 2); + } + function randomize() { + return {x: randomizeDimension(center.x), y: randomizeDimension(center.y), z: randomizeDimension(center.z)}; + } + function addOne() { + entityProps.position = randomize(); + entities.push(Entities.addEntity(entityProps)); + } + var i; + entityProps.userData = userDataString; + entityProps.color.red = 200; + addOne(); + delete entityProps.userData; + entityProps.color.red = 10; + for (i = 0; i < N_SILENT_ENTITIES_PER_SOUND; i++) { + addOne(); + } + if (++nSounds < N_SOUNDS) { + Script.setTimeout(addOneSet, ADD_PERIOD); + } } - -var soundEntity = Entities.addEntity(entityProps); - +addOneSet(); function cleanup() { - Entities.deleteEntity(soundEntity); + entities.forEach(Entities.deleteEntity); } +// In console: +// Entities.findEntities(MyAvatar.position, 100).forEach(function (id) { if (Entities.getEntityProperties(id).name === 'audioSearchEntity') Entities.deleteEntity(id); }) -Script.scriptEnding.connect(cleanup); \ No newline at end of file +Script.scriptEnding.connect(cleanup); diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 1fd6045fe0..f2acfb9b47 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -73,9 +73,7 @@ var PICK_MAX_DISTANCE = 500; // max length of pick-ray var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position -var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable. var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected -var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size @@ -173,6 +171,10 @@ var STATE_WAITING_FOR_BUMPER_RELEASE = 15; var COLLIDES_WITH_WHILE_GRABBED = "dynamic,otherAvatar"; var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic"; +var HEART_BEAT_INTERVAL = 5; // seconds +var HEART_BEAT_TIMEOUT = 15; + + function stateToName(state) { switch (state) { case STATE_OFF: @@ -1103,6 +1105,8 @@ function MyController(hand) { return; } + this.heartBeat(this.grabbedEntity); + var handPosition = this.getHandPosition(); var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; @@ -1438,6 +1442,8 @@ function MyController(hand) { return; } + this.heartBeat(this.grabbedEntity); + var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position"]); if (props.parentID == MyAvatar.sessionUUID && Vec3.length(props.localPosition) > NEAR_PICK_MAX_DISTANCE * 2.0) { @@ -1644,7 +1650,7 @@ function MyController(hand) { // this next line allowed both: // (1) far-grab, pull to self, near grab, then throw // (2) equip something physical and adjust it with a other-hand grab without the thing drifting - (!this.isInitialGrab && grabData.refCount > 1)) { + grabData.refCount > 1) { noVelocity = true; } } @@ -1670,19 +1676,39 @@ function MyController(hand) { Entities.deleteEntity(this.pointLight); }; + this.heartBeat = function(entityID) { + var now = Date.now(); + if (now - this.lastHeartBeat > HEART_BEAT_INTERVAL) { + var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); + data["heartBeat"] = now; + setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); + this.lastHeartBeat = now; + } + }; + + this.resetAbandonedGrab = function(entityID) { + print("cleaning up abandoned grab on " + entityID); + var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); + data["refCount"] = 1; + setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); + this.deactivateEntity(entityID, false); + }; + this.activateEntity = function(entityID, grabbedProperties, wasLoaded) { var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA); var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - data["activated"] = true; - data["avatarId"] = MyAvatar.sessionUUID; + var now = Date.now(); + if (wasLoaded) { data["refCount"] = 1; - data["avatarId"] = MyAvatar.sessionUUID; } else { data["refCount"] = data["refCount"] ? data["refCount"] + 1 : 1; // zero gravity and set ignoreForCollisions in a way that lets us put them back, after all grabs are done if (data["refCount"] == 1) { + data["heartBeat"] = now; + this.lastHeartBeat = now; + this.isInitialGrab = true; data["gravity"] = grabbedProperties.gravity; data["collidesWith"] = grabbedProperties.collidesWith; @@ -1698,12 +1724,21 @@ function MyController(hand) { z: 0 }, // bummer, it isn't easy to do bitwise collisionMask operations like this: - //"collisionMask": COLLISION_MASK_WHILE_GRABBED | grabbedProperties.collisionMask + // "collisionMask": COLLISION_MASK_WHILE_GRABBED | grabbedProperties.collisionMask // when using string values "collidesWith": COLLIDES_WITH_WHILE_GRABBED }; Entities.editEntity(entityID, whileHeldProperties); } else if (data["refCount"] > 1) { + if (data["heartBeat"] === undefined || + now - data["heartBeat"] > HEART_BEAT_TIMEOUT) { + // this entity has userData suggesting it is grabbed, but nobody is updating the hearbeat. + // deactivate it before grabbing. + this.resetAbandonedGrab(entityID); + grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); + return this.activateEntity(entityID, grabbedProperties, wasLoaded); + } + this.isInitialGrab = false; // if an object is being grabbed by more than one person (or the same person twice, but nevermind), switch // the collision groups so that it wont collide with "other" avatars. This avoids a situation where two diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index e3ad77870d..ccbbc5557a 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -72,10 +72,13 @@ }; } - function createEmitNumberPropertyUpdateFunction(propertyName) { + function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { + decimals = decimals == undefined ? 4 : decimals; return function() { + var value = parseFloat(this.value).toFixed(decimals); + EventBridge.emitWebEvent( - '{ "type":"update", "properties":{"' + propertyName + '":' + parseFloat(this.value).toFixed(4) + '}}' + '{ "type":"update", "properties":{"' + propertyName + '":' + value + '}}' ); }; } @@ -323,6 +326,7 @@ var elLightColorBlue = document.getElementById("property-light-color-blue"); var elLightIntensity = document.getElementById("property-light-intensity"); + var elLightFalloffRadius = document.getElementById("property-light-falloff-radius"); var elLightExponent = document.getElementById("property-light-exponent"); var elLightCutoff = document.getElementById("property-light-cutoff"); @@ -604,9 +608,10 @@ elLightColorGreen.value = properties.color.green; elLightColorBlue.value = properties.color.blue; - elLightIntensity.value = properties.intensity; - elLightExponent.value = properties.exponent; - elLightCutoff.value = properties.cutoff; + elLightIntensity.value = properties.intensity.toFixed(1); + elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); + elLightExponent.value = properties.exponent.toFixed(2); + elLightCutoff.value = properties.cutoff.toFixed(2); } else if (properties.type == "Zone") { for (var i = 0; i < elZoneSections.length; i++) { elZoneSections[i].style.display = 'block'; @@ -795,9 +800,10 @@ } }) - elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity')); - elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent')); - elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff')); + elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1)); + elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1)); + elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2)); + elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2)); elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); @@ -1043,25 +1049,25 @@
Line Height
- +
Text Color
-
R
-
G
-
B
+
R
+
G
+
B
Background Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1085,32 +1091,32 @@
Light Color
-
R
-
G
-
B
+
R
+
G
+
B
Light Intensity
- +
Light Direction
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
Ambient Intensity
- +
@@ -1129,19 +1135,19 @@
Stage Latitude
- +
Stage Longitude
- +
Stage Altitude
- +
@@ -1155,13 +1161,13 @@
Stage Day
- +
Stage Hour
- +
@@ -1186,9 +1192,9 @@
Skybox Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1264,23 +1270,23 @@
Registration
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Dimensions
-
X
-
Y
-
Z
+
X
+
Y
+
Z
- % + %
@@ -1291,9 +1297,9 @@
Voxel Volume Size
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Surface Extractor
@@ -1325,9 +1331,9 @@
Rotation
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
@@ -1339,66 +1345,66 @@
Linear Velocity
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Linear Damping
- +
Angular Velocity
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
Angular Damping
- +
Restitution
- +
Friction
- +
Gravity
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Acceleration
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Density
- +
@@ -1406,9 +1412,9 @@
Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1483,7 +1489,7 @@
Lifetime
- +
@@ -1541,25 +1547,25 @@
Animation FPS
- +
Animation Frame
- +
Animation First Frame
- +
Animation Last Frame
- +
@@ -1591,37 +1597,43 @@
+
+
Color
+
+
+
R
+
G
+
B
+
+
+
+
Intensity
+
+ +
+
+
+
Falloff Radius
+
+ +
+
Spot Light
-
-
Color
-
-
-
R
-
G
-
B
-
-
-
-
Intensity
-
- -
-
Spot Light Exponent
- +
Spot Light Cutoff (degrees)
- +
diff --git a/examples/libraries/utils.js b/examples/libraries/utils.js index b2b2902cb0..f4a431a657 100644 --- a/examples/libraries/utils.js +++ b/examples/libraries/utils.js @@ -199,7 +199,7 @@ pointInExtents = function(point, minPoint, maxPoint) { * @param Number l The lightness * @return Array The RGB representation */ -hslToRgb = function(hsl, hueOffset) { +hslToRgb = function(hsl) { var r, g, b; if (hsl.s == 0) { r = g = b = hsl.l; // achromatic diff --git a/examples/particle_explorer/particleExplorer.js b/examples/particle_explorer/particleExplorer.js index a3cfd32676..307e361ff1 100644 --- a/examples/particle_explorer/particleExplorer.js +++ b/examples/particle_explorer/particleExplorer.js @@ -43,10 +43,10 @@ var keysToAllow = [ 'emitSpeed', 'speedSpread', 'emitOrientation', - 'emitDimensios', - 'emitRadiusStart', + 'emitDimensions', 'polarStart', 'polarFinish', + 'azimuthStart', 'azimuthFinish', 'emitAcceleration', 'accelerationSpread', diff --git a/examples/playa/fireworks/fireworksLaunchButtonEntityScript.js b/examples/playa/fireworks/fireworksLaunchButtonEntityScript.js new file mode 100644 index 0000000000..63c4f3616e --- /dev/null +++ b/examples/playa/fireworks/fireworksLaunchButtonEntityScript.js @@ -0,0 +1,229 @@ + // + // fireworksLaunchEntityScript.js + // examples/playa/fireworks + // + // Created by Eric Levin on 2/24/16. + // Copyright 2016 High Fidelity, Inc. + // + // Run this script to spawn a big fireworks launch button that a user can press + // + // Distributed under the Apache License, Version 2.0. + // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + (function() { + Script.include("../../libraries/utils.js"); + var _this; + Fireworks = function() { + _this = this; + _this.launchSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/missle+launch.wav"); + _this.explosionSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/fireworksExplosion.wav"); + _this.timeToExplosionRange = { + min: 2500, + max: 4500 + }; + }; + + Fireworks.prototype = { + + startNearTrigger: function() { + _this.shootFireworks(); + }, + + startFarTrigger: function() { + _this.shootFireworks(); + }, + + clickReleaseOnEntity: function() { + _this.shootFireworks(); + }, + + shootFireworks: function() { + // Get launch position + var launchPosition = getEntityUserData(_this.entityID).launchPosition || _this.position; + + var numMissles = randInt(1, 3); + for(var i = 0; i < numMissles; i++) { + _this.shootMissle(launchPosition); + } + }, + + shootMissle: function(launchPosition) { + Audio.playSound(_this.launchSound, { + position: launchPosition, + volume: 0.5 + }); + + var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Rocket-2.fbx"; + var missleDimensions = Vec3.multiply({ + x: 0.24, + y: 0.7, + z: 0.24 + }, randFloat(0.2, 1.5)); + var missleRotation = Quat.fromPitchYawRollDegrees(randInt(-60, 60), 0, randInt(-60, 60)); + var missleVelocity = Vec3.multiply(Quat.getUp(missleRotation), randFloat(2, 4)); + var missleAcceleration = Vec3.multiply(Quat.getUp(missleRotation), randFloat(1, 3)); + var missle = Entities.addEntity({ + type: "Model", + modelURL: MODEL_URL, + position: launchPosition, + rotation: missleRotation, + dimensions: missleDimensions, + damping: 0, + dynamic: true, + lifetime: 20, // Just in case + velocity: missleVelocity, + acceleration: missleAcceleration, + angularVelocity: { + x: 0, + y: randInt(-1, 1), + z: 0 + }, + angularDamping: 0, + visible: false + }); + + var smokeTrailPosition = Vec3.sum(launchPosition, Vec3.multiply(-missleDimensions.y / 2 + 0.1, Quat.getUp(missleRotation))); + var smoke = Entities.addEntity({ + type: "ParticleEffect", + position: smokeTrailPosition, + lifespan: 10, + lifetime: 20, + name: "Smoke Trail", + maxParticles: 3000, + emitRate: 80, + emitSpeed: 0, + speedSpread: 0, + dimensions: { + x: 1000, + y: 1000, + z: 1000 + }, + polarStart: 0, + polarFinish: 0, + azimuthStart: -3.14, + azimuthFinish: 3.14, + emitAcceleration: { + x: 0, + y: 0.01, + z: 0 + }, + accelerationSpread: { + x: 0.01, + y: 0, + z: 0.01 + }, + radiusSpread: 0.03, + particleRadius: 0.3, + radiusStart: 0.06, + radiusFinish: 0.9, + alpha: 0.1, + alphaSpread: 0, + alphaStart: 0.7, + alphaFinish: 0, + textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png", + emitterShouldTrail: true, + parentID: missle, + }); + + + Script.setTimeout(function() { + Entities.editEntity(smoke, { + parentID: null, + isEmitting: false + }); + + var explodeBasePosition = Entities.getEntityProperties(missle, "position").position; + + Entities.deleteEntity(missle); + // Explode 1 firework immediately + _this.explodeFirework(explodeBasePosition); + var numAdditionalFireworks = randInt(1, 5); + for (var i = 0; i < numAdditionalFireworks; i++) { + Script.setTimeout(function() { + var explodePosition = Vec3.sum(explodeBasePosition, {x: randFloat(-3, 3), y: randFloat(-3, 3), z: randFloat(-3, 3)}); + _this.explodeFirework(explodePosition); + }, randInt(0, 1000)) + } + }, randFloat(_this.timeToExplosionRange.min, _this.timeToExplosionRange.max)); + + + }, + + explodeFirework: function(explodePosition) { + // We just exploded firework, so stop emitting its fire and smoke + + Audio.playSound(_this.explosionSound, { + position: explodePosition + }); + var firework = Entities.addEntity({ + name: "fireworks emitter", + position: explodePosition, + type: "ParticleEffect", + colorStart: hslToRgb({ + h: Math.random(), + s: 0.5, + l: 0.7 + }), + color: hslToRgb({ + h: Math.random(), + s: 0.5, + l: 0.5 + }), + colorFinish: hslToRgb({ + h: Math.random(), + s: 0.5, + l: 0.7 + }), + maxParticles: 10000, + lifetime: 20, + lifespan: randFloat(1.5, 3), + emitRate: randInt(500, 5000), + emitSpeed: randFloat(0.5, 2), + speedSpread: 0.2, + emitOrientation: Quat.fromPitchYawRollDegrees(randInt(0, 360), randInt(0, 360), randInt(0, 360)), + polarStart: 1, + polarFinish: randFloat(1.2, 3), + azimuthStart: -Math.PI, + azimuthFinish: Math.PI, + emitAcceleration: { + x: 0, + y: randFloat(-1, -0.2), + z: 0 + }, + accelerationSpread: { + x: Math.random(), + y: 0, + z: Math.random() + }, + particleRadius: randFloat(0.001, 0.1), + radiusSpread: Math.random() * 0.1, + radiusStart: randFloat(0.001, 0.1), + radiusFinish: randFloat(0.001, 0.1), + alpha: randFloat(0.8, 1.0), + alphaSpread: randFloat(0.1, 0.2), + alphaStart: randFloat(0.7, 1.0), + alphaFinish: randFloat(0.7, 1.0), + textures: "http://ericrius1.github.io/PlatosCave/assets/star.png", + }); + + + Script.setTimeout(function() { + Entities.editEntity(firework, { + isEmitting: false + }); + }, randInt(500, 1000)); + + }, + + preload: function(entityID) { + _this.entityID = entityID; + _this.position = Entities.getEntityProperties(_this.entityID, "position").position; + print("EBL RELOAD ENTITY SCRIPT!!!"); + + } + + }; + + // entity scripts always need to return a newly constructed object of our type + return new Fireworks(); + }); diff --git a/examples/playa/fireworks/fireworksLaunchButtonSpawner.js b/examples/playa/fireworks/fireworksLaunchButtonSpawner.js new file mode 100644 index 0000000000..20527a7e3a --- /dev/null +++ b/examples/playa/fireworks/fireworksLaunchButtonSpawner.js @@ -0,0 +1,46 @@ + // + // fireworksLaunchButtonSpawner.js + // examples/playa/fireworks + // + // Created by Eric Levina on 2/24/16. + // Copyright 2015 High Fidelity, Inc. + // + // Run this script to spawn a big fireworks launch button that a user can press + // + // Distributed under the Apache License, Version 2.0. + // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + var orientation = Camera.getOrientation(); + orientation = Quat.safeEulerAngles(orientation); + orientation.x = 0; + orientation = Quat.fromVec3Degrees(orientation); + var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); + + // Math.random ensures no caching of script + var SCRIPT_URL = Script.resolvePath("fireworksLaunchButtonEntityScript.js"); + var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Launch-Button.fbx"; + var launchButton = Entities.addEntity({ + type: "Model", + name: "launch pad", + modelURL: MODEL_URL, + position: center, + dimensions: { + x: 0.98, + y: 1.16, + z: 0.98 + }, + script: SCRIPT_URL, + userData: JSON.stringify({ + launchPosition: {x: 1, y: 1.8, z: -20.9}, + grabbableKey: { + wantsTrigger: true + } + }) + }) + + + function cleanup() { + Entities.deleteEntity(launchButton); + } + + Script.scriptEnding.connect(cleanup); \ No newline at end of file diff --git a/ice-server/CMakeLists.txt b/ice-server/CMakeLists.txt index cfec3c966c..e5bdffe2e2 100644 --- a/ice-server/CMakeLists.txt +++ b/ice-server/CMakeLists.txt @@ -6,3 +6,17 @@ setup_hifi_project(Network) # link the shared hifi libraries link_hifi_libraries(embedded-webserver networking shared) package_libraries_for_deployment() + +# find OpenSSL +find_package(OpenSSL REQUIRED) + +if (APPLE AND ${OPENSSL_INCLUDE_DIR} STREQUAL "/usr/include") + # this is a user on OS X using system OpenSSL, which is going to throw warnings since they're deprecating for their common crypto + message(WARNING "The found version of OpenSSL is the OS X system version. This will produce deprecation warnings." + "\nWe recommend you install a newer version (at least 1.0.1h) in a different directory and set OPENSSL_ROOT_DIR in your env so Cmake can find it.") +endif () + +include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") + +# append OpenSSL to our list of libraries to link +target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES}) diff --git a/ice-server/src/IceServer.cpp b/ice-server/src/IceServer.cpp index 2baa7a13a7..f38923b873 100644 --- a/ice-server/src/IceServer.cpp +++ b/ice-server/src/IceServer.cpp @@ -9,14 +9,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include "IceServer.h" + +#include +#include + +#include +#include +#include +#include #include +#include #include #include -#include "IceServer.h" - const int CLEAR_INACTIVE_PEERS_INTERVAL_MSECS = 1 * 1000; const int PEER_SILENCE_THRESHOLD_MSECS = 5 * 1000; @@ -45,7 +52,6 @@ IceServer::IceServer(int argc, char* argv[]) : QTimer* inactivePeerTimer = new QTimer(this); connect(inactivePeerTimer, &QTimer::timeout, this, &IceServer::clearInactivePeers); inactivePeerTimer->start(CLEAR_INACTIVE_PEERS_INTERVAL_MSECS); - } bool IceServer::packetVersionMatch(const udt::Packet& packet) { @@ -70,9 +76,14 @@ void IceServer::processPacket(std::unique_ptr packet) { if (nlPacket->getType() == PacketType::ICEServerHeartbeat) { SharedNetworkPeer peer = addOrUpdateHeartbeatingPeer(*nlPacket); - - // so that we can send packets to the heartbeating peer when we need, we need to activate a socket now - peer->activateMatchingOrNewSymmetricSocket(nlPacket->getSenderSockAddr()); + if (peer) { + // so that we can send packets to the heartbeating peer when we need, we need to activate a socket now + peer->activateMatchingOrNewSymmetricSocket(nlPacket->getSenderSockAddr()); + } else { + // we couldn't verify this peer - respond back to them so they know they may need to perform keypair re-generation + static auto deniedPacket = NLPacket::create(PacketType::ICEServerHeartbeatDenied); + _serverSocket.writePacket(*deniedPacket, nlPacket->getSenderSockAddr()); + } } else if (nlPacket->getType() == PacketType::ICEServerQuery) { QDataStream heartbeatStream(nlPacket.get()); @@ -114,31 +125,135 @@ SharedNetworkPeer IceServer::addOrUpdateHeartbeatingPeer(NLPacket& packet) { // pull the UUID, public and private sock addrs for this peer QUuid senderUUID; HifiSockAddr publicSocket, localSocket; + QByteArray signature; QDataStream heartbeatStream(&packet); - - heartbeatStream >> senderUUID; - heartbeatStream >> publicSocket >> localSocket; + heartbeatStream >> senderUUID >> publicSocket >> localSocket; - // make sure we have this sender in our peer hash - SharedNetworkPeer matchingPeer = _activePeers.value(senderUUID); + auto signedPlaintext = QByteArray::fromRawData(packet.getPayload(), heartbeatStream.device()->pos()); + heartbeatStream >> signature; - if (!matchingPeer) { - // if we don't have this sender we need to create them now - matchingPeer = QSharedPointer::create(senderUUID, publicSocket, localSocket); - _activePeers.insert(senderUUID, matchingPeer); + // make sure this is a verified heartbeat before performing any more processing + if (isVerifiedHeartbeat(senderUUID, signedPlaintext, signature)) { + // make sure we have this sender in our peer hash + SharedNetworkPeer matchingPeer = _activePeers.value(senderUUID); - qDebug() << "Added a new network peer" << *matchingPeer; + if (!matchingPeer) { + // if we don't have this sender we need to create them now + matchingPeer = QSharedPointer::create(senderUUID, publicSocket, localSocket); + _activePeers.insert(senderUUID, matchingPeer); + + qDebug() << "Added a new network peer" << *matchingPeer; + } else { + // we already had the peer so just potentially update their sockets + matchingPeer->setPublicSocket(publicSocket); + matchingPeer->setLocalSocket(localSocket); + } + + // update our last heard microstamp for this network peer to now + matchingPeer->setLastHeardMicrostamp(usecTimestampNow()); + + return matchingPeer; } else { - // we already had the peer so just potentially update their sockets - matchingPeer->setPublicSocket(publicSocket); - matchingPeer->setLocalSocket(localSocket); + // not verified, return the empty peer object + return SharedNetworkPeer(); + } +} + +bool IceServer::isVerifiedHeartbeat(const QUuid& domainID, const QByteArray& plaintext, const QByteArray& signature) { + // check if we have a private key for this domain ID - if we do not then fire off the request for it + auto it = _domainPublicKeys.find(domainID); + if (it != _domainPublicKeys.end()) { + + // attempt to verify the signature for this heartbeat + const unsigned char* publicKeyData = reinterpret_cast(it->second.constData()); + + // first load up the public key into an RSA struct + RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, it->second.size()); + + if (rsaPublicKey) { + auto hashedPlaintext = QCryptographicHash::hash(plaintext, QCryptographicHash::Sha256); + int verificationResult = RSA_verify(NID_sha256, + reinterpret_cast(hashedPlaintext.constData()), + hashedPlaintext.size(), + reinterpret_cast(signature.constData()), + signature.size(), + rsaPublicKey); + + // free up the public key and remove connection token before we return + RSA_free(rsaPublicKey); + + if (verificationResult == 1) { + // this is the only success case - we return true here to indicate that the heartbeat is verified + return true; + } else { + qDebug() << "Failed to verify heartbeat for" << domainID << "- re-requesting public key from API."; + } + + } else { + // we can't let this user in since we couldn't convert their public key to an RSA key we could use + qWarning() << "Could not convert in-memory public key for" << domainID << "to usable RSA public key."; + qWarning() << "Re-requesting public key from API"; + } } - // update our last heard microstamp for this network peer to now - matchingPeer->setLastHeardMicrostamp(usecTimestampNow()); + // we could not verify this heartbeat (missing public key, could not load public key, bad actor) + // ask the metaverse API for the right public key and return false to indicate that this is not verified + requestDomainPublicKey(domainID); - return matchingPeer; + return false; +} + +void IceServer::requestDomainPublicKey(const QUuid& domainID) { + // send a request to the metaverse API for the public key for this domain + QNetworkAccessManager* manager = new QNetworkAccessManager { this }; + connect(manager, &QNetworkAccessManager::finished, this, &IceServer::publicKeyReplyFinished); + + QUrl publicKeyURL { NetworkingConstants::METAVERSE_SERVER_URL }; + QString publicKeyPath = QString("/api/v1/domains/%1/public_key").arg(uuidStringWithoutCurlyBraces(domainID)); + publicKeyURL.setPath(publicKeyPath); + + QNetworkRequest publicKeyRequest { publicKeyURL }; + publicKeyRequest.setAttribute(QNetworkRequest::User, domainID); + + qDebug() << "Requesting public key for domain with ID" << domainID; + + manager->get(publicKeyRequest); +} + +void IceServer::publicKeyReplyFinished(QNetworkReply* reply) { + // get the domain ID from the QNetworkReply attribute + QUuid domainID = reply->request().attribute(QNetworkRequest::User).toUuid(); + + if (reply->error() == QNetworkReply::NoError) { + // pull out the public key and store it for this domain + + // the response should be JSON + QJsonDocument responseDocument = QJsonDocument::fromJson(reply->readAll()); + + static const QString DATA_KEY = "data"; + static const QString PUBLIC_KEY_KEY = "public_key"; + static const QString STATUS_KEY = "status"; + static const QString SUCCESS_VALUE = "success"; + + auto responseObject = responseDocument.object(); + if (responseObject[STATUS_KEY].toString() == SUCCESS_VALUE) { + auto dataObject = responseObject[DATA_KEY].toObject(); + if (dataObject.contains(PUBLIC_KEY_KEY)) { + _domainPublicKeys[domainID] = QByteArray::fromBase64(dataObject[PUBLIC_KEY_KEY].toString().toUtf8()); + } else { + qWarning() << "There was no public key present in response for domain with ID" << domainID; + } + } else { + qWarning() << "The metaverse API did not return success for public key request for domain with ID" << domainID; + } + + } else { + // there was a problem getting the public key for the domain + // log it since it will be re-requested on the next heartbeat + + qWarning() << "Error retreiving public key for domain with ID" << domainID << "-" << reply->errorString(); + } } void IceServer::sendPeerInformationPacket(const NetworkPeer& peer, const HifiSockAddr* destinationSockAddr) { diff --git a/ice-server/src/IceServer.h b/ice-server/src/IceServer.h index f1c2c06b65..81234b2c3c 100644 --- a/ice-server/src/IceServer.h +++ b/ice-server/src/IceServer.h @@ -16,13 +16,15 @@ #include #include +#include + #include #include #include #include #include -typedef QHash NetworkPeerHash; +class QNetworkReply; class IceServer : public QCoreApplication, public HTTPRequestHandler { Q_OBJECT @@ -31,6 +33,7 @@ public: bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false); private slots: void clearInactivePeers(); + void publicKeyReplyFinished(QNetworkReply* reply); private: bool packetVersionMatch(const udt::Packet& packet); void processPacket(std::unique_ptr packet); @@ -38,10 +41,19 @@ private: SharedNetworkPeer addOrUpdateHeartbeatingPeer(NLPacket& incomingPacket); void sendPeerInformationPacket(const NetworkPeer& peer, const HifiSockAddr* destinationSockAddr); + bool isVerifiedHeartbeat(const QUuid& domainID, const QByteArray& plaintext, const QByteArray& signature); + void requestDomainPublicKey(const QUuid& domainID); + QUuid _id; udt::Socket _serverSocket; + + using NetworkPeerHash = QHash; NetworkPeerHash _activePeers; + HTTPManager _httpManager; + + using DomainPublicKeyHash = std::unordered_map; + DomainPublicKeyHash _domainPublicKeys; }; #endif // hifi_IceServer_h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index df7b203bea..b072867400 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -425,11 +425,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _maxOctreePPS(maxOctreePacketsPerSecond.get()), _lastFaceTrackerUpdate(0) { - // FIXME this may be excessivly conservative. On the other hand + // FIXME this may be excessivly conservative. On the other hand // maybe I'm used to having an 8-core machine - // Perhaps find the ideal thread count and subtract 2 or 3 + // Perhaps find the ideal thread count and subtract 2 or 3 // (main thread, present thread, random OS load) - // More threads == faster concurrent loads, but also more concurrent + // More threads == faster concurrent loads, but also more concurrent // load on the GPU until we can serialize GPU transfers (off the main thread) QThreadPool::globalInstance()->setMaxThreadCount(2); thread()->setPriority(QThread::HighPriority); @@ -553,14 +553,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); - connect(&domainHandler, &DomainHandler::settingsReceived, this, &Application::domainSettingsReceived); // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * 1000; auto discoverabilityManager = DependencyManager::get(); connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); - connect(&locationUpdateTimer, &QTimer::timeout, + connect(&locationUpdateTimer, &QTimer::timeout, DependencyManager::get().data(), &AddressManager::storeCurrentAddress); locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); @@ -590,6 +589,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&accountManager, &AccountManager::usernameChanged, this, &Application::updateWindowTitle); // set the account manager's root URL and trigger a login request if we don't have the access token + accountManager.setIsAgent(true); accountManager.setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); UserActivityLogger::getInstance().launch(applicationVersion()); @@ -604,7 +604,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); - + // Save avatar location immediately after a teleport. connect(getMyAvatar(), &MyAvatar::positionGoneTo, DependencyManager::get().data(), &AddressManager::storeCurrentAddress); @@ -625,7 +625,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : getEntities()->reloadEntityScripts(); }, Qt::QueuedConnection); - connect(scriptEngines, &ScriptEngines::scriptLoadError, + connect(scriptEngines, &ScriptEngines::scriptLoadError, scriptEngines, [](const QString& filename, const QString& error){ OffscreenUi::warning(nullptr, "Error Loading Script", filename + " failed to load."); }, Qt::QueuedConnection); @@ -905,9 +905,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : SpacemouseManager::getInstance().init(); #endif - auto& packetReceiver = nodeList->getPacketReceiver(); - packetReceiver.registerListener(PacketType::DomainConnectionDenied, this, "handleDomainConnectionDeniedPacket"); - // If the user clicks an an entity, we will check that it's an unlocked web entity, and if so, set the focus to it auto entityScriptingInterface = DependencyManager::get(); connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity, @@ -977,7 +974,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : disconnect(_idleTimer); }); // Setting the interval to zero forces this to get called whenever there are no messages - // in the queue, which can be pretty damn frequent. Hence the idle function has a bunch + // in the queue, which can be pretty damn frequent. Hence the idle function has a bunch // of logic to abort early if it's being called too often. _idleTimer->start(0); } @@ -1025,7 +1022,7 @@ void Application::cleanupBeforeQuit() { getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts DependencyManager::get()->saveScripts(); DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts - DependencyManager::destroy(); + DependencyManager::destroy(); // first stop all timers directly or by invokeMethod // depending on what thread they run in @@ -1213,10 +1210,10 @@ void Application::initializeUi() { setupPreferences(); - // For some reason there is already an "Application" object in the QML context, + // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("SnapshotUploader", new SnapshotUploader()); - rootContext->setContextProperty("ApplicationInterface", this); + rootContext->setContextProperty("ApplicationInterface", this); rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); rootContext->setContextProperty("Controller", DependencyManager::get().data()); @@ -2448,7 +2445,6 @@ void Application::idle(uint64_t now) { _overlayConductor.setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Overlays)); } - auto displayPlugin = getActiveDisplayPlugin(); // depending on whether we're throttling or not. // Once rendering is off on another thread we should be able to have Application::idle run at start(0) in @@ -2472,7 +2468,7 @@ void Application::idle(uint64_t now) { // Nested ifs are for clarity in the logic. Don't collapse them into a giant single if. // Don't saturate the main thread with rendering, no paint calls until the last one is complete if (!_pendingPaint) { - // Also no paint calls until the display plugin has increased by at least one frame + // Also no paint calls until the display plugin has increased by at least one frame // (don't render at 90fps if the display plugin only goes at 60) if (_renderedFrameIndex == INVALID_FRAME || presentCount > _renderedFrameIndex) { // Record what present frame we're on @@ -2482,14 +2478,14 @@ void Application::idle(uint64_t now) { // But when we DO request a paint, get to it as soon as possible: high priority postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); } - } - - // For the rest of idle, we want to cap at the max sim rate, so we might not call + } + + // For the rest of idle, we want to cap at the max sim rate, so we might not call // the remaining idle work every paint frame, or vice versa // In theory this means we could call idle processing more often than painting, // but in practice, when the paintGL calls aren't keeping up, there's no room left // in the main thread to call idle more often than paint. - // This check is mostly to keep idle from burning up CPU cycles by running at + // This check is mostly to keep idle from burning up CPU cycles by running at // hundreds of idles per second when the rendering is that fast if ((timeSinceLastUpdateUs / USECS_PER_MSEC) < CAPPED_SIM_FRAME_PERIOD_MS) { // No paint this round, but might be time for a new idle, otherwise return @@ -3445,7 +3441,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node rootDetails.y * TREE_SCALE, rootDetails.z * TREE_SCALE) - glm::vec3(HALF_TREE_SCALE), rootDetails.s * TREE_SCALE); - ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds); + ViewFrustum::location serverFrustumLocation = _viewFrustum.computeCubeViewLocation(serverBounds); if (serverFrustumLocation != ViewFrustum::OUTSIDE) { inViewServers++; @@ -3513,7 +3509,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node rootDetails.s * TREE_SCALE); - ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds); + ViewFrustum::location serverFrustumLocation = _viewFrustum.computeCubeViewLocation(serverBounds); if (serverFrustumLocation != ViewFrustum::OUTSIDE) { inView = true; } else { @@ -3840,18 +3836,10 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se }); } - // Setup the current Zone Entity lighting and skybox + // Setup the current Zone Entity lighting { - // FIXME: Use a zone setting to determine the ambient light mode - DependencyManager::get()->setAmbientLightMode(-1); - auto skyStage = DependencyManager::get()->getSkyStage(); - DependencyManager::get()->setGlobalLight(skyStage->getSunLight()->getDirection(), skyStage->getSunLight()->getColor(), skyStage->getSunLight()->getIntensity(), skyStage->getSunLight()->getAmbientIntensity()); - - auto skybox = model::SkyboxPointer(); - if (skyStage->getBackgroundMode() == model::SunSkyStage::SKY_BOX) { - skybox = skyStage->getSkybox(); - } - DependencyManager::get()->setGlobalSkybox(skybox); + auto stage = DependencyManager::get()->getSkyStage(); + DependencyManager::get()->setGlobalLight(stage->getSunLight(), stage->getSkybox()->getCubemap()); } { @@ -3987,30 +3975,10 @@ void Application::clearDomainOctreeDetails() { void Application::domainChanged(const QString& domainHostname) { updateWindowTitle(); clearDomainOctreeDetails(); - _domainConnectionRefusals.clear(); // disable physics until we have enough information about our new location to not cause craziness. _physicsEnabled = false; } -void Application::handleDomainConnectionDeniedPacket(QSharedPointer message) { - // Read deny reason from packet - quint16 reasonSize; - message->readPrimitive(&reasonSize); - QString reason = QString::fromUtf8(message->readWithoutCopy(reasonSize)); - - // output to the log so the user knows they got a denied connection request - // and check and signal for an access token so that we can make sure they are logged in - qCDebug(interfaceapp) << "The domain-server denied a connection request: " << reason; - qCDebug(interfaceapp) << "You may need to re-log to generate a keypair so you can provide a username signature."; - - if (!_domainConnectionRefusals.contains(reason)) { - _domainConnectionRefusals.append(reason); - emit domainConnectionRefused(reason); - } - - AccountManager::getInstance().checkAndSignalForAccessToken(); -} - void Application::connectedToDomain(const QString& hostname) { AccountManager& accountManager = AccountManager::getInstance(); const QUuid& domainID = DependencyManager::get()->getDomainHandler().getUUID(); @@ -4554,33 +4522,6 @@ void Application::openUrl(const QUrl& url) { } } -void Application::domainSettingsReceived(const QJsonObject& domainSettingsObject) { - // from the domain-handler, figure out the satoshi cost per voxel and per meter cubed - const QString VOXEL_SETTINGS_KEY = "voxels"; - const QString PER_VOXEL_COST_KEY = "per-voxel-credits"; - const QString PER_METER_CUBED_COST_KEY = "per-meter-cubed-credits"; - const QString VOXEL_WALLET_UUID = "voxel-wallet"; - - const QJsonObject& voxelObject = domainSettingsObject[VOXEL_SETTINGS_KEY].toObject(); - - qint64 satoshisPerVoxel = 0; - qint64 satoshisPerMeterCubed = 0; - QUuid voxelWalletUUID; - - if (!domainSettingsObject.isEmpty()) { - float perVoxelCredits = (float) voxelObject[PER_VOXEL_COST_KEY].toDouble(); - float perMeterCubedCredits = (float) voxelObject[PER_METER_CUBED_COST_KEY].toDouble(); - - satoshisPerVoxel = (qint64) floorf(perVoxelCredits * SATOSHIS_PER_CREDIT); - satoshisPerMeterCubed = (qint64) floorf(perMeterCubedCredits * SATOSHIS_PER_CREDIT); - - voxelWalletUUID = QUuid(voxelObject[VOXEL_WALLET_UUID].toString()); - } - - qCDebug(interfaceapp) << "Octree edits costs are" << satoshisPerVoxel << "per octree cell and" << satoshisPerMeterCubed << "per meter cubed"; - qCDebug(interfaceapp) << "Destination wallet UUID for edit payments is" << voxelWalletUUID; -} - void Application::loadDialog() { auto scriptEngines = DependencyManager::get(); QString fileNameString = OffscreenUi::getOpenFileName( @@ -4782,7 +4723,7 @@ static void addDisplayPluginToMenu(DisplayPluginPointer displayPlugin, bool acti groupingMenu = "Developer"; break; default: - groupingMenu = "Standard"; + groupingMenu = "Standard"; break; } diff --git a/interface/src/Application.h b/interface/src/Application.h index aacf0b5b08..a0260f61d4 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -225,7 +225,6 @@ signals: void svoImportRequested(const QString& url); void checkBackgroundDownloads(); - void domainConnectionRefused(const QString& reason); void fullAvatarURLChanged(const QString& newValue, const QString& modelName); @@ -295,9 +294,6 @@ private slots: void activeChanged(Qt::ApplicationState state); - void domainSettingsReceived(const QJsonObject& domainSettingsObject); - void handleDomainConnectionDeniedPacket(QSharedPointer message); - void notifyPacketVersionMismatch(); void loadSettings(); @@ -476,7 +472,6 @@ private: typedef bool (Application::* AcceptURLMethod)(const QString &); static const QHash _acceptedExtensions; - QList _domainConnectionRefusals; glm::uvec2 _renderResolution; int _maxOctreePPS = DEFAULT_MAX_OCTREE_PPS; diff --git a/interface/src/CrashHandler.cpp b/interface/src/CrashHandler.cpp index ce5facb580..f9cd7679ae 100644 --- a/interface/src/CrashHandler.cpp +++ b/interface/src/CrashHandler.cpp @@ -22,11 +22,8 @@ #include #include -#include "DataServerAccountInfo.h" #include "Menu.h" -Q_DECLARE_METATYPE(DataServerAccountInfo) - static const QString RUNNING_MARKER_FILENAME = "Interface.running"; void CrashHandler::checkForAndHandleCrash() { @@ -57,7 +54,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() { layout->addWidget(label); QRadioButton* option1 = new QRadioButton("Reset all my settings"); - QRadioButton* option2 = new QRadioButton("Reset my settings but retain login and avatar info."); + QRadioButton* option2 = new QRadioButton("Reset my settings but retain avatar info."); QRadioButton* option3 = new QRadioButton("Continue with my current settings"); option3->setChecked(true); layout->addWidget(option1); @@ -79,7 +76,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() { return CrashHandler::DELETE_INTERFACE_INI; } if (option2->isChecked()) { - return CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO; + return CrashHandler::RETAIN_AVATAR_INFO; } } @@ -88,7 +85,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() { } void CrashHandler::handleCrash(CrashHandler::Action action) { - if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) { + if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_AVATAR_INFO) { // CrashHandler::DO_NOTHING or unexpected value return; } @@ -101,18 +98,13 @@ void CrashHandler::handleCrash(CrashHandler::Action action) { const QString DISPLAY_NAME_KEY = "displayName"; const QString FULL_AVATAR_URL_KEY = "fullAvatarURL"; const QString FULL_AVATAR_MODEL_NAME_KEY = "fullAvatarModelName"; - const QString ACCOUNTS_GROUP = "accounts"; QString displayName; QUrl fullAvatarURL; QString fullAvatarModelName; QUrl address; - QMap accounts; - if (action == CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) { - // Read login and avatar info - - qRegisterMetaType("DataServerAccountInfo"); - qRegisterMetaTypeStreamOperators("DataServerAccountInfo"); + if (action == CrashHandler::RETAIN_AVATAR_INFO) { + // Read avatar info // Location and orientation settings.beginGroup(ADDRESS_MANAGER_GROUP); @@ -125,13 +117,6 @@ void CrashHandler::handleCrash(CrashHandler::Action action) { fullAvatarURL = settings.value(FULL_AVATAR_URL_KEY).toUrl(); fullAvatarModelName = settings.value(FULL_AVATAR_MODEL_NAME_KEY).toString(); settings.endGroup(); - - // Accounts - settings.beginGroup(ACCOUNTS_GROUP); - foreach(const QString& key, settings.allKeys()) { - accounts.insert(key, settings.value(key).value()); - } - settings.endGroup(); } // Delete Interface.ini @@ -140,8 +125,8 @@ void CrashHandler::handleCrash(CrashHandler::Action action) { settingsFile.remove(); } - if (action == CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) { - // Write login and avatar info + if (action == CrashHandler::RETAIN_AVATAR_INFO) { + // Write avatar info // Location and orientation settings.beginGroup(ADDRESS_MANAGER_GROUP); @@ -154,13 +139,6 @@ void CrashHandler::handleCrash(CrashHandler::Action action) { settings.setValue(FULL_AVATAR_URL_KEY, fullAvatarURL); settings.setValue(FULL_AVATAR_MODEL_NAME_KEY, fullAvatarModelName); settings.endGroup(); - - // Accounts - settings.beginGroup(ACCOUNTS_GROUP); - foreach(const QString& key, accounts.keys()) { - settings.setValue(key, QVariant::fromValue(accounts.value(key))); - } - settings.endGroup(); } } diff --git a/interface/src/CrashHandler.h b/interface/src/CrashHandler.h index fc754cf1ac..61361b6107 100644 --- a/interface/src/CrashHandler.h +++ b/interface/src/CrashHandler.h @@ -25,7 +25,7 @@ public: private: enum Action { DELETE_INTERFACE_INI, - RETAIN_LOGIN_AND_AVATAR_INFO, + RETAIN_AVATAR_INFO, DO_NOTHING }; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a5795c5a3b..72e138e0cb 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -70,17 +70,9 @@ Menu::Menu() { dialogsManager.data(), &DialogsManager::toggleLoginDialog); } - // File > Update -- FIXME: needs implementation - auto action = addActionToQMenuAndActionHash(fileMenu, "Update"); - action->setDisabled(true); - // File > Help addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp())); - // File > Crash Reporter...-- FIXME: needs implementation - auto crashReporterAction = addActionToQMenuAndActionHash(fileMenu, "Crash Reporter..."); - crashReporterAction->setDisabled(true); - // File > About addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole); @@ -167,7 +159,7 @@ Menu::Menu() { QObject* avatar = avatarManager->getMyAvatar(); // Avatar > Attachments... - action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments); + auto action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments); connect(action, &QAction::triggered, [] { DependencyManager::get()->show(QString("hifi/dialogs/AttachmentsDialog.qml"), "AttachmentsDialog"); }); @@ -262,16 +254,10 @@ Menu::Menu() { // Navigate menu ---------------------------------- MenuWrapper* navigateMenu = addMenu("Navigate"); - // Navigate > Home -- FIXME: needs implementation - auto homeAction = addActionToQMenuAndActionHash(navigateMenu, "Home"); - homeAction->setDisabled(true); - + // Navigate > Show Address Bar addActionToQMenuAndActionHash(navigateMenu, MenuOption::AddressBar, Qt::CTRL | Qt::Key_L, dialogsManager.data(), SLOT(toggleAddressBar())); - // Navigate > Directory -- FIXME: needs implementation - addActionToQMenuAndActionHash(navigateMenu, "Directory"); - // Navigate > Bookmark related menus -- Note: the Bookmark class adds its own submenus here. qApp->getBookmarks()->setupMenus(this, navigateMenu); @@ -302,20 +288,19 @@ Menu::Menu() { DependencyManager::get()->toggle(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), "GeneralPreferencesDialog"); }); - - // Settings > Avatar...-- FIXME: needs implementation + // Settings > Avatar... action = addActionToQMenuAndActionHash(settingsMenu, "Avatar..."); connect(action, &QAction::triggered, [] { DependencyManager::get()->toggle(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), "AvatarPreferencesDialog"); }); - // Settings > Audio...-- FIXME: needs implementation + // Settings > Audio... action = addActionToQMenuAndActionHash(settingsMenu, "Audio..."); connect(action, &QAction::triggered, [] { DependencyManager::get()->toggle(QString("hifi/dialogs/AudioPreferencesDialog.qml"), "AudioPreferencesDialog"); }); - // Settings > LOD...-- FIXME: needs implementation + // Settings > LOD... action = addActionToQMenuAndActionHash(settingsMenu, "LOD..."); connect(action, &QAction::triggered, [] { DependencyManager::get()->toggle(QString("hifi/dialogs/LodPreferencesDialog.qml"), "LodPreferencesDialog"); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ee0ef21ae0..ad04e8da3a 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -136,7 +136,7 @@ glm::quat Avatar::getWorldAlignedOrientation () const { AABox Avatar::getBounds() const { // Our skeleton models are rigged, and this method call safely produces the static bounds of the model. - // Except, that getPartBounds produces an infinite, uncentered bounding box when the model is not yet parsed, + // Except, that getPartBounds produces an infinite, uncentered bounding box when the model is not yet parsed, // and we want a centered one. NOTE: There is code that may never try to render, and thus never load and get the // real model bounds, if this is unrealistically small. if (!_skeletonModel.isRenderable()) { @@ -188,7 +188,7 @@ void Avatar::simulate(float deltaTime) { // simple frustum check float boundingRadius = getBoundingRadius(); - bool inViewFrustum = qApp->getViewFrustum()->sphereInFrustum(getPosition(), boundingRadius) != + bool inViewFrustum = qApp->getViewFrustum()->computeSphereViewLocation(getPosition(), boundingRadius) != ViewFrustum::OUTSIDE; { @@ -401,7 +401,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { frustum = qApp->getDisplayViewFrustum(); } - if (frustum->sphereInFrustum(getPosition(), boundingRadius) == ViewFrustum::OUTSIDE) { + if (frustum->computeSphereViewLocation(getPosition(), boundingRadius) == ViewFrustum::OUTSIDE) { endRender(); return; } @@ -430,6 +430,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { if (renderArgs->_renderMode != RenderArgs::SHADOW_RENDER_MODE) { // add local lights const float BASE_LIGHT_DISTANCE = 2.0f; + const float LIGHT_FALLOFF_RADIUS = 0.01f; const float LIGHT_EXPONENT = 1.0f; const float LIGHT_CUTOFF = glm::radians(80.0f); float distance = BASE_LIGHT_DISTANCE * getUniformScale(); @@ -438,7 +439,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { foreach (const AvatarManager::LocalLight& light, DependencyManager::get()->getLocalLights()) { glm::vec3 direction = orientation * light.direction; DependencyManager::get()->addSpotLight(position - direction * distance, - distance * 2.0f, light.color, 0.5f, orientation, LIGHT_EXPONENT, LIGHT_CUTOFF); + distance * 2.0f, light.color, 0.5f, LIGHT_FALLOFF_RADIUS, orientation, LIGHT_EXPONENT, LIGHT_CUTOFF); } } @@ -516,7 +517,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { auto& frustum = *renderArgs->_viewFrustum; auto textPosition = getDisplayNamePosition(); - if (frustum.pointInFrustum(textPosition, true) == ViewFrustum::INSIDE) { + if (frustum.computePointFrustumLocation(textPosition) == ViewFrustum::INSIDE) { renderDisplayName(batch, frustum, textPosition); } } @@ -670,7 +671,7 @@ glm::vec3 Avatar::getDisplayNamePosition() const { } Transform Avatar::calculateDisplayNameTransform(const ViewFrustum& frustum, const glm::vec3& textPosition) const { - Q_ASSERT_X(frustum.pointInFrustum(textPosition, true) == ViewFrustum::INSIDE, + Q_ASSERT_X(frustum.computePointFrustumLocation(textPosition) == ViewFrustum::INSIDE, "Avatar::calculateDisplayNameTransform", "Text not in viewfrustum."); glm::vec3 toFrustum = frustum.getPosition() - textPosition; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6318a9bb1e..fc715eebe9 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -656,7 +656,7 @@ void MyAvatar::saveData() { settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); - settings.setValue("snapTurn", _useSnapTurn); + settings.setValue("useSnapTurn", _useSnapTurn); settings.endGroup(); } @@ -750,7 +750,7 @@ void MyAvatar::loadData() { setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); - setSnapTurn(settings.value("snapTurn").toBool()); + setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); settings.endGroup(); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index b8c82498a1..4c9c0df1cb 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -25,7 +25,7 @@ WindowScriptingInterface::WindowScriptingInterface() { const DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged); - connect(qApp, &Application::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused); connect(qApp, &Application::svoImportRequested, [this](const QString& urlString) { static const QMetaMethod svoImportRequestedSignal = diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 85a4b13b29..bc1f614f79 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -134,13 +134,14 @@ void EntityTreeRenderer::update() { EntityTreePointer tree = std::static_pointer_cast(_tree); tree->update(); - // check to see if the avatar has moved and if we need to handle enter/leave entity logic - checkEnterLeaveEntities(); + // Handle enter/leave entity logic + bool updated = checkEnterLeaveEntities(); - // even if we haven't changed positions, if we previously attempted to set the skybox, but - // have a pending download of the skybox texture, then we should attempt to reapply to - // get the correct texture. - if (_pendingSkyboxTexture && _skyboxTexture && _skyboxTexture->isLoaded()) { + // If we haven't already updated and previously attempted to load a texture, + // check if the texture loaded and apply it + if (!updated && ( + (_pendingSkyboxTexture && _skyboxTexture && _skyboxTexture->isLoaded()) || + (_pendingAmbientTexture && _ambientTexture && _ambientTexture->isLoaded()))) { applyZonePropertiesToScene(_bestZone); } @@ -156,7 +157,7 @@ void EntityTreeRenderer::update() { deleteReleasedModels(); } -void EntityTreeRenderer::checkEnterLeaveEntities() { +bool EntityTreeRenderer::checkEnterLeaveEntities() { if (_tree && !_shuttingDown) { glm::vec3 avatarPosition = _viewState->getAvatarPosition(); @@ -171,7 +172,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { std::static_pointer_cast(_tree)->findEntities(avatarPosition, radius, foundEntities); // Whenever you're in an intersection between zones, we will always choose the smallest zone. - _bestZone = NULL; // NOTE: Is this what we want? + _bestZone = nullptr; // NOTE: Is this what we want? _bestZoneVolume = std::numeric_limits::max(); // create a list of entities that actually contain the avatar's position @@ -204,7 +205,6 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { } applyZonePropertiesToScene(_bestZone); - }); // Note: at this point we don't need to worry about the tree being locked, because we only deal with @@ -228,8 +228,11 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { } _currentEntitiesInside = entitiesContainingAvatar; _lastAvatarPosition = avatarPosition; + + return true; } } + return false; } void EntityTreeRenderer::leaveAllEntities() { @@ -253,6 +256,7 @@ void EntityTreeRenderer::forceRecheckEntities() { void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr zone) { + auto textureCache = DependencyManager::get(); auto scene = DependencyManager::get(); auto sceneStage = scene->getStage(); auto skyStage = scene->getSkyStage(); @@ -264,7 +268,11 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrresetAmbientSphere(); sceneKeyLight->setColor(_previousKeyLightColor); sceneKeyLight->setIntensity(_previousKeyLightIntensity); sceneKeyLight->setAmbientIntensity(_previousKeyLightAmbientIntensity); @@ -274,6 +282,7 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrsetHour(_previousStageHour); sceneTime->setDay(_previousStageDay); + _hasPreviousZone = false; } @@ -306,6 +315,23 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrsetHour(zone->getStageProperties().calculateHour()); sceneTime->setDay(zone->getStageProperties().calculateDay()); + bool isAmbientTextureSet = false; + if (zone->getKeyLightProperties().getAmbientURL().isEmpty()) { + _pendingAmbientTexture = false; + _ambientTexture.clear(); + } else { + _ambientTexture = textureCache->getTexture(zone->getKeyLightProperties().getAmbientURL(), CUBE_TEXTURE); + if (_ambientTexture->getGPUTexture()) { + _pendingAmbientTexture = false; + if (_ambientTexture->getGPUTexture()->getIrradiance()) { + sceneKeyLight->setAmbientSphere(_ambientTexture->getGPUTexture()->getIrradiance()); + isAmbientTextureSet = true; + } + } else { + _pendingAmbientTexture = true; + } + } + switch (zone->getBackgroundMode()) { case BACKGROUND_MODE_SKYBOX: { auto skybox = std::dynamic_pointer_cast(skyStage->getSkybox()); @@ -326,12 +352,16 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr(); _skyboxTexture = textureCache->getTexture(zone->getSkyboxProperties().getURL(), CUBE_TEXTURE); if (_skyboxTexture->getGPUTexture()) { - skybox->setCubemap(_skyboxTexture->getGPUTexture()); + auto texture = _skyboxTexture->getGPUTexture(); + skybox->setCubemap(texture); _pendingSkyboxTexture = false; + if (!isAmbientTextureSet && texture->getIrradiance()) { + sceneKeyLight->setAmbientSphere(texture->getIrradiance()); + isAmbientTextureSet = true; + } } else { _pendingSkyboxTexture = true; } @@ -348,6 +378,10 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrresetAmbientSphere(); + } } const FBXGeometry* EntityTreeRenderer::getGeometryForEntity(EntityItemPointer entityItem) { @@ -819,3 +853,19 @@ void EntityTreeRenderer::updateEntityRenderStatus(bool shouldRenderEntities) { } } } + +void EntityTreeRenderer::updateZone(const EntityItemID& id) { + if (!_bestZone) { + // Get in the zone! + auto zone = getTree()->findEntityByEntityItemID(id); + if (zone && zone->contains(_lastAvatarPosition)) { + _currentEntitiesInside << id; + emit enterEntity(id); + _entitiesScriptEngine->callEntityScriptMethod(id, "enterEntity"); + _bestZone = std::dynamic_pointer_cast(zone); + } + } + if (_bestZone && _bestZone->getID() == id) { + applyZonePropertiesToScene(_bestZone); + } +} diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index f0454b2ecf..5aadf7d299 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -109,6 +109,7 @@ public slots: void entitySciptChanging(const EntityItemID& entityID, const bool reload); void entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); void updateEntityRenderStatus(bool shouldRenderEntities); + void updateZone(const EntityItemID& id); // optional slots that can be wired to menu items void setDisplayModelBounds(bool value) { _displayModelBounds = value; } @@ -136,16 +137,19 @@ private: EntityItemID _currentClickingOnEntityID; QScriptValueList createEntityArgs(const EntityItemID& entityID); - void checkEnterLeaveEntities(); + bool checkEnterLeaveEntities(); void leaveAllEntities(); void forceRecheckEntities(); - glm::vec3 _lastAvatarPosition; + glm::vec3 _lastAvatarPosition { 0.0f }; QVector _currentEntitiesInside; bool _pendingSkyboxTexture { false }; NetworkTexturePointer _skyboxTexture; + bool _pendingAmbientTexture { false }; + NetworkTexturePointer _ambientTexture; + bool _wantScripts; ScriptEngine* _entitiesScriptEngine; diff --git a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp index 39182f322c..fb6061e94f 100644 --- a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp @@ -36,15 +36,16 @@ void RenderableLightEntityItem::render(RenderArgs* args) { glm::vec3 color = toGlm(getXColor()); float intensity = getIntensity(); + float falloffRadius = getFalloffRadius(); float exponent = getExponent(); float cutoff = glm::radians(getCutoff()); if (_isSpotlight) { DependencyManager::get()->addSpotLight(position, largestDiameter / 2.0f, - color, intensity, rotation, exponent, cutoff); + color, intensity, falloffRadius, rotation, exponent, cutoff); } else { DependencyManager::get()->addPointLight(position, largestDiameter / 2.0f, - color, intensity); + color, intensity, falloffRadius); } #ifdef WANT_DEBUG diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 44345ac0a2..ef47a777c2 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -18,6 +18,7 @@ #include #include +#include "EntityTreeRenderer.h" #include "RenderableEntityItem.h" // Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1 @@ -62,6 +63,10 @@ bool RenderableZoneEntityItem::setProperties(const EntityItemProperties& propert return somethingChanged; } +void RenderableZoneEntityItem::somethingChangedNotification() { + DependencyManager::get()->updateZone(_id); +} + int RenderableZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, EntityPropertyFlags& propertyFlags, bool overwriteLocalData, diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.h b/libraries/entities-renderer/src/RenderableZoneEntityItem.h index 6eb829a48f..4ba862fff8 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.h +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.h @@ -28,6 +28,8 @@ public: { } virtual bool setProperties(const EntityItemProperties& properties); + virtual void somethingChangedNotification() override; + virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, EntityPropertyFlags& propertyFlags, bool overwriteLocalData, diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 8e0983f62a..550ec205c0 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -245,6 +245,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_DYNAMIC, dynamic); CHECK_PROPERTY_CHANGE(PROP_IS_SPOTLIGHT, isSpotlight); CHECK_PROPERTY_CHANGE(PROP_INTENSITY, intensity); + CHECK_PROPERTY_CHANGE(PROP_FALLOFF_RADIUS, falloffRadius); CHECK_PROPERTY_CHANGE(PROP_EXPONENT, exponent); CHECK_PROPERTY_CHANGE(PROP_CUTOFF, cutoff); CHECK_PROPERTY_CHANGE(PROP_LOCKED, locked); @@ -445,6 +446,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool if (_type == EntityTypes::Light) { COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_SPOTLIGHT, isSpotlight); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_INTENSITY, intensity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FALLOFF_RADIUS, falloffRadius); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EXPONENT, exponent); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CUTOFF, cutoff); } @@ -597,6 +599,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(dynamic, bool, setDynamic); COPY_PROPERTY_FROM_QSCRIPTVALUE(isSpotlight, bool, setIsSpotlight); COPY_PROPERTY_FROM_QSCRIPTVALUE(intensity, float, setIntensity); + COPY_PROPERTY_FROM_QSCRIPTVALUE(falloffRadius, float, setFalloffRadius); COPY_PROPERTY_FROM_QSCRIPTVALUE(exponent, float, setExponent); COPY_PROPERTY_FROM_QSCRIPTVALUE(cutoff, float, setCutoff); COPY_PROPERTY_FROM_QSCRIPTVALUE(locked, bool, setLocked); @@ -762,6 +765,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, dynamic, unused); ADD_PROPERTY_TO_MAP(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool); ADD_PROPERTY_TO_MAP(PROP_INTENSITY, Intensity, intensity, float); + ADD_PROPERTY_TO_MAP(PROP_FALLOFF_RADIUS, FalloffRadius, falloffRadius, float); ADD_PROPERTY_TO_MAP(PROP_EXPONENT, Exponent, exponent, float); ADD_PROPERTY_TO_MAP(PROP_CUTOFF, Cutoff, cutoff, float); ADD_PROPERTY_TO_MAP(PROP_LOCKED, Locked, locked, bool); @@ -1043,6 +1047,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_IS_SPOTLIGHT, properties.getIsSpotlight()); APPEND_ENTITY_PROPERTY(PROP_COLOR, properties.getColor()); APPEND_ENTITY_PROPERTY(PROP_INTENSITY, properties.getIntensity()); + APPEND_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, properties.getFalloffRadius()); APPEND_ENTITY_PROPERTY(PROP_EXPONENT, properties.getExponent()); APPEND_ENTITY_PROPERTY(PROP_CUTOFF, properties.getCutoff()); } @@ -1332,6 +1337,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_IS_SPOTLIGHT, bool, setIsSpotlight); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLOR, xColor, setColor); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_INTENSITY, float, setIntensity); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FALLOFF_RADIUS, float, setFalloffRadius); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_EXPONENT, float, setExponent); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CUTOFF, float, setCutoff); } @@ -1477,6 +1483,7 @@ void EntityItemProperties::markAllChanged() { _dynamicChanged = true; _intensityChanged = true; + _falloffRadiusChanged = true; _exponentChanged = true; _cutoffChanged = true; _lockedChanged = true; @@ -1719,6 +1726,9 @@ QList EntityItemProperties::listChangedProperties() { if (intensityChanged()) { out += "intensity"; } + if (falloffRadiusChanged()) { + out += "falloffRadius"; + } if (exponentChanged()) { out += "exponent"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 2cfef17c1b..c732d01fa5 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -35,6 +35,7 @@ #include "EntityItemPropertiesMacros.h" #include "EntityTypes.h" #include "EntityPropertyFlags.h" +#include "LightEntityItem.h" #include "LineEntityItem.h" #include "ParticleEffectEntityItem.h" #include "PolyVoxEntityItem.h" @@ -129,10 +130,11 @@ public: DEFINE_PROPERTY(PROP_COLLISIONLESS, Collisionless, collisionless, bool, ENTITY_ITEM_DEFAULT_COLLISIONLESS); DEFINE_PROPERTY(PROP_COLLISION_MASK, CollisionMask, collisionMask, uint8_t, ENTITY_COLLISION_MASK_DEFAULT); DEFINE_PROPERTY(PROP_DYNAMIC, Dynamic, dynamic, bool, ENTITY_ITEM_DEFAULT_DYNAMIC); - DEFINE_PROPERTY(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool, false); - DEFINE_PROPERTY(PROP_INTENSITY, Intensity, intensity, float, 1.0f); - DEFINE_PROPERTY(PROP_EXPONENT, Exponent, exponent, float, 0.0f); - DEFINE_PROPERTY(PROP_CUTOFF, Cutoff, cutoff, float, ENTITY_ITEM_DEFAULT_CUTOFF); + DEFINE_PROPERTY(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool, LightEntityItem::DEFAULT_IS_SPOTLIGHT); + DEFINE_PROPERTY(PROP_INTENSITY, Intensity, intensity, float, LightEntityItem::DEFAULT_INTENSITY); + DEFINE_PROPERTY(PROP_FALLOFF_RADIUS, FalloffRadius, falloffRadius, float, LightEntityItem::DEFAULT_FALLOFF_RADIUS); + DEFINE_PROPERTY(PROP_EXPONENT, Exponent, exponent, float, LightEntityItem::DEFAULT_EXPONENT); + DEFINE_PROPERTY(PROP_CUTOFF, Cutoff, cutoff, float, LightEntityItem::DEFAULT_CUTOFF); DEFINE_PROPERTY(PROP_LOCKED, Locked, locked, bool, ENTITY_ITEM_DEFAULT_LOCKED); DEFINE_PROPERTY_REF(PROP_TEXTURES, Textures, textures, QString, ""); DEFINE_PROPERTY_REF(PROP_USER_DATA, UserData, userData, QString, ENTITY_ITEM_DEFAULT_USER_DATA); @@ -359,6 +361,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, Dynamic, dynamic, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, IsSpotlight, isSpotlight, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Intensity, intensity, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, FalloffRadius, falloffRadius, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Exponent, exponent, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Cutoff, cutoff, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Locked, locked, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index 5375b1bc3a..aa4fb5c619 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -72,8 +72,6 @@ const bool ENTITY_ITEM_DEFAULT_COLLISIONLESS = false; const bool ENTITY_ITEM_DEFAULT_DYNAMIC = false; const bool ENTITY_ITEM_DEFAULT_BILLBOARDED = false; -const float ENTITY_ITEM_DEFAULT_CUTOFF = PI / 2; - const QString ENTITY_ITEM_DEFAULT_NAME = QString(""); #endif // hifi_EntityItemPropertiesDefaults_h diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index b60fc6174c..90a7c1e2f7 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -167,6 +167,8 @@ enum EntityPropertyList { PROP_COLLISION_MASK, // one byte of collision group flags + PROP_FALLOFF_RADIUS, // for Light entity + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 4abbe3c3fa..8ff93bf808 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -85,7 +85,7 @@ void EntityTreeElement::initializeExtraEncodeData(EncodeBitstreamParams& params) forEachEntity([&](EntityItemPointer entity) { entityTreeElementExtraEncodeData->entities.insert(entity->getEntityItemID(), entity->getEntityProperties(params)); }); - + // TODO: some of these inserts might be redundant!!! extraEncodeData->insert(this, entityTreeElementExtraEncodeData); } @@ -96,39 +96,39 @@ bool EntityTreeElement::shouldIncludeChildData(int childIndex, EncodeBitstreamPa assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData + EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = static_cast(extraEncodeData->value(this)); - + bool childCompleted = entityTreeElementExtraEncodeData->childCompleted[childIndex]; - + // If we haven't completely sent the child yet, then we should include it return !childCompleted; } - + // I'm not sure this should ever happen, since we should have the extra encode data if we're considering // the child data for this element assert(false); return false; } -bool EntityTreeElement::shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { +bool EntityTreeElement::shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { EntityTreeElementPointer childElement = getChildAtIndex(childIndex); if (childElement->alreadyFullyEncoded(params)) { return false; } - + return true; // if we don't know otherwise than recurse! } -bool EntityTreeElement::alreadyFullyEncoded(EncodeBitstreamParams& params) const { +bool EntityTreeElement::alreadyFullyEncoded(EncodeBitstreamParams& params) const { OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData; assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData + EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = static_cast(extraEncodeData->value(this)); - // If we know that ALL subtrees below us have already been recursed, then we don't + // If we know that ALL subtrees below us have already been recursed, then we don't // need to recurse this child. return entityTreeElementExtraEncodeData->subtreeCompleted; } @@ -139,7 +139,7 @@ void EntityTreeElement::updateEncodedData(int childIndex, AppendState childAppen OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData; assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData + EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = static_cast(extraEncodeData->value(this)); if (childAppendState == OctreeElement::COMPLETED) { @@ -155,7 +155,7 @@ void EntityTreeElement::updateEncodedData(int childIndex, AppendState childAppen void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) const { const bool wantDebug = false; - + if (wantDebug) { qCDebug(entities) << "EntityTreeElement::elementEncodeComplete() element:" << _cube; } @@ -188,7 +188,7 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con // If we've encoding this element before... but we're coming back a second time in an attempt to // encoud our parent... this might happen. if (extraEncodeData->contains(childElement.get())) { - EntityTreeElementExtraEncodeData* childExtraEncodeData + EntityTreeElementExtraEncodeData* childExtraEncodeData = static_cast(extraEncodeData->value(childElement.get())); if (wantDebug) { @@ -197,7 +197,7 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con qCDebug(entities) << " childExtraEncodeData->elementCompleted:" << childExtraEncodeData->elementCompleted; qCDebug(entities) << " childExtraEncodeData->subtreeCompleted:" << childExtraEncodeData->subtreeCompleted; } - + if (childElement->isLeaf() && childExtraEncodeData->elementCompleted) { if (wantDebug) { qCDebug(entities) << " CHILD IS LEAF -- AND CHILD ELEMENT DATA COMPLETED!!!"; @@ -217,24 +217,24 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con qCDebug(entities) << " WAS elementCompleted:" << thisExtraEncodeData->elementCompleted; qCDebug(entities) << " WAS subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; } - + thisExtraEncodeData->subtreeCompleted = !someChildTreeNotComplete; if (wantDebug) { qCDebug(entities) << " NOW elementCompleted:" << thisExtraEncodeData->elementCompleted; qCDebug(entities) << " NOW subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; - + if (thisExtraEncodeData->subtreeCompleted) { qCDebug(entities) << " YEAH!!!!! >>>>>>>>>>>>>> NOW subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; } } } -OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData* packetData, +OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const { OctreeElement::AppendState appendElementState = OctreeElement::COMPLETED; // assume the best... - + // first, check the params.extraEncodeData to see if there's any partial re-encode data for this element OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData; EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = NULL; @@ -280,7 +280,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData QVector indexesOfEntitiesToInclude; // It's possible that our element has been previous completed. In this case we'll simply not include any of our - // entities for encoding. This is needed because we encode the element data at the "parent" level, and so we + // entities for encoding. This is needed because we encode the element data at the "parent" level, and so we // need to handle the case where our sibling elements need encoding but we don't. if (!entityTreeElementExtraEncodeData->elementCompleted) { for (uint16_t i = 0; i < _entityItems.size(); i++) { @@ -304,7 +304,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData // frustum culling on rendering. bool success; AACube entityCube = entity->getQueryAACube(success); - if (!success || params.viewFrustum->cubeInFrustum(entityCube) == ViewFrustum::OUTSIDE) { + if (!success || params.viewFrustum->computeCubeViewLocation(entityCube) == ViewFrustum::OUTSIDE) { includeThisEntity = false; // out of view, don't include it } @@ -397,7 +397,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData // this octree element. if (extraEncodeData && entityTreeElementExtraEncodeData) { - // After processing, if we are PARTIAL or COMPLETED then we need to re-include our extra data. + // After processing, if we are PARTIAL or COMPLETED then we need to re-include our extra data. // Only our parent can remove our extra data in these cases and only after it knows that all of its // children have been encoded. // If we weren't able to encode ANY data about ourselves, then we go ahead and remove our element data @@ -412,7 +412,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData extraEncodeData->insert(this, entityTreeElementExtraEncodeData); } } else { - + // If we weren't previously completed, check to see if we are if (!entityTreeElementExtraEncodeData->elementCompleted) { // If all of our items have been encoded, then we are complete as an element. @@ -426,9 +426,9 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData } } - // Determine if no entities at all were able to fit + // Determine if no entities at all were able to fit bool noEntitiesFit = (numberOfEntities > 0 && actualNumberOfEntities == 0); - + // If we wrote fewer entities than we expected, update the number of entities in our packet bool successUpdateEntityCount = true; if (numberOfEntities != actualNumberOfEntities) { @@ -504,7 +504,7 @@ bool EntityTreeElement::bestFitBounds(const glm::vec3& minPoint, const glm::vec3 glm::vec3 clampedMax = glm::clamp(maxPoint, (float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); if (_cube.contains(clampedMin) && _cube.contains(clampedMax)) { - + // If our child would be smaller than our smallest reasonable element, then we are the best fit. float childScale = _cube.getScale() / 2.0f; if (childScale <= SMALLEST_REASONABLE_OCTREE_ELEMENT_SCALE) { @@ -524,7 +524,7 @@ bool EntityTreeElement::bestFitBounds(const glm::vec3& minPoint, const glm::vec3 bool EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, + BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard, void** intersectedObject, bool precisionPicking) { keepSearching = true; // assume that we will continue searching after this. @@ -607,7 +607,7 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con // we can use the AABox's ray intersection by mapping our origin and direction into the entity frame // and testing intersection there. - if (entityFrameBox.findRayIntersection(entityFrameOrigin, entityFrameDirection, localDistance, + if (entityFrameBox.findRayIntersection(entityFrameOrigin, entityFrameDirection, localDistance, localFace, localSurfaceNormal)) { if (localDistance < distance) { // now ask the entity if we actually intersect @@ -862,12 +862,12 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int if (this == _myTree->getRoot().get() && args.bitstreamVersion < VERSION_ROOT_ELEMENT_HAS_DATA) { return 0; } - + const unsigned char* dataAt = data; int bytesRead = 0; uint16_t numberOfEntities = 0; int expectedBytesPerEntity = EntityItem::expectedBytes(); - + args.elementsPerPacket++; if (bytesLeftToRead >= (int)sizeof(numberOfEntities)) { @@ -947,7 +947,7 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int entityItem->recordCreationTime(); } } else { - qDebug() << "Recieved packet for previously deleted entity [" << + qDebug() << "Recieved packet for previously deleted entity [" << entityItem->getID() << "] ignoring. (inside " << __FUNCTION__ << ")"; } } @@ -959,7 +959,7 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int } } } - + return bytesRead; } @@ -990,7 +990,7 @@ bool EntityTreeElement::pruneChildren() { bool somethingPruned = false; for (int childIndex = 0; childIndex < NUMBER_OF_CHILDREN; childIndex++) { EntityTreeElementPointer child = getChildAtIndex(childIndex); - + // if my child is a leaf, but has no entities, then it's safe to delete my child if (child && child->isLeaf() && !child->hasEntities()) { deleteChildAtIndex(childIndex); @@ -1040,4 +1040,4 @@ void EntityTreeElement::debugDump() { } }); } - + diff --git a/libraries/entities/src/KeyLightPropertyGroup.cpp b/libraries/entities/src/KeyLightPropertyGroup.cpp index b7e0a70b85..cac47c907a 100644 --- a/libraries/entities/src/KeyLightPropertyGroup.cpp +++ b/libraries/entities/src/KeyLightPropertyGroup.cpp @@ -30,7 +30,7 @@ void KeyLightPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desired COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_INTENSITY, KeyLight, keyLight, Intensity, intensity); COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_INTENSITY, KeyLight, keyLight, AmbientIntensity, ambientIntensity); COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_DIRECTION, KeyLight, keyLight, Direction, direction); - COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_URL, KeyLight, keyLight, AmbientURL, ambientUrl); + COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_AMBIENT_URL, KeyLight, keyLight, AmbientURL, ambientURL); } diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index ac56fc9c1f..852b37a751 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -21,6 +21,12 @@ #include "EntityTreeElement.h" #include "LightEntityItem.h" +const bool LightEntityItem::DEFAULT_IS_SPOTLIGHT = false; +const float LightEntityItem::DEFAULT_INTENSITY = 1.0f; +const float LightEntityItem::DEFAULT_FALLOFF_RADIUS = 0.1f; +const float LightEntityItem::DEFAULT_EXPONENT = 0.0f; +const float LightEntityItem::DEFAULT_CUTOFF = PI / 2.0f; + bool LightEntityItem::_lightsArePickable = false; EntityItemPointer LightEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { @@ -32,12 +38,7 @@ EntityItemPointer LightEntityItem::factory(const EntityItemID& entityID, const E // our non-pure virtual subclass for now... LightEntityItem::LightEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { _type = EntityTypes::Light; - - // default property values _color[RED_INDEX] = _color[GREEN_INDEX] = _color[BLUE_INDEX] = 0; - _intensity = 1.0f; - _exponent = 0.0f; - _cutoff = PI; } void LightEntityItem::setDimensions(const glm::vec3& value) { @@ -62,10 +63,15 @@ EntityItemProperties LightEntityItem::getProperties(EntityPropertyFlags desiredP COPY_ENTITY_PROPERTY_TO_PROPERTIES(intensity, getIntensity); COPY_ENTITY_PROPERTY_TO_PROPERTIES(exponent, getExponent); COPY_ENTITY_PROPERTY_TO_PROPERTIES(cutoff, getCutoff); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(falloffRadius, getFalloffRadius); return properties; } +void LightEntityItem::setFalloffRadius(float value) { + _falloffRadius = glm::max(value, 0.0f); +} + void LightEntityItem::setIsSpotlight(bool value) { if (value != _isSpotlight) { _isSpotlight = value; @@ -101,6 +107,7 @@ bool LightEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(intensity, setIntensity); SET_ENTITY_PROPERTY_FROM_PROPERTIES(exponent, setExponent); SET_ENTITY_PROPERTY_FROM_PROPERTIES(cutoff, setCutoff); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(falloffRadius, setFalloffRadius); if (somethingChanged) { bool wantDebug = false; @@ -150,6 +157,7 @@ int LightEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_INTENSITY, float, setIntensity); READ_ENTITY_PROPERTY(PROP_EXPONENT, float, setExponent); READ_ENTITY_PROPERTY(PROP_CUTOFF, float, setCutoff); + READ_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, float, setFalloffRadius); } return bytesRead; @@ -164,6 +172,7 @@ EntityPropertyFlags LightEntityItem::getEntityProperties(EncodeBitstreamParams& requestedProperties += PROP_INTENSITY; requestedProperties += PROP_EXPONENT; requestedProperties += PROP_CUTOFF; + requestedProperties += PROP_FALLOFF_RADIUS; return requestedProperties; } @@ -181,4 +190,5 @@ void LightEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_INTENSITY, getIntensity()); APPEND_ENTITY_PROPERTY(PROP_EXPONENT, getExponent()); APPEND_ENTITY_PROPERTY(PROP_CUTOFF, getCutoff()); + APPEND_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, getFalloffRadius()); } diff --git a/libraries/entities/src/LightEntityItem.h b/libraries/entities/src/LightEntityItem.h index 103c462809..4c84d3204c 100644 --- a/libraries/entities/src/LightEntityItem.h +++ b/libraries/entities/src/LightEntityItem.h @@ -16,6 +16,12 @@ class LightEntityItem : public EntityItem { public: + static const bool DEFAULT_IS_SPOTLIGHT; + static const float DEFAULT_INTENSITY; + static const float DEFAULT_FALLOFF_RADIUS; + static const float DEFAULT_EXPONENT; + static const float DEFAULT_CUTOFF; + static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); LightEntityItem(const EntityItemID& entityItemID); @@ -65,6 +71,9 @@ public: float getIntensity() const { return _intensity; } void setIntensity(float value) { _intensity = value; } + float getFalloffRadius() const { return _falloffRadius; } + void setFalloffRadius(float value); + float getExponent() const { return _exponent; } void setExponent(float value) { _exponent = value; } @@ -78,10 +87,11 @@ protected: // properties of a light rgbColor _color; - bool _isSpotlight; - float _intensity; - float _exponent; - float _cutoff; + bool _isSpotlight { DEFAULT_IS_SPOTLIGHT }; + float _intensity { DEFAULT_INTENSITY }; + float _falloffRadius { DEFAULT_FALLOFF_RADIUS }; + float _exponent { DEFAULT_EXPONENT }; + float _cutoff { DEFAULT_CUTOFF }; static bool _lightsArePickable; }; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index 779d70cc88..abaa0f9ef1 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -21,6 +21,7 @@ Framebuffer::~Framebuffer() { Framebuffer* Framebuffer::create() { auto framebuffer = new Framebuffer(); framebuffer->_renderBuffers.resize(MAX_NUM_RENDER_BUFFERS); + framebuffer->_colorStamps.resize(MAX_NUM_RENDER_BUFFERS, 0); return framebuffer; } @@ -174,6 +175,8 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin } } + ++_colorStamps[slot]; + updateSize(texture); // assign the new one @@ -190,6 +193,7 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin } void Framebuffer::removeRenderBuffers() { + if (isSwapchain()) { return; } @@ -230,6 +234,7 @@ uint32 Framebuffer::getRenderBufferSubresource(uint32 slot) const { } bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const Format& format, uint32 subresource) { + ++_depthStamp; if (isSwapchain()) { return false; } diff --git a/libraries/gpu/src/gpu/Framebuffer.h b/libraries/gpu/src/gpu/Framebuffer.h index e986e4a481..5d016645fe 100755 --- a/libraries/gpu/src/gpu/Framebuffer.h +++ b/libraries/gpu/src/gpu/Framebuffer.h @@ -135,10 +135,15 @@ public: static uint32 getMaxNumRenderBuffers() { return MAX_NUM_RENDER_BUFFERS; } const GPUObjectPointer gpuObject {}; - + + Stamp getDepthStamp() const { return _depthStamp; } + const std::vector& getColorStamps() const { return _colorStamps; } + protected: SwapchainPointer _swapchain; + Stamp _depthStamp { 0 }; + std::vector _colorStamps; TextureViews _renderBuffers; TextureView _depthStencilBuffer; diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 7f37a73c57..15338d7587 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -173,6 +173,9 @@ public: public: GLuint _fbo = 0; std::vector _colorBuffers; + Stamp _depthStamp { 0 }; + std::vector _colorStamps; + GLFramebuffer(); ~GLFramebuffer(); diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp index 5f226141a8..37a10e670b 100755 --- a/libraries/gpu/src/gpu/GLBackendOutput.cpp +++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp @@ -27,8 +27,15 @@ GLBackend::GLFramebuffer::~GLFramebuffer() { GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffer) { GLFramebuffer* object = Backend::getGPUObject(framebuffer); + bool needsUpate { false }; + if (!object || + framebuffer.getDepthStamp() != object->_depthStamp || + framebuffer.getColorStamps() != object->_colorStamps) { + needsUpate = true; + } + // If GPU object already created and in sync - if (object) { + if (!needsUpate) { return object; } else if (framebuffer.isEmpty()) { // NO framebuffer definition yet so let's avoid thinking @@ -37,94 +44,112 @@ GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffe // need to have a gpu object? if (!object) { - GLint currentFBO; + // All is green, assign the gpuobject to the Framebuffer + object = new GLFramebuffer(); + Backend::setGPUObject(framebuffer, object); + glGenFramebuffers(1, &object->_fbo); + (void)CHECK_GL_ERROR(); + } + + if (needsUpate) { + GLint currentFBO = -1; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤tFBO); - - GLuint fbo; - glGenFramebuffers(1, &fbo); - (void) CHECK_GL_ERROR(); + glBindFramebuffer(GL_FRAMEBUFFER, object->_fbo); - glBindFramebuffer(GL_FRAMEBUFFER, fbo); + GLTexture* gltexture = nullptr; + TexturePointer surface; + if (framebuffer.getColorStamps() != object->_colorStamps) { + if (framebuffer.hasColor()) { + object->_colorBuffers.clear(); + static const GLenum colorAttachments[] = { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1, + GL_COLOR_ATTACHMENT2, + GL_COLOR_ATTACHMENT3, + GL_COLOR_ATTACHMENT4, + GL_COLOR_ATTACHMENT5, + GL_COLOR_ATTACHMENT6, + GL_COLOR_ATTACHMENT7, + GL_COLOR_ATTACHMENT8, + GL_COLOR_ATTACHMENT9, + GL_COLOR_ATTACHMENT10, + GL_COLOR_ATTACHMENT11, + GL_COLOR_ATTACHMENT12, + GL_COLOR_ATTACHMENT13, + GL_COLOR_ATTACHMENT14, + GL_COLOR_ATTACHMENT15 }; - std::vector colorBuffers; - if (framebuffer.hasColor()) { - static const GLenum colorAttachments[] = { - GL_COLOR_ATTACHMENT0, - GL_COLOR_ATTACHMENT1, - GL_COLOR_ATTACHMENT2, - GL_COLOR_ATTACHMENT3, - GL_COLOR_ATTACHMENT4, - GL_COLOR_ATTACHMENT5, - GL_COLOR_ATTACHMENT6, - GL_COLOR_ATTACHMENT7, - GL_COLOR_ATTACHMENT8, - GL_COLOR_ATTACHMENT9, - GL_COLOR_ATTACHMENT10, - GL_COLOR_ATTACHMENT11, - GL_COLOR_ATTACHMENT12, - GL_COLOR_ATTACHMENT13, - GL_COLOR_ATTACHMENT14, - GL_COLOR_ATTACHMENT15 }; + int unit = 0; + for (auto& b : framebuffer.getRenderBuffers()) { + surface = b._texture; + if (surface) { + gltexture = GLBackend::syncGPUObject(*surface); + } else { + gltexture = nullptr; + } - int unit = 0; - for (auto& b : framebuffer.getRenderBuffers()) { - auto surface = b._texture; - if (surface) { - auto gltexture = GLBackend::syncGPUObject(*surface); if (gltexture) { glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); + object->_colorBuffers.push_back(colorAttachments[unit]); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, 0, 0); } - colorBuffers.push_back(colorAttachments[unit]); unit++; } } - } -#if (GPU_FEATURE_PROFILE == GPU_LEGACY) - // for reasons that i don't understand yet, it seems that on mac gl, a fbo must have a color buffer... - else { - GLuint renderBuffer = 0; - glGenRenderbuffers(1, &renderBuffer); - glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, framebuffer.getWidth(), framebuffer.getHeight()); - glBindRenderbuffer(GL_RENDERBUFFER, 0); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer); - (void) CHECK_GL_ERROR(); - } - - // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); -#endif - - - if (framebuffer.hasDepthStencil()) { - auto surface = framebuffer.getDepthStencilBuffer(); - if (surface) { - auto gltexture = GLBackend::syncGPUObject(*surface); - if (gltexture) { - GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; - if (!framebuffer.hasStencil()) { - attachement = GL_DEPTH_ATTACHMENT; - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); - } else if (!framebuffer.hasDepth()) { - attachement = GL_STENCIL_ATTACHMENT; - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); - } else { - attachement = GL_DEPTH_STENCIL_ATTACHMENT; - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); - } - (void) CHECK_GL_ERROR(); - } + #if (GPU_FEATURE_PROFILE == GPU_LEGACY) + // for reasons that i don't understand yet, it seems that on mac gl, a fbo must have a color buffer... + else { + GLuint renderBuffer = 0; + glGenRenderbuffers(1, &renderBuffer); + glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, framebuffer.getWidth(), framebuffer.getHeight()); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer); + (void) CHECK_GL_ERROR(); } + // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + #endif + object->_colorStamps = framebuffer.getColorStamps(); } + GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; + if (!framebuffer.hasStencil()) { + attachement = GL_DEPTH_ATTACHMENT; + } else if (!framebuffer.hasDepth()) { + attachement = GL_STENCIL_ATTACHMENT; + } + + if (framebuffer.getDepthStamp() != object->_depthStamp) { + auto surface = framebuffer.getDepthStencilBuffer(); + if (framebuffer.hasDepthStencil() && surface) { + gltexture = GLBackend::syncGPUObject(*surface); + } + + if (gltexture) { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, 0, 0); + } + object->_depthStamp = framebuffer.getDepthStamp(); + } + + // Last but not least, define where we draw - if (!colorBuffers.empty()) { - glDrawBuffers((GLsizei)colorBuffers.size(), colorBuffers.data()); + if (!object->_colorBuffers.empty()) { + glDrawBuffers((GLsizei)object->_colorBuffers.size(), object->_colorBuffers.data()); } else { glDrawBuffer( GL_NONE ); } // Now check for completness GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + // restore the current framebuffer + if (currentFBO != -1) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); + } + bool result = false; switch (status) { case GL_FRAMEBUFFER_COMPLETE : @@ -147,20 +172,10 @@ GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffe qCDebug(gpulogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; break; } - if (!result && fbo) { - glDeleteFramebuffers( 1, &fbo ); + if (!result && object->_fbo) { + glDeleteFramebuffers(1, &object->_fbo); return nullptr; } - - - // All is green, assign the gpuobject to the Framebuffer - object = new GLFramebuffer(); - object->_fbo = fbo; - object->_colorBuffers = colorBuffers; - Backend::setGPUObject(framebuffer, object); - - // restore the current framebuffer - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); } return object; diff --git a/libraries/model/src/model/Light.cpp b/libraries/model/src/model/Light.cpp index 7ab8b77f5e..8a016c4d77 100755 --- a/libraries/model/src/model/Light.cpp +++ b/libraries/model/src/model/Light.cpp @@ -12,19 +12,18 @@ using namespace model; -Light::Light() : - _flags(0), - _schemaBuffer(), - _transform() { +Light::Light() { // only if created from nothing shall we create the Buffer to store the properties Schema schema; _schemaBuffer = std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema); + updateLightRadius(); } Light::Light(const Light& light) : _flags(light._flags), _schemaBuffer(light._schemaBuffer), - _transform(light._transform) { + _transform(light._transform) +{ } Light& Light::operator= (const Light& light) { @@ -70,18 +69,36 @@ void Light::setAmbientIntensity(float intensity) { editSchema()._ambientIntensity = intensity; } +void Light::setFalloffRadius(float radius) { + if (radius <= 0.0f) { + radius = 0.1f; + } + editSchema()._attenuation.x = radius; + updateLightRadius(); +} void Light::setMaximumRadius(float radius) { if (radius <= 0.f) { radius = 1.0f; } - editSchema()._attenuation.w = radius; + editSchema()._attenuation.y = radius; updateLightRadius(); } void Light::updateLightRadius() { - float CutOffIntensityRatio = 0.05f; - float surfaceRadius = getMaximumRadius() / (sqrtf((getIntensity() * std::max(std::max(getColor().x, getColor().y), getColor().z)) / CutOffIntensityRatio) - 1.0f); - editSchema()._attenuation = Vec4(surfaceRadius, 1.0f/surfaceRadius, CutOffIntensityRatio, getMaximumRadius()); + // This function relies on the attenuation equation: + // I = Li / (1 + (d + Lr)/Lr)^2 + // where I = calculated intensity, Li = light intensity, Lr = light falloff radius, d = distance from surface + // see: https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/ + // note that falloff radius replaces surface radius in linked example + // This equation is biased back by Lr so that all lights act as true points, regardless of surface radii + + const float MIN_CUTOFF_INTENSITY = 0.001f; + // Get cutoff radius at minimum intensity + float intensity = getIntensity() * std::max(std::max(getColor().x, getColor().y), getColor().z); + float cutoffRadius = getFalloffRadius() * ((glm::sqrt(intensity / MIN_CUTOFF_INTENSITY) - 1) - 1); + + // If it is less than max radius, store it to buffer to avoid extra shading + editSchema()._attenuation.z = std::min(getMaximumRadius(), cutoffRadius); } #include diff --git a/libraries/model/src/model/Light.h b/libraries/model/src/model/Light.h index 96bf7fc427..dafbf90397 100755 --- a/libraries/model/src/model/Light.h +++ b/libraries/model/src/model/Light.h @@ -74,8 +74,17 @@ public: bool isRanged() const { return (getType() == POINT) || (getType() == SPOT ); } + // FalloffRradius is the physical radius of the light sphere through which energy shines, + // expressed in meters. It is used only to calculate the falloff curve of the light. + // Actual rendered lights will all have surface radii approaching 0. + void setFalloffRadius(float radius); + float getFalloffRadius() const { return getSchema()._attenuation.x; } + + // Maximum radius is the cutoff radius of the light energy, expressed in meters. + // It is used to bound light entities, and *will not* affect the falloff curve of the light. + // Setting it low will result in a noticeable cutoff. void setMaximumRadius(float radius); - float getMaximumRadius() const { return getSchema()._attenuation.w; } + float getMaximumRadius() const { return getSchema()._attenuation.y; } // Spot properties bool isSpot() const { return getType() == SPOT; } @@ -107,7 +116,7 @@ public: float _ambientIntensity{0.0f}; Color _color{1.0f}; float _intensity{1.0f}; - Vec4 _attenuation{1.0f}; + Vec4 _attenuation{0.1f, 1.0f, 0.0f, 0.0f}; Vec4 _spot{0.0f, 0.0f, 0.0f, 0.0f}; Vec4 _shadow{0.0f}; @@ -120,7 +129,7 @@ public: protected: - Flags _flags; + Flags _flags{ 0 }; UniformBufferView _schemaBuffer; Transform _transform; diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index 6529da1b61..af1c251ccb 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -66,14 +66,6 @@ vec3 getLightColor(Light l) { return l._color.rgb; } float getLightIntensity(Light l) { return l._color.w; } float getLightAmbientIntensity(Light l) { return l._direction.w; } -float evalLightAttenuation(Light l, float r) { - float d = max(r - l._attenuation.x, 0.0); - float denom = d * l._attenuation.y + 1.0; - float attenuation = 1.0 / (denom * denom); - return max((attenuation - l._attenuation.z)/(1.0 - l._attenuation.z), 0.0); - // return clamp(1.0/(l._attenuation.x + l._attenuation.y * r + l._attenuation.z * r * r), 0.0, 1.0); -} - float getLightSpotAngleCos(Light l) { return l._spot.x; } @@ -86,22 +78,33 @@ float evalLightSpotAttenuation(Light l, float cosA) { return pow(cosA, l._spot.w); } -float getLightSquareRadius(Light l) { - return l._attenuation.w * l._attenuation.w; -} - float getLightRadius(Light l) { - return l._attenuation.w; + return l._attenuation.x; } -float getLightAttenuationCutoff(Light l) { +float getLightSquareRadius(Light l) { + return getLightRadius(l) * getLightRadius(l); +} + +float getLightCutoffRadius(Light l) { return l._attenuation.z; } +float getLightCutoffSquareRadius(Light l) { + return getLightCutoffRadius(l) * getLightCutoffRadius(l); +} + float getLightShowContour(Light l) { return l._control.w; } +float evalLightAttenuation(Light l, float d) { + float radius = getLightRadius(l); + float denom = d / radius + 1.0; + float attenuation = min(1.0, 1.0 / (denom * denom)); + return attenuation; +} + SphericalHarmonics getLightAmbientSphere(Light l) { return l._ambientSphere; } diff --git a/libraries/model/src/model/Stage.cpp b/libraries/model/src/model/Stage.cpp index 0ff613bd4b..6a1e95936b 100644 --- a/libraries/model/src/model/Stage.cpp +++ b/libraries/model/src/model/Stage.cpp @@ -202,14 +202,13 @@ void SunSkyStage::setSunModelEnable(bool isEnabled) { invalidate(); } -void SunSkyStage::setSunColor(const Vec3& color) { - _sunLight->setColor(color); -} -void SunSkyStage::setSunIntensity(float intensity) { - _sunLight->setIntensity(intensity); -} -void SunSkyStage::setSunAmbientIntensity(float intensity) { - _sunLight->setAmbientIntensity(intensity); +void SunSkyStage::setSunAmbientSphere(const gpu::SHPointer& sphere) { + if (sphere) { + _sunLight->setAmbientSphere(*sphere); + } else { + const gpu::SphericalHarmonics::Preset DEFAULT_AMBIENT_SPHERE = gpu::SphericalHarmonics::OLD_TOWN_SQUARE; + _sunLight->setAmbientSpherePreset(DEFAULT_AMBIENT_SPHERE); + } } void SunSkyStage::setSunDirection(const Vec3& direction) { diff --git a/libraries/model/src/model/Stage.h b/libraries/model/src/model/Stage.h index 912a7b77a8..31772d5e48 100644 --- a/libraries/model/src/model/Stage.h +++ b/libraries/model/src/model/Stage.h @@ -11,7 +11,7 @@ #ifndef hifi_model_Stage_h #define hifi_model_Stage_h -#include "gpu/Pipeline.h" +#include #include "Light.h" #include "Skybox.h" @@ -143,12 +143,13 @@ public: bool isSunModelEnabled() const { return _sunModelEnable; } // Sun properties - void setSunColor(const Vec3& color); + void setSunColor(const Vec3& color) { _sunLight->setColor(color); } const Vec3& getSunColor() const { return getSunLight()->getColor(); } - void setSunIntensity(float intensity); + void setSunIntensity(float intensity) { _sunLight->setIntensity(intensity); } float getSunIntensity() const { return getSunLight()->getIntensity(); } - void setSunAmbientIntensity(float intensity); + void setSunAmbientIntensity(float intensity) { _sunLight->setAmbientIntensity(intensity); } float getSunAmbientIntensity() const { return getSunLight()->getAmbientIntensity(); } + void setSunAmbientSphere(const gpu::SHPointer& sphere); // The sun direction is expressed in the world space void setSunDirection(const Vec3& direction); diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 4ded2216d0..d0838c4a8f 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -12,10 +12,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -60,13 +62,12 @@ JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, co updateReciever(updateReceiver), updateSlot(updateSlot) { + } AccountManager::AccountManager() : _authURL(), - _pendingCallbackMap(), - _accountInfo(), - _shouldPersistToSettingsFile(true) + _pendingCallbackMap() { qRegisterMetaType("OAuthAccessToken"); qRegisterMetaTypeStreamOperators("OAuthAccessToken"); @@ -80,9 +81,6 @@ AccountManager::AccountManager() : qRegisterMetaType("QHttpMultiPart*"); connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged); - - // once we have a profile in account manager make sure we generate a new keypair - connect(this, &AccountManager::profileChanged, this, &AccountManager::generateNewKeypair); } const QString DOUBLE_SLASH_SUBSTITUTE = "slashslash"; @@ -93,16 +91,9 @@ void AccountManager::logout() { emit balanceChanged(0); connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged); - - if (_shouldPersistToSettingsFile) { - QString keyURLString(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE)); - QStringList path = QStringList() << ACCOUNTS_GROUP << keyURLString; - Setting::Handle(path).remove(); - - qCDebug(networking) << "Removed account info for" << _authURL << "from in-memory accounts and .ini file"; - } else { - qCDebug(networking) << "Cleared data server account info in account manager."; - } + + // remove this account from the account settings file + removeAccountFromFile(); emit logoutComplete(); // the username has changed to blank @@ -124,35 +115,83 @@ void AccountManager::accountInfoBalanceChanged(qint64 newBalance) { emit balanceChanged(newBalance); } +QString accountFilePath() { + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/AccountInfo.bin"; +} + +QVariantMap accountMapFromFile(bool& success) { + QFile accountFile { accountFilePath() }; + + if (accountFile.open(QIODevice::ReadOnly)) { + // grab the current QVariantMap from the settings file + QDataStream readStream(&accountFile); + QVariantMap accountMap; + + readStream >> accountMap; + + // close the file now that we have read the data + accountFile.close(); + + success = true; + + return accountMap; + } else { + // failed to open file, return empty QVariantMap + // there was only an error if the account file existed when we tried to load it + success = !accountFile.exists(); + + return QVariantMap(); + } +} + void AccountManager::setAuthURL(const QUrl& authURL) { if (_authURL != authURL) { _authURL = authURL; qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString()); - if (_shouldPersistToSettingsFile) { + // check if there are existing access tokens to load from settings + QFile accountsFile { accountFilePath() }; + bool loadedMap = false; + auto accountsMap = accountMapFromFile(loadedMap); + + if (accountsFile.exists() && loadedMap) { + // pull out the stored account info and store it in memory + _accountInfo = accountsMap[_authURL.toString()].value(); + + qCDebug(networking) << "Found metaverse API account information for" << qPrintable(_authURL.toString()); + } else { + // we didn't have a file - see if we can migrate old settings and store them in the new file + // check if there are existing access tokens to load from settings Settings settings; settings.beginGroup(ACCOUNTS_GROUP); - + foreach(const QString& key, settings.allKeys()) { // take a key copy to perform the double slash replacement QString keyCopy(key); QUrl keyURL(keyCopy.replace(DOUBLE_SLASH_SUBSTITUTE, "//")); - + if (keyURL == _authURL) { // pull out the stored access token and store it in memory _accountInfo = settings.value(key).value(); - qCDebug(networking) << "Found a data-server access token for" << qPrintable(keyURL.toString()); - - // profile info isn't guaranteed to be saved too - if (_accountInfo.hasProfile()) { - emit profileChanged(); - } else { - requestProfile(); - } + + qCDebug(networking) << "Migrated an access token for" << qPrintable(keyURL.toString()) + << "from previous settings file"; } } + + if (_accountInfo.getAccessToken().token.isEmpty()) { + qCWarning(networking) << "Unable to load account file. No existing account settings will be loaded."; + } else { + // persist the migrated settings to file + persistAccountToFile(); + } + } + + if (_isAgent && !_accountInfo.getAccessToken().token.isEmpty() && !_accountInfo.hasProfile()) { + // we are missing profile information, request it now + requestProfile(); } // tell listeners that the auth endpoint has changed @@ -299,9 +338,11 @@ void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) { } else { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { - qCDebug(networking) << "Received JSON response from data-server that has no matching callback."; + qCDebug(networking) << "Received JSON response from metaverse API that has no matching callback."; qCDebug(networking) << QJsonDocument::fromJson(requestReply->readAll()); } + + requestReply->deleteLater(); } } @@ -317,22 +358,69 @@ void AccountManager::passErrorToCallback(QNetworkReply* requestReply) { _pendingCallbackMap.remove(requestReply); } else { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { - qCDebug(networking) << "Received error response from data-server that has no matching callback."; + qCDebug(networking) << "Received error response from metaverse API that has no matching callback."; qCDebug(networking) << "Error" << requestReply->error() << "-" << requestReply->errorString(); qCDebug(networking) << requestReply->readAll(); } + + requestReply->deleteLater(); } } -void AccountManager::persistAccountToSettings() { - if (_shouldPersistToSettingsFile) { - // store this access token into the local settings - QString keyURLString(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE)); - QStringList path = QStringList() << ACCOUNTS_GROUP << keyURLString; - Setting::Handle(path).set(QVariant::fromValue(_accountInfo)); +bool writeAccountMapToFile(const QVariantMap& accountMap) { + // re-open the file and truncate it + QFile accountFile { accountFilePath() }; + if (accountFile.open(QIODevice::WriteOnly)) { + QDataStream writeStream(&accountFile); + + // persist the updated account QVariantMap to file + writeStream << accountMap; + + // close the file with the newly persisted settings + accountFile.close(); + + return true; + } else { + return false; } } +void AccountManager::persistAccountToFile() { + + qCDebug(networking) << "Persisting AccountManager accounts to" << accountFilePath(); + + bool wasLoaded = false; + auto accountMap = accountMapFromFile(wasLoaded); + + if (wasLoaded) { + // replace the current account information for this auth URL in the account map + accountMap[_authURL.toString()] = QVariant::fromValue(_accountInfo); + + // re-open the file and truncate it + if (writeAccountMapToFile(accountMap)) { + return; + } + } + + qCWarning(networking) << "Could not load accounts file - unable to persist account information to file."; +} + +void AccountManager::removeAccountFromFile() { + bool wasLoaded = false; + auto accountMap = accountMapFromFile(wasLoaded); + + if (wasLoaded) { + accountMap.remove(_authURL.toString()); + if (writeAccountMapToFile(accountMap)) { + qCDebug(networking) << "Removed account info for" << _authURL << "from settings file."; + return; + } + } + + qCWarning(networking) << "Count not load accounts file - unable to remove account information for" << _authURL + << "from settings file."; +} + bool AccountManager::hasValidAccessToken() { if (_accountInfo.getAccessToken().token.isEmpty() || _accountInfo.getAccessToken().isExpired()) { @@ -359,16 +447,19 @@ bool AccountManager::checkAndSignalForAccessToken() { } void AccountManager::setAccessTokenForCurrentAuthURL(const QString& accessToken) { - // clear our current DataServerAccountInfo - _accountInfo = DataServerAccountInfo(); - - // start the new account info with a new OAuthAccessToken + // replace the account info access token with a new OAuthAccessToken OAuthAccessToken newOAuthToken; newOAuthToken.token = accessToken; - - qCDebug(networking) << "Setting new account manager access token. F2C:" << accessToken.left(2) << "L2C:" << accessToken.right(2); + + if (!accessToken.isEmpty()) { + qCDebug(networking) << "Setting new AccountManager OAuth token. F2C:" << accessToken.left(2) << "L2C:" << accessToken.right(2); + } else if (!_accountInfo.getAccessToken().token.isEmpty()) { + qCDebug(networking) << "Clearing AccountManager OAuth token."; + } _accountInfo.setAccessToken(newOAuthToken); + + persistAccountToFile(); } void AccountManager::requestAccessToken(const QString& login, const QString& password) { @@ -423,7 +514,7 @@ void AccountManager::requestAccessTokenFinished() { emit loginComplete(rootURL); - persistAccountToSettings(); + persistAccountToFile(); requestProfile(); } @@ -469,7 +560,7 @@ void AccountManager::requestProfileFinished() { emit usernameChanged(_accountInfo.getUsername()); // store the whole profile into the local settings - persistAccountToSettings(); + persistAccountToFile(); } else { // TODO: error handling @@ -482,57 +573,141 @@ void AccountManager::requestProfileError(QNetworkReply::NetworkError error) { qCDebug(networking) << "AccountManager requestProfileError - " << error; } -void AccountManager::generateNewKeypair() { - // setup a new QThread to generate the keypair on, in case it takes a while - QThread* generateThread = new QThread(this); - generateThread->setObjectName("Account Manager Generator Thread"); - - // setup a keypair generator - RSAKeypairGenerator* keypairGenerator = new RSAKeypairGenerator(); - - connect(generateThread, &QThread::started, keypairGenerator, &RSAKeypairGenerator::generateKeypair); - connect(keypairGenerator, &RSAKeypairGenerator::generatedKeypair, this, &AccountManager::processGeneratedKeypair); - connect(keypairGenerator, &RSAKeypairGenerator::errorGeneratingKeypair, - this, &AccountManager::handleKeypairGenerationError); - connect(keypairGenerator, &QObject::destroyed, generateThread, &QThread::quit); - connect(generateThread, &QThread::finished, generateThread, &QThread::deleteLater); - - keypairGenerator->moveToThread(generateThread); - - qCDebug(networking) << "Starting worker thread to generate 2048-bit RSA key-pair."; - generateThread->start(); +void AccountManager::generateNewKeypair(bool isUserKeypair, const QUuid& domainID) { + + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "generateNewKeypair", Q_ARG(bool, isUserKeypair), Q_ARG(QUuid, domainID)); + return; + } + + if (!isUserKeypair && domainID.isNull()) { + qCWarning(networking) << "AccountManager::generateNewKeypair called for domain keypair with no domain ID. Will not generate keypair."; + return; + } + + // make sure we don't already have an outbound keypair generation request + if (!_isWaitingForKeypairResponse) { + _isWaitingForKeypairResponse = true; + + // clear the current private key + qDebug() << "Clearing current private key in DataServerAccountInfo"; + _accountInfo.setPrivateKey(QByteArray()); + + // setup a new QThread to generate the keypair on, in case it takes a while + QThread* generateThread = new QThread(this); + generateThread->setObjectName("Account Manager Generator Thread"); + + // setup a keypair generator + RSAKeypairGenerator* keypairGenerator = new RSAKeypairGenerator; + + if (!isUserKeypair) { + keypairGenerator->setDomainID(domainID); + _accountInfo.setDomainID(domainID); + } + + // start keypair generation when the thread starts + connect(generateThread, &QThread::started, keypairGenerator, &RSAKeypairGenerator::generateKeypair); + + // handle success or failure of keypair generation + connect(keypairGenerator, &RSAKeypairGenerator::generatedKeypair, this, &AccountManager::processGeneratedKeypair); + connect(keypairGenerator, &RSAKeypairGenerator::errorGeneratingKeypair, + this, &AccountManager::handleKeypairGenerationError); + + connect(keypairGenerator, &QObject::destroyed, generateThread, &QThread::quit); + connect(generateThread, &QThread::finished, generateThread, &QThread::deleteLater); + + keypairGenerator->moveToThread(generateThread); + + qCDebug(networking) << "Starting worker thread to generate 2048-bit RSA keypair."; + generateThread->start(); + } } -void AccountManager::processGeneratedKeypair(const QByteArray& publicKey, const QByteArray& privateKey) { +void AccountManager::processGeneratedKeypair() { - qCDebug(networking) << "Generated 2048-bit RSA key-pair. Storing private key and uploading public key."; - - // set the private key on our data-server account info - _accountInfo.setPrivateKey(privateKey); - persistAccountToSettings(); - - // upload the public key so data-web has an up-to-date key - const QString PUBLIC_KEY_UPDATE_PATH = "api/v1/user/public_key"; - - // setup a multipart upload to send up the public key - QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - - QHttpPart keyPart; - keyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - keyPart.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"public_key\"; filename=\"public_key\"")); - keyPart.setBody(publicKey); - - requestMultiPart->append(keyPart); - - sendRequest(PUBLIC_KEY_UPDATE_PATH, AccountManagerAuth::Required, QNetworkAccessManager::PutOperation, - JSONCallbackParameters(), QByteArray(), requestMultiPart); - - // get rid of the keypair generator now that we don't need it anymore - sender()->deleteLater(); + qCDebug(networking) << "Generated 2048-bit RSA keypair. Uploading public key now."; + + RSAKeypairGenerator* keypairGenerator = qobject_cast(sender()); + + if (keypairGenerator) { + // hold the private key to later set our metaverse API account info if upload succeeds + _pendingPrivateKey = keypairGenerator->getPrivateKey(); + + // upload the public key so data-web has an up-to-date key + const QString USER_PUBLIC_KEY_UPDATE_PATH = "api/v1/user/public_key"; + const QString DOMAIN_PUBLIC_KEY_UPDATE_PATH = "api/v1/domains/%1/public_key"; + + QString uploadPath; + if (keypairGenerator->getDomainID().isNull()) { + uploadPath = USER_PUBLIC_KEY_UPDATE_PATH; + } else { + uploadPath = DOMAIN_PUBLIC_KEY_UPDATE_PATH.arg(uuidStringWithoutCurlyBraces(keypairGenerator->getDomainID())); + } + + // setup a multipart upload to send up the public key + QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + QHttpPart keyPart; + keyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); + keyPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"public_key\"; filename=\"public_key\"")); + keyPart.setBody(keypairGenerator->getPublicKey()); + + requestMultiPart->append(keyPart); + + // setup callback parameters so we know once the keypair upload has succeeded or failed + JSONCallbackParameters callbackParameters; + callbackParameters.jsonCallbackReceiver = this; + callbackParameters.jsonCallbackMethod = "publicKeyUploadSucceeded"; + callbackParameters.errorCallbackReceiver = this; + callbackParameters.errorCallbackMethod = "publicKeyUploadFailed"; + + sendRequest(uploadPath, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, + callbackParameters, QByteArray(), requestMultiPart); + + keypairGenerator->deleteLater(); + } else { + qCWarning(networking) << "Expected processGeneratedKeypair to be called by a live RSAKeypairGenerator" + << "but the casted sender is NULL. Will not process generated keypair."; + } +} + +void AccountManager::publicKeyUploadSucceeded(QNetworkReply& reply) { + qDebug() << "Uploaded public key to Metaverse API. RSA keypair generation is completed."; + + // public key upload complete - store the matching private key and persist the account to settings + _accountInfo.setPrivateKey(_pendingPrivateKey); + _pendingPrivateKey.clear(); + persistAccountToFile(); + + // clear our waiting state + _isWaitingForKeypairResponse = false; + + emit newKeypair(); + + // delete the reply object now that we are done with it + reply.deleteLater(); +} + +void AccountManager::publicKeyUploadFailed(QNetworkReply& reply) { + // the public key upload has failed + qWarning() << "Public key upload failed from AccountManager" << reply.errorString(); + + // we aren't waiting for a response any longer + _isWaitingForKeypairResponse = false; + + // clear our pending private key + _pendingPrivateKey.clear(); + + // delete the reply object now that we are done with it + reply.deleteLater(); } void AccountManager::handleKeypairGenerationError() { - // for now there isn't anything we do with this except get the worker thread to clean up + qCritical() << "Error generating keypair - this is likely to cause authentication issues."; + + // reset our waiting state for keypair response + _isWaitingForKeypairResponse = false; + sender()->deleteLater(); } diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 719279b0cf..0ebefafbed 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -62,12 +62,12 @@ public: QHttpMultiPart* dataMultiPart = NULL, const QVariantMap& propertyMap = QVariantMap()); + void setIsAgent(bool isAgent) { _isAgent = isAgent; } + const QUrl& getAuthURL() const { return _authURL; } void setAuthURL(const QUrl& authURL); bool hasAuthEndpoint() { return !_authURL.isEmpty(); } - void disableSettingsFilePersistence() { _shouldPersistToSettingsFile = false; } - bool isLoggedIn() { return !_authURL.isEmpty() && hasValidAccessToken(); } bool hasValidAccessToken(); Q_INVOKABLE bool checkAndSignalForAccessToken(); @@ -87,7 +87,9 @@ public slots: void logout(); void updateBalance(); void accountInfoBalanceChanged(qint64 newBalance); - void generateNewKeypair(); + void generateNewUserKeypair() { generateNewKeypair(); } + void generateNewDomainKeypair(const QUuid& domainID) { generateNewKeypair(false, domainID); } + signals: void authRequired(); void authEndpointChanged(); @@ -97,25 +99,36 @@ signals: void loginFailed(); void logoutComplete(); void balanceChanged(qint64 newBalance); + void newKeypair(); + private slots: void processReply(); void handleKeypairGenerationError(); - void processGeneratedKeypair(const QByteArray& publicKey, const QByteArray& privateKey); + void processGeneratedKeypair(); + void publicKeyUploadSucceeded(QNetworkReply& reply); + void publicKeyUploadFailed(QNetworkReply& reply); + void generateNewKeypair(bool isUserKeypair = true, const QUuid& domainID = QUuid()); + private: AccountManager(); - AccountManager(AccountManager const& other); // not implemented - void operator=(AccountManager const& other); // not implemented + AccountManager(AccountManager const& other) = delete; + void operator=(AccountManager const& other) = delete; - void persistAccountToSettings(); + void persistAccountToFile(); + void removeAccountFromFile(); void passSuccessToCallback(QNetworkReply* reply); void passErrorToCallback(QNetworkReply* reply); QUrl _authURL; + QMap _pendingCallbackMap; DataServerAccountInfo _accountInfo; - bool _shouldPersistToSettingsFile; + bool _isAgent { false }; + + bool _isWaitingForKeypairResponse { false }; + QByteArray _pendingPrivateKey; }; #endif // hifi_AccountManager_h diff --git a/libraries/networking/src/DataServerAccountInfo.cpp b/libraries/networking/src/DataServerAccountInfo.cpp index 5d633a8df1..9455fb1b88 100644 --- a/libraries/networking/src/DataServerAccountInfo.cpp +++ b/libraries/networking/src/DataServerAccountInfo.cpp @@ -25,19 +25,6 @@ #pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif -DataServerAccountInfo::DataServerAccountInfo() : - _accessToken(), - _username(), - _xmppPassword(), - _discourseApiKey(), - _walletID(), - _balance(0), - _hasBalance(false), - _privateKey() -{ - -} - DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherInfo) : QObject() { _accessToken = otherInfo._accessToken; _username = otherInfo._username; @@ -47,6 +34,7 @@ DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherI _balance = otherInfo._balance; _hasBalance = otherInfo._hasBalance; _privateKey = otherInfo._privateKey; + _domainID = otherInfo._domainID; } DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountInfo& otherInfo) { @@ -66,6 +54,7 @@ void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) { swap(_balance, otherInfo._balance); swap(_hasBalance, otherInfo._hasBalance); swap(_privateKey, otherInfo._privateKey); + swap(_domainID, otherInfo._domainID); } void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject) { @@ -128,59 +117,62 @@ void DataServerAccountInfo::setProfileInfoFromJSON(const QJsonObject& jsonObject } QByteArray DataServerAccountInfo::getUsernameSignature(const QUuid& connectionToken) { - - if (!_privateKey.isEmpty()) { - const char* privateKeyData = _privateKey.constData(); - RSA* rsaPrivateKey = d2i_RSAPrivateKey(NULL, - reinterpret_cast(&privateKeyData), - _privateKey.size()); - if (rsaPrivateKey) { - QByteArray lowercaseUsername = _username.toLower().toUtf8(); - QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()), - QCryptographicHash::Sha256); - - QByteArray usernameSignature(RSA_size(rsaPrivateKey), 0); - unsigned int usernameSignatureSize = 0; - - int encryptReturn = RSA_sign(NID_sha256, - reinterpret_cast(usernameWithToken.constData()), - usernameWithToken.size(), - reinterpret_cast(usernameSignature.data()), - &usernameSignatureSize, - rsaPrivateKey); - - // free the private key RSA struct now that we are done with it - RSA_free(rsaPrivateKey); + auto lowercaseUsername = _username.toLower().toUtf8(); + auto plaintext = lowercaseUsername.append(connectionToken.toRfc4122()); - if (encryptReturn == -1) { - qCDebug(networking) << "Error encrypting username signature."; - qCDebug(networking) << "Will re-attempt on next domain-server check in."; - } else { - qDebug(networking) << "Returning username" << _username << "signed with connection UUID" << uuidStringWithoutCurlyBraces(connectionToken); - return usernameSignature; - } - - } else { - qCDebug(networking) << "Could not create RSA struct from QByteArray private key."; - qCDebug(networking) << "Will re-attempt on next domain-server check in."; - } - } - return QByteArray(); + auto signature = signPlaintext(plaintext); + if (!signature.isEmpty()) { + qDebug(networking) << "Returning username" << _username + << "signed with connection UUID" << uuidStringWithoutCurlyBraces(connectionToken); + } else { + qCDebug(networking) << "Error signing username with connection token"; + qCDebug(networking) << "Will re-attempt on next domain-server check in."; + } + + return signature; } -void DataServerAccountInfo::setPrivateKey(const QByteArray& privateKey) { - _privateKey = privateKey; - +QByteArray DataServerAccountInfo::signPlaintext(const QByteArray& plaintext) { + if (!_privateKey.isEmpty()) { + const char* privateKeyData = _privateKey.constData(); + RSA* rsaPrivateKey = d2i_RSAPrivateKey(NULL, + reinterpret_cast(&privateKeyData), + _privateKey.size()); + if (rsaPrivateKey) { + QByteArray signature(RSA_size(rsaPrivateKey), 0); + unsigned int signatureBytes = 0; + + QByteArray hashedPlaintext = QCryptographicHash::hash(plaintext, QCryptographicHash::Sha256); + + int encryptReturn = RSA_sign(NID_sha256, + reinterpret_cast(hashedPlaintext.constData()), + hashedPlaintext.size(), + reinterpret_cast(signature.data()), + &signatureBytes, + rsaPrivateKey); + + // free the private key RSA struct now that we are done with it + RSA_free(rsaPrivateKey); + + if (encryptReturn != -1) { + return signature; + } + } else { + qCDebug(networking) << "Could not create RSA struct from QByteArray private key."; + } + } + return QByteArray(); } QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) { out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey - << info._walletID << info._privateKey; + << info._walletID << info._privateKey << info._domainID; + return out; } QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info) { in >> info._accessToken >> info._username >> info._xmppPassword >> info._discourseApiKey - >> info._walletID >> info._privateKey; + >> info._walletID >> info._privateKey >> info._domainID; return in; } diff --git a/libraries/networking/src/DataServerAccountInfo.h b/libraries/networking/src/DataServerAccountInfo.h index 9b80de5422..6223bc008e 100644 --- a/libraries/networking/src/DataServerAccountInfo.h +++ b/libraries/networking/src/DataServerAccountInfo.h @@ -23,7 +23,7 @@ const float SATOSHIS_PER_CREDIT = 100000000.0f; class DataServerAccountInfo : public QObject { Q_OBJECT public: - DataServerAccountInfo(); + DataServerAccountInfo() {}; DataServerAccountInfo(const DataServerAccountInfo& otherInfo); DataServerAccountInfo& operator=(const DataServerAccountInfo& otherInfo); @@ -42,10 +42,6 @@ public: const QUuid& getWalletID() const { return _walletID; } void setWalletID(const QUuid& walletID); - - QByteArray getUsernameSignature(const QUuid& connectionToken); - bool hasPrivateKey() const { return !_privateKey.isEmpty(); } - void setPrivateKey(const QByteArray& privateKey); qint64 getBalance() const { return _balance; } float getBalanceInSatoshis() const { return _balance / SATOSHIS_PER_CREDIT; } @@ -54,6 +50,15 @@ public: void setHasBalance(bool hasBalance) { _hasBalance = hasBalance; } Q_INVOKABLE void setBalanceFromJSON(QNetworkReply& requestReply); + QByteArray getUsernameSignature(const QUuid& connectionToken); + bool hasPrivateKey() const { return !_privateKey.isEmpty(); } + void setPrivateKey(const QByteArray& privateKey) { _privateKey = privateKey; } + + QByteArray signPlaintext(const QByteArray& plaintext); + + void setDomainID(const QUuid& domainID) { _domainID = domainID; } + const QUuid& getDomainID() const { return _domainID; } + bool hasProfile() const; void setProfileInfoFromJSON(const QJsonObject& jsonObject); @@ -70,8 +75,9 @@ private: QString _xmppPassword; QString _discourseApiKey; QUuid _walletID; - qint64 _balance; - bool _hasBalance; + qint64 _balance { 0 }; + bool _hasBalance { false }; + QUuid _domainID; // if this holds account info for a domain, this holds the ID of that domain QByteArray _privateKey; }; diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index db775983e1..34ca722537 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -92,7 +92,9 @@ void DomainHandler::softReset() { disconnect(); clearSettings(); - + + _connectionDenialsSinceKeypairRegen = 0; + // cancel the failure timeout for any pending requests for settings QMetaObject::invokeMethod(&_settingsTimer, "stop"); } @@ -106,6 +108,9 @@ void DomainHandler::hardReset() { _hostname = QString(); _sockAddr.clear(); + _hasCheckedForAccessToken = false; + _domainConnectionRefusals.clear(); + // clear any pending path we may have wanted to ask the previous DS about _pendingPath.clear(); } @@ -347,3 +352,35 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes emit icePeerSocketsReceived(); } } + +void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer message) { + // Read deny reason from packet + quint16 reasonSize; + message->readPrimitive(&reasonSize); + QString reason = QString::fromUtf8(message->readWithoutCopy(reasonSize)); + + // output to the log so the user knows they got a denied connection request + // and check and signal for an access token so that we can make sure they are logged in + qCWarning(networking) << "The domain-server denied a connection request: " << reason; + qCWarning(networking) << "Make sure you are logged in."; + + if (!_domainConnectionRefusals.contains(reason)) { + _domainConnectionRefusals.append(reason); + emit domainConnectionRefused(reason); + } + + auto& accountManager = AccountManager::getInstance(); + + if (!_hasCheckedForAccessToken) { + accountManager.checkAndSignalForAccessToken(); + _hasCheckedForAccessToken = true; + } + + static const int CONNECTION_DENIALS_FOR_KEYPAIR_REGEN = 3; + + // force a re-generation of key-pair after CONNECTION_DENIALS_FOR_KEYPAIR_REGEN failed connection attempts + if (++_connectionDenialsSinceKeypairRegen >= CONNECTION_DENIALS_FOR_KEYPAIR_REGEN) { + accountManager.generateNewUserKeypair(); + _connectionDenialsSinceKeypairRegen = 0; + } +} diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index f60ac2fbe6..b245305a93 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -92,6 +92,7 @@ public slots: void processICEPingReplyPacket(QSharedPointer message); void processDTLSRequirementPacket(QSharedPointer dtlsRequirementPacket); void processICEResponsePacket(QSharedPointer icePacket); + void processDomainServerConnectionDeniedPacket(QSharedPointer message); private slots: void completedHostnameLookup(const QHostInfo& hostInfo); @@ -113,6 +114,8 @@ signals: void settingsReceived(const QJsonObject& domainSettingsObject); void settingsReceiveFail(); + void domainConnectionRefused(QString reason); + private: void sendDisconnectPacket(); void hardReset(); @@ -130,6 +133,10 @@ private: QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; + + QStringList _domainConnectionRefusals; + bool _hasCheckedForAccessToken { false }; + int _connectionDenialsSinceKeypairRegen { 0 }; }; #endif // hifi_DomainHandler_h diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a3707d19ba..f236f9d596 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -902,10 +902,6 @@ void LimitedNodeList::updateLocalSockAddr() { } } -void LimitedNodeList::sendHeartbeatToIceServer(const HifiSockAddr& iceServerSockAddr) { - sendPacketToIceServer(PacketType::ICEServerHeartbeat, iceServerSockAddr, _sessionUUID); -} - void LimitedNodeList::sendPeerQueryToIceServer(const HifiSockAddr& iceServerSockAddr, const QUuid& clientID, const QUuid& peerID) { sendPacketToIceServer(PacketType::ICEServerQuery, iceServerSockAddr, clientID, peerID); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index fcad23da8f..de110c7e7f 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -143,6 +143,7 @@ public: bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; } const HifiSockAddr& getLocalSockAddr() const { return _localSockAddr; } + const HifiSockAddr& getPublicSockAddr() const { return _publicSockAddr; } const HifiSockAddr& getSTUNSockAddr() const { return _stunSockAddr; } void processKillNode(ReceivedMessage& message); @@ -161,7 +162,6 @@ public: std::unique_ptr constructICEPingPacket(PingType_t pingType, const QUuid& iceID); std::unique_ptr constructICEPingReplyPacket(ReceivedMessage& message, const QUuid& iceID); - void sendHeartbeatToIceServer(const HifiSockAddr& iceServerSockAddr); void sendPeerQueryToIceServer(const HifiSockAddr& iceServerSockAddr, const QUuid& clientID, const QUuid& peerID); SharedNodePointer findNodeWithAddr(const HifiSockAddr& addr); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 677a1ad1e6..02bb17e870 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -80,11 +80,16 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // send a ping punch immediately connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer); + auto &accountManager = AccountManager::getInstance(); + + // assume that we may need to send a new DS check in anytime a new keypair is generated + connect(&accountManager, &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn); + // clear out NodeList when login is finished - connect(&AccountManager::getInstance(), &AccountManager::loginComplete , this, &NodeList::reset); + connect(&accountManager, &AccountManager::loginComplete , this, &NodeList::reset); // clear our NodeList when logout is requested - connect(&AccountManager::getInstance(), &AccountManager::logoutComplete , this, &NodeList::reset); + connect(&accountManager, &AccountManager::logoutComplete , this, &NodeList::reset); // anytime we get a new node we will want to attempt to punch to it connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); @@ -105,6 +110,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned packetReceiver.registerListener(PacketType::ICEPing, this, "processICEPingPacket"); packetReceiver.registerListener(PacketType::DomainServerAddedNode, this, "processDomainServerAddedNode"); packetReceiver.registerListener(PacketType::DomainServerConnectionToken, this, "processDomainServerConnectionTokenPacket"); + packetReceiver.registerListener(PacketType::DomainConnectionDenied, &_domainHandler, "processDomainServerConnectionDeniedPacket"); packetReceiver.registerListener(PacketType::DomainSettings, &_domainHandler, "processSettingsPacketList"); packetReceiver.registerListener(PacketType::ICEServerPeerInformation, &_domainHandler, "processICEResponsePacket"); packetReceiver.registerListener(PacketType::DomainServerRequireDTLS, &_domainHandler, "processDTLSRequirementPacket"); @@ -265,6 +271,26 @@ void NodeList::sendDomainServerCheckIn() { } + // check if we're missing a keypair we need to verify ourselves with the domain-server + auto& accountManager = AccountManager::getInstance(); + const QUuid& connectionToken = _domainHandler.getConnectionToken(); + + // we assume that we're on the same box as the DS if it has the same local address and + // it didn't present us with a connection token to use for username signature + bool localhostDomain = _domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost + || (_domainHandler.getSockAddr().getAddress() == _localSockAddr.getAddress() && connectionToken.isNull()); + + bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull() && !localhostDomain; + + if (requiresUsernameSignature && !accountManager.getAccountInfo().hasPrivateKey()) { + qWarning() << "A keypair is required to present a username signature to the domain-server" + << "but no keypair is present. Waiting for keypair generation to complete."; + accountManager.generateNewUserKeypair(); + + // don't send the check in packet - wait for the keypair first + return; + } + auto domainPacket = NLPacket::create(domainPacketType); QDataStream packetStream(domainPacket.get()); @@ -289,23 +315,15 @@ void NodeList::sendDomainServerCheckIn() { // pack our data to send to the domain-server packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); - - // if this is a connect request, and we can present a username signature, send it along - if (!_domainHandler.isConnected() ) { - - DataServerAccountInfo& accountInfo = AccountManager::getInstance().getAccountInfo(); + + if (!_domainHandler.isConnected()) { + DataServerAccountInfo& accountInfo = accountManager.getAccountInfo(); packetStream << accountInfo.getUsername(); - - // get connection token from the domain-server - const QUuid& connectionToken = _domainHandler.getConnectionToken(); - - if (!connectionToken.isNull()) { - - const QByteArray& usernameSignature = AccountManager::getInstance().getAccountInfo().getUsernameSignature(connectionToken); - - if (!usernameSignature.isEmpty()) { - packetStream << usernameSignature; - } + + // if this is a connect request, and we can present a username signature, send it along + if (requiresUsernameSignature && accountManager.getAccountInfo().hasPrivateKey()) { + const QByteArray& usernameSignature = accountManager.getAccountInfo().getUsernameSignature(connectionToken); + packetStream << usernameSignature; } } diff --git a/libraries/networking/src/RSAKeypairGenerator.cpp b/libraries/networking/src/RSAKeypairGenerator.cpp index 53b9b27cc6..a98cf74564 100644 --- a/libraries/networking/src/RSAKeypairGenerator.cpp +++ b/libraries/networking/src/RSAKeypairGenerator.cpp @@ -85,12 +85,12 @@ void RSAKeypairGenerator::generateKeypair() { // we can cleanup the RSA struct before we continue on RSA_free(keyPair); - QByteArray publicKeyArray(reinterpret_cast(publicKeyDER), publicKeyLength); - QByteArray privateKeyArray(reinterpret_cast(privateKeyDER), privateKeyLength); + _publicKey = QByteArray { reinterpret_cast(publicKeyDER), publicKeyLength }; + _privateKey = QByteArray { reinterpret_cast(privateKeyDER), privateKeyLength }; // cleanup the publicKeyDER and publicKeyDER data OPENSSL_free(publicKeyDER); OPENSSL_free(privateKeyDER); - emit generatedKeypair(publicKeyArray, privateKeyArray); + emit generatedKeypair(); } diff --git a/libraries/networking/src/RSAKeypairGenerator.h b/libraries/networking/src/RSAKeypairGenerator.h index dd90313625..36f4a9550b 100644 --- a/libraries/networking/src/RSAKeypairGenerator.h +++ b/libraries/networking/src/RSAKeypairGenerator.h @@ -12,17 +12,31 @@ #ifndef hifi_RSAKeypairGenerator_h #define hifi_RSAKeypairGenerator_h -#include +#include +#include class RSAKeypairGenerator : public QObject { Q_OBJECT public: RSAKeypairGenerator(QObject* parent = 0); + + void setDomainID(const QUuid& domainID) { _domainID = domainID; } + const QUuid& getDomainID() const { return _domainID; } + + const QByteArray& getPublicKey() const { return _publicKey; } + const QByteArray& getPrivateKey() const { return _privateKey; } + public slots: void generateKeypair(); + signals: void errorGeneratingKeypair(); - void generatedKeypair(const QByteArray& publicKey, const QByteArray& privateKey); + void generatedKeypair(); + +private: + QUuid _domainID; + QByteArray _publicKey; + QByteArray _privateKey; }; -#endif // hifi_RSAKeypairGenerator_h \ No newline at end of file +#endif // hifi_RSAKeypairGenerator_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 4e6418279d..cbe577c14b 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -30,7 +30,7 @@ const QSet NON_SOURCED_PACKETS = QSet() << PacketType::DomainServerAddedNode << PacketType::DomainServerConnectionToken << PacketType::DomainSettingsRequest << PacketType::DomainSettings << PacketType::ICEServerPeerInformation << PacketType::ICEServerQuery << PacketType::ICEServerHeartbeat - << PacketType::ICEPing << PacketType::ICEPingReply + << PacketType::ICEPing << PacketType::ICEPingReply << PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode << PacketType::DomainServerRemovedNode; @@ -41,10 +41,12 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ATMOSPHERE_REMOVED; + return VERSION_LIGHT_HAS_FALLOFF_RADIUS; case PacketType::AvatarData: case PacketType::BulkAvatarData: return static_cast(AvatarMixerPacketVersion::SoftAttachmentSupport); + case PacketType::ICEServerHeartbeat: + return 18; // ICE Server Heartbeat signing default: return 17; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index af3fd49710..7840ecb17e 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -90,7 +90,8 @@ public: DomainServerRemovedNode, MessagesData, MessagesSubscribe, - MessagesUnsubscribe + MessagesUnsubscribe, + ICEServerHeartbeatDenied }; }; @@ -166,6 +167,7 @@ const PacketVersion VERSION_MODEL_ENTITIES_JOINTS_ON_WIRE = 53; const PacketVersion VERSION_ENTITITES_HAVE_QUERY_BOX = 54; const PacketVersion VERSION_ENTITITES_HAVE_COLLISION_MASK = 55; const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56; +const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index a685c2580c..d78f21d964 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1034,7 +1034,7 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element, // if we are INSIDE, INTERSECT, or OUTSIDE if (parentLocationThisView != ViewFrustum::INSIDE) { assert(parentLocationThisView != ViewFrustum::OUTSIDE); // we shouldn't be here if our parent was OUTSIDE! - nodeLocationThisView = element->inFrustum(*params.viewFrustum); + nodeLocationThisView = element->computeViewLocation(*params.viewFrustum); } // If we're at a element that is out of view, then we can return, because no nodes below us will be in view! @@ -1053,7 +1053,7 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element, bool wasInView = false; if (params.deltaViewFrustum && params.lastViewFrustum) { - ViewFrustum::location location = element->inFrustum(*params.lastViewFrustum); + ViewFrustum::location location = element->computeViewLocation(*params.lastViewFrustum); // If we're a leaf, then either intersect or inside is considered "formerly in view" if (element->isLeaf()) { @@ -1237,7 +1237,7 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element, bool childWasInView = false; if (childElement && params.deltaViewFrustum && params.lastViewFrustum) { - ViewFrustum::location location = childElement->inFrustum(*params.lastViewFrustum); + ViewFrustum::location location = childElement->computeViewLocation(*params.lastViewFrustum); // If we're a leaf, then either intersect or inside is considered "formerly in view" if (childElement->isLeaf()) { diff --git a/libraries/octree/src/OctreeElement.cpp b/libraries/octree/src/OctreeElement.cpp index f16e1dc88d..342a0abf01 100644 --- a/libraries/octree/src/OctreeElement.cpp +++ b/libraries/octree/src/OctreeElement.cpp @@ -458,8 +458,8 @@ float OctreeElement::getEnclosingRadius() const { return getScale() * sqrtf(3.0f) / 2.0f; } -ViewFrustum::location OctreeElement::inFrustum(const ViewFrustum& viewFrustum) const { - return viewFrustum.cubeInFrustum(_cube); +ViewFrustum::location OctreeElement::computeViewLocation(const ViewFrustum& viewFrustum) const { + return viewFrustum.computeCubeViewLocation(_cube); } // There are two types of nodes for which we want to "render" diff --git a/libraries/octree/src/OctreeElement.h b/libraries/octree/src/OctreeElement.h index 3c25ec0850..3f58175337 100644 --- a/libraries/octree/src/OctreeElement.h +++ b/libraries/octree/src/OctreeElement.h @@ -49,20 +49,20 @@ protected: OctreeElement(); virtual OctreeElementPointer createNewElement(unsigned char * octalCode = NULL) = 0; - + public: virtual void init(unsigned char * octalCode); /// Your subclass must call init on construction. virtual ~OctreeElement(); // methods you can and should override to implement your tree functionality - + /// Adds a child to the current element. Override this if there is additional child initialization your class needs. virtual OctreeElementPointer addChildAtIndex(int childIndex); - /// Override this to implement LOD averaging on changes to the tree. + /// Override this to implement LOD averaging on changes to the tree. virtual void calculateAverageFromChildren() { } - /// Override this to implement LOD collapsing and identical child pruning on changes to the tree. + /// Override this to implement LOD collapsing and identical child pruning on changes to the tree. virtual bool collapseChildren() { return false; } /// Should this element be considered to have content in it. This will be used in collision and ray casting methods. @@ -72,12 +72,12 @@ public: /// Should this element be considered to have detailed content in it. Specifically should it be rendered. /// By default we assume that only leaves have detailed content, but some octrees may have different semantics. virtual bool hasDetailedContent() const { return isLeaf(); } - + /// Override this to break up large octree elements when an edit operation is performed on a smaller octree element. - /// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the + /// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the /// meaningful split would be to break the larger cube into smaller cubes of the same color/texture. virtual void splitChildren() { } - + /// Override to indicate that this element requires a split before editing lower elements in the octree virtual bool requiresSplit() const { return false; } @@ -88,17 +88,17 @@ public: virtual void initializeExtraEncodeData(EncodeBitstreamParams& params) { } virtual bool shouldIncludeChildData(int childIndex, EncodeBitstreamParams& params) const { return true; } virtual bool shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { return true; } - + virtual void updateEncodedData(int childIndex, AppendState childAppendState, EncodeBitstreamParams& params) const { } virtual void elementEncodeComplete(EncodeBitstreamParams& params) const { } /// Override to serialize the state of this element. This is used for persistance and for transmission across the network. - virtual AppendState appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const + virtual AppendState appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const { return COMPLETED; } - + /// Override to deserialize the state of this element. This is used for loading from a persisted file or from reading /// from the network. - virtual int readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) + virtual int readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) { return 0; } /// Override to indicate that the item is currently rendered in the rendering engine. By default we assume that if @@ -106,7 +106,7 @@ public: /// where an element is not actually rendering all should render elements. If the isRendered() state doesn't match the /// shouldRender() state, the tree will remark elements as changed even in cases there the elements have not changed. virtual bool isRendered() const { return getShouldRender(); } - + virtual bool deleteApproved() const { return true; } virtual bool canRayIntersect() const { return isLeaf(); } @@ -114,7 +114,7 @@ public: /// \param radius radius of sphere in meters /// \param[out] penetration pointing into cube from sphere /// \param penetratedObject unused - virtual bool findSpherePenetration(const glm::vec3& center, float radius, + virtual bool findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration, void** penetratedObject) const; // Base class methods you don't need to implement @@ -125,7 +125,7 @@ public: bool isParentOf(OctreeElementPointer possibleChild) const; /// handles deletion of all descendants, returns false if delete not approved - bool safeDeepDeleteChildAtIndex(int childIndex, int recursionCount = 0); + bool safeDeepDeleteChildAtIndex(int childIndex, int recursionCount = 0); const AACube& getAACube() const { return _cube; } @@ -134,8 +134,8 @@ public: int getLevel() const { return numberOfThreeBitSectionsInCode(getOctalCode()) + 1; } float getEnclosingRadius() const; - bool isInView(const ViewFrustum& viewFrustum) const { return inFrustum(viewFrustum) != ViewFrustum::OUTSIDE; } - ViewFrustum::location inFrustum(const ViewFrustum& viewFrustum) const; + bool isInView(const ViewFrustum& viewFrustum) const { return computeViewLocation(viewFrustum) != ViewFrustum::OUTSIDE; } + ViewFrustum::location computeViewLocation(const ViewFrustum& viewFrustum) const; float distanceToCamera(const ViewFrustum& viewFrustum) const; float furthestDistanceToCamera(const ViewFrustum& viewFrustum) const; @@ -257,7 +257,7 @@ protected: static std::map _mapSourceUUIDsToKeys; static std::map _mapKeysToSourceUUIDs; - unsigned char _childBitmask; // 1 byte + unsigned char _childBitmask; // 1 byte bool _falseColored : 1, /// Client only, is this voxel false colored, 1 bit _isDirty : 1, /// Client only, has this voxel changed since being rendered, 1 bit diff --git a/libraries/octree/src/OctreeHeadlessViewer.cpp b/libraries/octree/src/OctreeHeadlessViewer.cpp index 547b3ac32b..42bbfb025e 100644 --- a/libraries/octree/src/OctreeHeadlessViewer.cpp +++ b/libraries/octree/src/OctreeHeadlessViewer.cpp @@ -27,9 +27,9 @@ void OctreeHeadlessViewer::init() { void OctreeHeadlessViewer::queryOctree() { char serverType = getMyNodeType(); PacketType packetType = getMyQueryMessageType(); - + NodeToJurisdictionMap& jurisdictions = *_jurisdictionListener->getJurisdictions(); - + bool wantExtraDebugging = false; if (wantExtraDebugging) { @@ -77,7 +77,7 @@ void OctreeHeadlessViewer::queryOctree() { if (jurisdictions.find(nodeUUID) == jurisdictions.end()) { unknownJurisdictionServers++; return; - } + } const JurisdictionMap& map = (jurisdictions)[nodeUUID]; unsigned char* rootCode = map.getRootOctalCode(); @@ -91,7 +91,7 @@ void OctreeHeadlessViewer::queryOctree() { if (foundRootDetails) { AACube serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s); - ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds); + ViewFrustum::location serverFrustumLocation = _viewFrustum.computeCubeViewLocation(serverBounds); if (serverFrustumLocation != ViewFrustum::OUTSIDE) { inViewServers++; @@ -165,7 +165,7 @@ void OctreeHeadlessViewer::queryOctree() { if (foundRootDetails) { AACube serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s); - ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds); + ViewFrustum::location serverFrustumLocation = _viewFrustum.computeCubeViewLocation(serverBounds); if (serverFrustumLocation != ViewFrustum::OUTSIDE) { inView = true; } else { @@ -208,7 +208,7 @@ void OctreeHeadlessViewer::queryOctree() { // setup the query packet auto queryPacket = NLPacket::create(packetType); - + // read the data to our packet and set the payload size to fit the query int querySize = _octreeQuery.getBroadcastData(reinterpret_cast(queryPacket->getPayload())); queryPacket->setPayloadSize(querySize); diff --git a/libraries/octree/src/ViewFrustum.cpp b/libraries/octree/src/ViewFrustum.cpp index 4c07a0c784..4d729179e7 100644 --- a/libraries/octree/src/ViewFrustum.cpp +++ b/libraries/octree/src/ViewFrustum.cpp @@ -241,30 +241,18 @@ ViewFrustum::location ViewFrustum::boxInKeyhole(const AABox& box) const { return result; } -ViewFrustum::location ViewFrustum::pointInFrustum(const glm::vec3& point, bool ignoreKeyhole) const { - ViewFrustum::location regularResult = INSIDE; - ViewFrustum::location keyholeResult = OUTSIDE; - - // If we have a keyholeRadius, check that first, since it's cheaper - if (!ignoreKeyhole && _keyholeRadius >= 0.0f) { - keyholeResult = pointInKeyhole(point); - - if (keyholeResult == INSIDE) { - return keyholeResult; - } - } - - // If we're not known to be INSIDE the keyhole, then check the regular frustum +ViewFrustum::location ViewFrustum::computePointFrustumLocation(const glm::vec3& point) const { + // only checks against frustum, not sphere for(int i = 0; i < 6; ++i) { float distance = _planes[i].distance(point); - if (distance < 0) { - return keyholeResult; // escape early will be the value from checking the keyhole + if (distance < 0.0f) { + return OUTSIDE; } } - return regularResult; + return INSIDE; } -ViewFrustum::location ViewFrustum::sphereInFrustum(const glm::vec3& center, float radius) const { +ViewFrustum::location ViewFrustum::computeSphereViewLocation(const glm::vec3& center, float radius) const { ViewFrustum::location regularResult = INSIDE; ViewFrustum::location keyholeResult = OUTSIDE; @@ -291,7 +279,7 @@ ViewFrustum::location ViewFrustum::sphereInFrustum(const glm::vec3& center, floa } -ViewFrustum::location ViewFrustum::cubeInFrustum(const AACube& cube) const { +ViewFrustum::location ViewFrustum::computeCubeViewLocation(const AACube& cube) const { ViewFrustum::location regularResult = INSIDE; ViewFrustum::location keyholeResult = OUTSIDE; @@ -326,7 +314,7 @@ ViewFrustum::location ViewFrustum::cubeInFrustum(const AACube& cube) const { return regularResult; } -ViewFrustum::location ViewFrustum::boxInFrustum(const AABox& box) const { +ViewFrustum::location ViewFrustum::computeBoxViewLocation(const AABox& box) const { ViewFrustum::location regularResult = INSIDE; ViewFrustum::location keyholeResult = OUTSIDE; @@ -490,7 +478,7 @@ PickRay ViewFrustum::computePickRay(float x, float y) { } void ViewFrustum::computePickRay(float x, float y, glm::vec3& origin, glm::vec3& direction) const { - origin = _cornersWorld[TOP_LEFT_NEAR] + x * (_cornersWorld[TOP_RIGHT_NEAR] - _cornersWorld[TOP_LEFT_NEAR]) + + origin = _cornersWorld[TOP_LEFT_NEAR] + x * (_cornersWorld[TOP_RIGHT_NEAR] - _cornersWorld[TOP_LEFT_NEAR]) + y * (_cornersWorld[BOTTOM_LEFT_NEAR] - _cornersWorld[TOP_LEFT_NEAR]); direction = glm::normalize(origin - _position); } @@ -804,7 +792,7 @@ float ViewFrustum::calculateRenderAccuracy(const AABox& bounds, float octreeSize // FIXME - for now, it's either visible or not visible. We want to adjust this to eventually return // a floating point for objects that have small angular size to indicate that they may be rendered // with lower preciscion - return (distanceToCamera <= visibleDistanceAtClosestScale) ? 1.0f : 0.0f; + return (distanceToCamera <= visibleDistanceAtClosestScale) ? 1.0f : 0.0f; } float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeScale) { diff --git a/libraries/octree/src/ViewFrustum.h b/libraries/octree/src/ViewFrustum.h index 89c632df8c..20609926fb 100644 --- a/libraries/octree/src/ViewFrustum.h +++ b/libraries/octree/src/ViewFrustum.h @@ -91,10 +91,11 @@ public: typedef enum {OUTSIDE, INTERSECT, INSIDE} location; - ViewFrustum::location pointInFrustum(const glm::vec3& point, bool ignoreKeyhole = false) const; - ViewFrustum::location sphereInFrustum(const glm::vec3& center, float radius) const; - ViewFrustum::location cubeInFrustum(const AACube& cube) const; - ViewFrustum::location boxInFrustum(const AABox& box) const; + ViewFrustum::location computePointFrustumLocation(const glm::vec3& point) const; + + ViewFrustum::location computeSphereViewLocation(const glm::vec3& center, float radius) const; + ViewFrustum::location computeCubeViewLocation(const AACube& cube) const; + ViewFrustum::location computeBoxViewLocation(const AABox& box) const; // some frustum comparisons bool matches(const ViewFrustum& compareTo, bool debug = false) const; @@ -114,15 +115,15 @@ public: glm::vec2 projectPoint(glm::vec3 point, bool& pointInView) const; OctreeProjectedPolygon getProjectedPolygon(const AACube& box) const; void getFurthestPointFromCamera(const AACube& box, glm::vec3& furthestPoint) const; - + float distanceToCamera(const glm::vec3& point) const; - + void evalProjectionMatrix(glm::mat4& proj) const; void evalViewTransform(Transform& view) const; /// renderAccuracy represents a floating point "visibility" of an object based on it's view from the camera. At a simple /// level it returns 0.0f for things that are so small for the current settings that they could not be visible. - float calculateRenderAccuracy(const AABox& bounds, float octreeSizeScale = DEFAULT_OCTREE_SIZE_SCALE, + float calculateRenderAccuracy(const AABox& bounds, float octreeSizeScale = DEFAULT_OCTREE_SIZE_SCALE, int boundaryLevelAdjust = 0) const; float getAccuracyAngle(float octreeSizeScale = DEFAULT_OCTREE_SIZE_SCALE, int boundaryLevelAdjust = 0) const; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index e39c3f5de2..e0cbab67bc 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -98,12 +98,12 @@ void DeferredLightingEffect::init() { } void DeferredLightingEffect::addPointLight(const glm::vec3& position, float radius, const glm::vec3& color, - float intensity) { - addSpotLight(position, radius, color, intensity); + float intensity, float falloffRadius) { + addSpotLight(position, radius, color, intensity, falloffRadius); } void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color, - float intensity, const glm::quat& orientation, float exponent, float cutoff) { + float intensity, float falloffRadius, const glm::quat& orientation, float exponent, float cutoff) { unsigned int lightID = (unsigned int)(_pointLights.size() + _spotLights.size() + _globalLights.size()); if (lightID >= _allocatedLights.size()) { @@ -115,7 +115,7 @@ void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radiu lp->setMaximumRadius(radius); lp->setColor(color); lp->setIntensity(intensity); - //lp->setShowContour(quadraticAttenuation); + lp->setFalloffRadius(falloffRadius); if (exponent == 0.0f && cutoff == PI) { lp->setType(model::Light::POINT); @@ -312,15 +312,13 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo // First Global directional light and ambient pass { - bool useSkyboxCubemap = (_skybox) && (_skybox->getCubemap()); - auto& program = _shadowMapEnabled ? _directionalLightShadow : _directionalLight; LightLocationsPtr locations = _shadowMapEnabled ? _directionalLightShadowLocations : _directionalLightLocations; // Setup the global directional pass pipeline { if (_shadowMapEnabled) { - if (useSkyboxCubemap) { + if (_skyboxTexture) { program = _directionalSkyboxLightShadow; locations = _directionalSkyboxLightShadowLocations; } else if (_ambientLightMode > -1) { @@ -328,7 +326,7 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo locations = _directionalAmbientSphereLightShadowLocations; } } else { - if (useSkyboxCubemap) { + if (_skyboxTexture) { program = _directionalSkyboxLight; locations = _directionalSkyboxLightLocations; } else if (_ambientLightMode > -1) { @@ -356,7 +354,7 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo geometryCache->renderQuad(batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, color); } - if (useSkyboxCubemap) { + if (_skyboxTexture) { batch.setResourceTexture(SKYBOX_MAP_UNIT, nullptr); } } @@ -501,9 +499,8 @@ void DeferredLightingEffect::setupKeyLightBatch(gpu::Batch& batch, int lightBuff batch.setUniformBuffer(lightBufferUnit, globalLight->getSchemaBuffer()); } - bool useSkyboxCubemap = (_skybox) && (_skybox->getCubemap()); - if (useSkyboxCubemap && (skyboxCubemapUnit >= 0)) { - batch.setResourceTexture(skyboxCubemapUnit, _skybox->getCubemap()); + if (_skyboxTexture && (skyboxCubemapUnit >= 0)) { + batch.setResourceTexture(skyboxCubemapUnit, _skyboxTexture); } } @@ -562,32 +559,9 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo } -void DeferredLightingEffect::setAmbientLightMode(int preset) { - if ((preset >= 0) && (preset < gpu::SphericalHarmonics::NUM_PRESET)) { - _ambientLightMode = preset; - auto light = _allocatedLights.front(); - light->setAmbientSpherePreset(gpu::SphericalHarmonics::Preset(preset % gpu::SphericalHarmonics::NUM_PRESET)); - } else { - // force to preset 0 - setAmbientLightMode(0); - } -} - -void DeferredLightingEffect::setGlobalLight(const glm::vec3& direction, const glm::vec3& diffuse, float intensity, float ambientIntensity) { - auto light = _allocatedLights.front(); - light->setDirection(direction); - light->setColor(diffuse); - light->setIntensity(intensity); - light->setAmbientIntensity(ambientIntensity); -} - -void DeferredLightingEffect::setGlobalSkybox(const model::SkyboxPointer& skybox) { - _skybox = skybox; - auto light = _allocatedLights.front(); - - if (_skybox && _skybox->getCubemap() && _skybox->getCubemap()->isDefined() && _skybox->getCubemap()->getIrradiance()) { - light->setAmbientSphere( (*_skybox->getCubemap()->getIrradiance()) ); - } +void DeferredLightingEffect::setGlobalLight(const model::LightPointer& light, const gpu::TexturePointer& skyboxTexture) { + _allocatedLights.front() = light; + _skyboxTexture = skyboxTexture; } model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 8cb4bbba8f..75cefc277d 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -18,7 +18,6 @@ #include #include "model/Light.h" -#include "model/Stage.h" #include "model/Geometry.h" #include "render/Context.h" @@ -37,11 +36,12 @@ public: /// Adds a point light to render for the current frame. void addPointLight(const glm::vec3& position, float radius, const glm::vec3& color = glm::vec3(0.0f, 0.0f, 0.0f), - float intensity = 0.5f); + float intensity = 0.5f, float falloffRadius = 0.01f); /// Adds a spot light to render for the current frame. void addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color = glm::vec3(1.0f, 1.0f, 1.0f), - float intensity = 0.5f, const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI); + float intensity = 0.5f, float falloffRadius = 0.01f, + const glm::quat& orientation = glm::quat(), float exponent = 0.0f, float cutoff = PI); void prepare(RenderArgs* args); void render(const render::RenderContextPointer& renderContext); @@ -49,9 +49,7 @@ public: void setupKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int skyboxCubemapUnit); // update global lighting - void setAmbientLightMode(int preset); - void setGlobalLight(const glm::vec3& direction, const glm::vec3& diffuse, float intensity, float ambientIntensity); - void setGlobalSkybox(const model::SkyboxPointer& skybox); + void setGlobalLight(const model::LightPointer& light, const gpu::TexturePointer& skyboxTexture); const LightStage& getLightStage() { return _lightStage; } void setShadowMapEnabled(bool enable) { _shadowMapEnabled = enable; }; @@ -98,7 +96,7 @@ private: std::vector _spotLights; int _ambientLightMode = 0; - model::SkyboxPointer _skybox; + gpu::TexturePointer _skyboxTexture; // Class describing the uniform buffer with all the parameters common to the deferred shaders class DeferredTransform { diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index c0003219a0..4ae8ba91c7 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -413,9 +413,9 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { if (!_isBlendShaped) { batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0); - + batch.setInputFormat((_drawMesh->getVertexFormat())); - + batch.setInputStream(0, _drawMesh->getVertexStream()); } else { batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0); @@ -426,7 +426,7 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { batch.setInputBuffer(1, _model->_blendedVertexBuffers[_meshIndex], _drawMesh->getNumVertices() * sizeof(glm::vec3), sizeof(glm::vec3)); batch.setInputStream(2, _drawMesh->getVertexStream().makeRangedStream(2)); } - + // TODO: Get rid of that extra call if (!_hasColorAttrib) { batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f); @@ -474,8 +474,8 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { #ifdef DEBUG_BOUNDING_PARTS { AABox partBounds = getPartBounds(_meshIndex, partIndex); - bool inView = args->_viewFrustum->boxInFrustum(partBounds) != ViewFrustum::OUTSIDE; - + bool inView = args->_viewFrustum->computeBoxViewLocation(partBounds) != ViewFrustum::OUTSIDE; + glm::vec4 cubeColor; if (isSkinned) { cubeColor = glm::vec4(0.0f, 1.0f, 1.0f, 1.0f); @@ -484,7 +484,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { } else { cubeColor = glm::vec4(1.0f, 1.0f, 0.0f, 1.0f); } - + Transform transform; transform.setTranslation(partBounds.calcCenter()); transform.setScale(partBounds.getDimensions()); @@ -492,7 +492,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { DependencyManager::get()->renderWireCube(batch, 1.0f, cubeColor); } #endif //def DEBUG_BOUNDING_PARTS - + auto locations = args->_pipeline->locations; assert(locations); @@ -500,23 +500,23 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { bool canCauterize = args->_renderMode != RenderArgs::SHADOW_RENDER_MODE; _model->updateClusterMatrices(_transform.getTranslation(), _transform.getRotation()); bindTransform(batch, locations, canCauterize); - + //Bind the index buffer and vertex buffer and Blend shapes if needed bindMesh(batch); - + // apply material properties bindMaterial(batch, locations); - + if (args) { args->_details._materialSwitches++; } - + // Draw! { PerformanceTimer perfTimer("batch.drawIndexed()"); drawCall(batch); } - + if (args) { const int INDICES_PER_TRIANGLE = 3; args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE; diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index fcfb0b336e..a64d4a81a8 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -48,7 +48,7 @@ void main(void) { vec3 fragLightVec = getLightPosition(light) - fragPos.xyz; // Kill if too far from the light center - if (dot(fragLightVec, fragLightVec) > getLightSquareRadius(light)) { + if (dot(fragLightVec, fragLightVec) > getLightCutoffSquareRadius(light)) { discard; } diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index 8170929636..b72598f810 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -47,7 +47,7 @@ void main(void) { vec3 fragLightVec = getLightPosition(light) - fragPos.xyz; // Kill if too far from the light center - if (dot(fragLightVec, fragLightVec) > getLightSquareRadius(light)) { + if (dot(fragLightVec, fragLightVec) > getLightCutoffSquareRadius(light)) { discard; } diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp index 3fc6bffbd3..66ba6f12f5 100644 --- a/libraries/render/src/render/CullTask.cpp +++ b/libraries/render/src/render/CullTask.cpp @@ -29,7 +29,7 @@ void render::cullItems(const RenderContextPointer& renderContext, const CullFunc ViewFrustum* frustum = args->_viewFrustum; details._considered += (int)inItems.size(); - + // Culling / LOD for (auto item : inItems) { if (item.bound.isNull()) { @@ -41,8 +41,8 @@ void render::cullItems(const RenderContextPointer& renderContext, const CullFunc // when they are outside of the view frustum... bool outOfView; { - PerformanceTimer perfTimer("boxInFrustum"); - outOfView = frustum->boxInFrustum(item.bound) == ViewFrustum::OUTSIDE; + PerformanceTimer perfTimer("computeBoxViewLocation"); + outOfView = frustum->computeBoxViewLocation(item.bound) == ViewFrustum::OUTSIDE; } if (!outOfView) { bool bigEnoughToRender; @@ -88,10 +88,10 @@ struct BackToFrontSort { void render::depthSortItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, bool frontToBack, const ItemBounds& inItems, ItemBounds& outItems) { assert(renderContext->args); assert(renderContext->args->_viewFrustum); - + auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; - + // Allocate and simply copy outItems.clear(); @@ -237,8 +237,8 @@ void CullSpatialSelection::run(const SceneContextPointer& sceneContext, const Re */ } - bool frustumTest(const AABox& bound) { - if (_args->_viewFrustum->boxInFrustum(bound) == ViewFrustum::OUTSIDE) { + bool viewTest(const AABox& bound) { + if (_args->_viewFrustum->computeBoxViewLocation(bound) == ViewFrustum::OUTSIDE) { _renderDetails._outOfView++; return false; } @@ -302,7 +302,7 @@ void CullSpatialSelection::run(const SceneContextPointer& sceneContext, const Re auto& item = scene->getItem(id); if (_filter.test(item.getKey())) { ItemBound itemBound(id, item.getBound()); - if (test.frustumTest(itemBound.bound)) { + if (test.viewTest(itemBound.bound)) { outItems.emplace_back(itemBound); } } @@ -316,7 +316,7 @@ void CullSpatialSelection::run(const SceneContextPointer& sceneContext, const Re auto& item = scene->getItem(id); if (_filter.test(item.getKey())) { ItemBound itemBound(id, item.getBound()); - if (test.frustumTest(itemBound.bound)) { + if (test.viewTest(itemBound.bound)) { if (test.solidAngleTest(itemBound.bound)) { outItems.emplace_back(itemBound); } diff --git a/libraries/script-engine/src/SceneScriptingInterface.cpp b/libraries/script-engine/src/SceneScriptingInterface.cpp index b2f3824221..079cfff2c7 100644 --- a/libraries/script-engine/src/SceneScriptingInterface.cpp +++ b/libraries/script-engine/src/SceneScriptingInterface.cpp @@ -77,6 +77,10 @@ void SceneScripting::KeyLight::setAmbientIntensity(float intensity) { _skyStage->setSunAmbientIntensity(intensity); } +void SceneScripting::KeyLight::setAmbientSphere(const gpu::SHPointer& sphere) { + _skyStage->setSunAmbientSphere(sphere); +} + glm::vec3 SceneScripting::KeyLight::getDirection() const { return _skyStage->getSunDirection(); } diff --git a/libraries/script-engine/src/SceneScriptingInterface.h b/libraries/script-engine/src/SceneScriptingInterface.h index 0be8b066aa..12681b1887 100644 --- a/libraries/script-engine/src/SceneScriptingInterface.h +++ b/libraries/script-engine/src/SceneScriptingInterface.h @@ -81,6 +81,10 @@ namespace SceneScripting { // setDirection is only effective if stage Sun model is disabled void setDirection(const glm::vec3& direction); + // AmbientTexture is unscriptable - it must be set through the zone entity + void setAmbientSphere(const gpu::SHPointer& sphere); + void resetAmbientSphere() { setAmbientSphere(nullptr); } + protected: model::SunSkyStagePointer _skyStage; }; diff --git a/libraries/shared/src/LogHandler.cpp b/libraries/shared/src/LogHandler.cpp index c1cf969f36..a338dfe8c2 100644 --- a/libraries/shared/src/LogHandler.cpp +++ b/libraries/shared/src/LogHandler.cpp @@ -26,9 +26,7 @@ LogHandler& LogHandler::getInstance() { return staticInstance; } -LogHandler::LogHandler() : - _shouldOutputProcessID(false), - _shouldOutputThreadID(false) +LogHandler::LogHandler() { // setup our timer to flush the verbose logs every 5 seconds QTimer* logFlushTimer = new QTimer(this); @@ -62,6 +60,9 @@ const char* stringForLogType(LogMsgType msgType) { // the following will produce 11/18 13:55:36 const QString DATE_STRING_FORMAT = "MM/dd hh:mm:ss"; +// the following will produce 11/18 13:55:36.999 +const QString DATE_STRING_FORMAT_WITH_MILLISECONDS = "MM/dd hh:mm:ss.zzz"; + void LogHandler::flushRepeatedMessages() { QMutexLocker locker(&_repeatedMessageLock); QHash::iterator message = _repeatMessageCountHash.begin(); @@ -132,7 +133,12 @@ QString LogHandler::printMessage(LogMsgType type, const QMessageLogContext& cont // log prefix is in the following format // [TIMESTAMP] [DEBUG] [PID] [TID] [TARGET] logged string - QString prefixString = QString("[%1]").arg(QDateTime::currentDateTime().toString(DATE_STRING_FORMAT)); + const QString* dateFormatPtr = &DATE_STRING_FORMAT; + if (_shouldDisplayMilliseconds) { + dateFormatPtr = &DATE_STRING_FORMAT_WITH_MILLISECONDS; + } + + QString prefixString = QString("[%1]").arg(QDateTime::currentDateTime().toString(*dateFormatPtr)); prefixString.append(QString(" [%1]").arg(stringForLogType(type))); diff --git a/libraries/shared/src/LogHandler.h b/libraries/shared/src/LogHandler.h index ee8e426c34..d346913dd3 100644 --- a/libraries/shared/src/LogHandler.h +++ b/libraries/shared/src/LogHandler.h @@ -42,6 +42,7 @@ public: void setShouldOutputProcessID(bool shouldOutputProcessID) { _shouldOutputProcessID = shouldOutputProcessID; } void setShouldOutputThreadID(bool shouldOutputThreadID) { _shouldOutputThreadID = shouldOutputThreadID; } + void setShouldDisplayMilliseconds(bool shouldDisplayMilliseconds) { _shouldDisplayMilliseconds = shouldDisplayMilliseconds; } QString printMessage(LogMsgType type, const QMessageLogContext& context, const QString &message); @@ -57,8 +58,9 @@ private: void flushRepeatedMessages(); QString _targetName; - bool _shouldOutputProcessID; - bool _shouldOutputThreadID; + bool _shouldOutputProcessID { false }; + bool _shouldOutputThreadID { false }; + bool _shouldDisplayMilliseconds { false }; QSet _repeatedMessageRegexes; QHash _repeatMessageCountHash; QHash _lastRepeatedMessage;