mirror of
https://github.com/overte-org/overte.git
synced 2025-08-05 04:10:11 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into toggleOverlay
Conflicts: interface/src/Application.cpp
This commit is contained in:
commit
b0c286bc3b
78 changed files with 2213 additions and 1297 deletions
|
@ -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 << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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.</br></br>"
|
||||
+ "In order to make your domain reachable, this will also enable full automatic networking.",
|
||||
showCancelButton: true,
|
||||
|
|
|
@ -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<const unsigned char*>(usernameWithToken.constData()),
|
||||
usernameWithToken.size(),
|
||||
|
|
|
@ -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<LimitedNodeList>();
|
||||
|
||||
|
@ -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<LimitedNodeList>()->sendHeartbeatToIceServer(_iceServerSocket);
|
||||
|
||||
auto& accountManager = AccountManager::getInstance();
|
||||
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
||||
|
||||
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<ReceivedMes
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> 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<LimitedNodeList>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ public slots:
|
|||
void processNodeJSONStatsPacket(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer sendingNode);
|
||||
void processPathQueryPacket(QSharedPointer<ReceivedMessage> packet);
|
||||
void processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message);
|
||||
void processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> 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<NLPacket> _iceServerHeartbeatPacket;
|
||||
|
||||
QTimer* _iceHeartbeatTimer { nullptr }; // this looks like it dangles when created but it's parented to the DomainServer
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 @@
|
|||
<div class="text-section property">
|
||||
<div class="label">Line Height</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-text-line-height" min="0" step="0.005">
|
||||
<input class="coord" type="number" id="property-text-line-height" min="0" step="0.005">
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-section property">
|
||||
<div class="label">Text Color</div>
|
||||
<div class="value">
|
||||
<div class='color-picker' id="property-text-text-color"></div>
|
||||
<div class="input-area">R <input class="coord" type='number' id="property-text-text-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type='number' id="property-text-text-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type='number' id="property-text-text-color-blue"></div>
|
||||
<div class="input-area">R <input class="coord" type="number" id="property-text-text-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type="number" id="property-text-text-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type="number" id="property-text-text-color-blue"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-section property">
|
||||
<div class="label">Background Color</div>
|
||||
<div class="value">
|
||||
<div class='color-picker' id="property-text-background-color"></div>
|
||||
<div class="input-area">R <input class="coord" type='number' id="property-text-background-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type='number' id="property-text-background-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type='number' id="property-text-background-color-blue"></div>
|
||||
<div class="input-area">R <input class="coord" type="number" id="property-text-background-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type="number" id="property-text-background-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type="number" id="property-text-background-color-blue"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1085,32 +1091,32 @@
|
|||
<div class="label">Light Color</div>
|
||||
<div class="value">
|
||||
<div class='color-picker' id="property-zone-key-light-color"></div>
|
||||
<div class="input-area">R <input class="coord" type='number' id="property-zone-key-light-color-red" min="0" max="255" step="1"></div>
|
||||
<div class="input-area">G <input class="coord" type='number' id="property-zone-key-light-color-green" min="0" max="255" step="1"></div>
|
||||
<div class="input-area">B <input class="coord" type='number' id="property-zone-key-light-color-blue" min="0" max="255" step="1"></div>
|
||||
<div class="input-area">R <input class="coord" type="number" id="property-zone-key-light-color-red" min="0" max="255" step="1"></div>
|
||||
<div class="input-area">G <input class="coord" type="number" id="property-zone-key-light-color-green" min="0" max="255" step="1"></div>
|
||||
<div class="input-area">B <input class="coord" type="number" id="property-zone-key-light-color-blue" min="0" max="255" step="1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone-section keyLight-section property">
|
||||
<div class="label">Light Intensity</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-zone-key-intensity" min="0" max="10" step="0.1">
|
||||
<input class="coord" type="number" id="property-zone-key-intensity" min="0" max="10" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone-section keyLight-section property">
|
||||
<div class="label">Light Direction</div>
|
||||
<div class="value">
|
||||
<div class="input-area">Pitch <input class="coord" type='number' id="property-zone-key-light-direction-x"></div>
|
||||
<div class="input-area">Yaw <input class="coord" type='number' id="property-zone-key-light-direction-y"></div>
|
||||
<div class="input-area">Roll <input class="coord" type='number' id="property-zone-key-light-direction-z"></div>
|
||||
<div class="input-area">Pitch <input class="coord" type="number" id="property-zone-key-light-direction-x"></div>
|
||||
<div class="input-area">Yaw <input class="coord" type="number" id="property-zone-key-light-direction-y"></div>
|
||||
<div class="input-area">Roll <input class="coord" type="number" id="property-zone-key-light-direction-z"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone-section keyLight-section property">
|
||||
<div class="label">Ambient Intensity</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-zone-key-ambient-intensity" min="0" max="10" step="0.1">
|
||||
<input class="coord" type="number" id="property-zone-key-ambient-intensity" min="0" max="10" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1129,19 +1135,19 @@
|
|||
<div class="zone-section stage-section property">
|
||||
<div class="label">Stage Latitude</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-zone-stage-latitude" min="-90" max="90" step="1">
|
||||
<input class="coord" type="number" id="property-zone-stage-latitude" min="-90" max="90" step="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="zone-section stage-section property">
|
||||
<div class="label">Stage Longitude</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-zone-stage-longitude" min="-180" max="180" step="1">
|
||||
<input class="coord" type="number" id="property-zone-stage-longitude" min="-180" max="180" step="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="zone-section stage-section property">
|
||||
<div class="label">Stage Altitude</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-zone-stage-altitude" step="1">
|
||||
<input class="coord" type="number" id="property-zone-stage-altitude" step="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1155,13 +1161,13 @@
|
|||
<div class="zone-section stage-section property">
|
||||
<div class="label">Stage Day</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-zone-stage-day" min="0" max="365" step="1">
|
||||
<input class="coord" type="number" id="property-zone-stage-day" min="0" max="365" step="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="zone-section stage-section property">
|
||||
<div class="label">Stage Hour</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-zone-stage-hour" min="0" max="24" step="0.5">
|
||||
<input class="coord" type="number" id="property-zone-stage-hour" min="0" max="24" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1186,9 +1192,9 @@
|
|||
<div class="label">Skybox Color</div>
|
||||
<div class="value">
|
||||
<div class='color-picker' id="property-zone-skybox-color"></div>
|
||||
<div class="input-area">R <input class="coord" type='number' id="property-zone-skybox-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type='number' id="property-zone-skybox-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type='number' id="property-zone-skybox-color-blue"></div>
|
||||
<div class="input-area">R <input class="coord" type="number" id="property-zone-skybox-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type="number" id="property-zone-skybox-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type="number" id="property-zone-skybox-color-blue"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1264,23 +1270,23 @@
|
|||
<div class="property">
|
||||
<div class="label">Registration</div>
|
||||
<div class="value">
|
||||
<div class="input-area">X <input class="coord" type='number' id="property-reg-x"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type='number' id="property-reg-y"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type='number' id="property-reg-z"><div class="prop-z"></div></div>
|
||||
<div class="input-area">X <input class="coord" type="number" id="property-reg-x"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type="number" id="property-reg-y"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type="number" id="property-reg-z"><div class="prop-z"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<div class="label">Dimensions</div>
|
||||
<div class="value">
|
||||
<div class="input-area">X <input class="coord" type='number' id="property-dim-x" step="0.1"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type='number' id="property-dim-y" step="0.1"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type='number' id="property-dim-z" step="0.1"><div class="prop-z"></div></div>
|
||||
<div class="input-area">X <input class="coord" type="number" id="property-dim-x" step="0.1"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type="number" id="property-dim-y" step="0.1"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type="number" id="property-dim-z" step="0.1"><div class="prop-z"></div></div>
|
||||
<div>
|
||||
<input type="button" id="reset-to-natural-dimensions" value="Reset to Natural Dimensions">
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<input class="" type='number' id="dimension-rescale-pct" value=100>%
|
||||
<input class="" type="number" id="dimension-rescale-pct" value=100>%
|
||||
</div>
|
||||
<span>
|
||||
<input type="button" id="dimension-rescale-button" value="Rescale">
|
||||
|
@ -1291,9 +1297,9 @@
|
|||
<div class="poly-vox-section property">
|
||||
<div class="label">Voxel Volume Size</div>
|
||||
<div class="value">
|
||||
<div class="input-area">X <br> <input class="coord" type='number' id="property-voxel-volume-size-x"></div>
|
||||
<div class="input-area">Y <br><input class="coord" type='number' id="property-voxel-volume-size-y"></div>
|
||||
<div class="input-area">Z <br><input class="coord" type='number' id="property-voxel-volume-size-z"></div>
|
||||
<div class="input-area">X <br> <input class="coord" type="number" id="property-voxel-volume-size-x"></div>
|
||||
<div class="input-area">Y <br><input class="coord" type="number" id="property-voxel-volume-size-y"></div>
|
||||
<div class="input-area">Z <br><input class="coord" type="number" id="property-voxel-volume-size-z"></div>
|
||||
</div>
|
||||
|
||||
<div class="label">Surface Extractor</div>
|
||||
|
@ -1325,9 +1331,9 @@
|
|||
<div class="property">
|
||||
<div class="label">Rotation</div>
|
||||
<div class="value">
|
||||
<div class="input-area">Pitch <input class="coord" type='number' id="property-rot-x" step="0.1"></div>
|
||||
<div class="input-area">Yaw <input class="coord" type='number' id="property-rot-y" step="0.1"></div>
|
||||
<div class="input-area">Roll <input class="coord" type='number' id="property-rot-z"step="0.1"></div>
|
||||
<div class="input-area">Pitch <input class="coord" type="number" id="property-rot-x" step="0.1"></div>
|
||||
<div class="input-area">Yaw <input class="coord" type="number" id="property-rot-y" step="0.1"></div>
|
||||
<div class="input-area">Roll <input class="coord" type="number" id="property-rot-z"step="0.1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1339,66 +1345,66 @@
|
|||
<div class="property">
|
||||
<div class="label">Linear Velocity</div>
|
||||
<div class="value">
|
||||
<div class="input-area">X <input class="coord" type='number' id="property-lvel-x"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type='number' id="property-lvel-y"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type='number' id="property-lvel-z"><div class="prop-z"></div></div>
|
||||
<div class="input-area">X <input class="coord" type="number" id="property-lvel-x"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type="number" id="property-lvel-y"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type="number" id="property-lvel-z"><div class="prop-z"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="label">Linear Damping</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-ldamping">
|
||||
<input class="coord" type="number" id="property-ldamping">
|
||||
</div>
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="label">Angular Velocity</div>
|
||||
<div class="value">
|
||||
<div class="input-area">Pitch <input class="coord" type='number' id="property-avel-x"></div>
|
||||
<div class="input-area">Yaw <input class="coord" type='number' id="property-avel-y"></div>
|
||||
<div class="input-area">Roll <input class="coord" type='number' id="property-avel-z"></div>
|
||||
<div class="input-area">Pitch <input class="coord" type="number" id="property-avel-x"></div>
|
||||
<div class="input-area">Yaw <input class="coord" type="number" id="property-avel-y"></div>
|
||||
<div class="input-area">Roll <input class="coord" type="number" id="property-avel-z"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="label">Angular Damping</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-adamping">
|
||||
<input class="coord" type="number" id="property-adamping">
|
||||
</div>
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="label">Restitution</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-restitution">
|
||||
<input class="coord" type="number" id="property-restitution">
|
||||
</div>
|
||||
</div>
|
||||
<div class="property">
|
||||
<div class="label">Friction</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-friction">
|
||||
<input class="coord" type="number" id="property-friction">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<div class="label">Gravity</div>
|
||||
<div class="value">
|
||||
<div class="input-area">X <input class="coord" type='number' id="property-grav-x"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type='number' id="property-grav-y"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type='number' id="property-grav-z"><div class="prop-z"></div></div>
|
||||
<div class="input-area">X <input class="coord" type="number" id="property-grav-x"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type="number" id="property-grav-y"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type="number" id="property-grav-z"><div class="prop-z"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<div class="label">Acceleration</div>
|
||||
<div class="value">
|
||||
<div class="input-area">X <input class="coord" type='number' id="property-lacc-x"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type='number' id="property-lacc-y"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type='number' id="property-lacc-z"><div class="prop-z"></div></div>
|
||||
<div class="input-area">X <input class="coord" type="number" id="property-lacc-x"><div class="prop-x"></div></div>
|
||||
<div class="input-area">Y <input class="coord" type="number" id="property-lacc-y"><div class="prop-y"></div></div>
|
||||
<div class="input-area">Z <input class="coord" type="number" id="property-lacc-z"><div class="prop-z"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<div class="label">Density</div>
|
||||
<div>
|
||||
<input type='number' id="property-density">
|
||||
<input type="number" id="property-density">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1406,9 +1412,9 @@
|
|||
<div class="label">Color</div>
|
||||
<div class="value">
|
||||
<div id="property-color" class='color-picker'></div>
|
||||
<div class="input-area">R <input class="coord" type='number' id="property-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type='number' id="property-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type='number' id="property-color-blue"></div>
|
||||
<div class="input-area">R <input class="coord" type="number" id="property-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type="number" id="property-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type="number" id="property-color-blue"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1483,7 +1489,7 @@
|
|||
<div class="property">
|
||||
<div class="label">Lifetime</div>
|
||||
<div class="value">
|
||||
<input type='number' id="property-lifetime">
|
||||
<input type="number" id="property-lifetime">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1541,25 +1547,25 @@
|
|||
<div class="model-section property">
|
||||
<div class="label">Animation FPS</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-model-animation-fps">
|
||||
<input class="coord" type="number" id="property-model-animation-fps">
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-section property">
|
||||
<div class="label">Animation Frame</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-model-animation-frame">
|
||||
<input class="coord" type="number" id="property-model-animation-frame">
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-section property">
|
||||
<div class="label">Animation First Frame</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-model-animation-first-frame">
|
||||
<input class="coord" type="number" id="property-model-animation-first-frame">
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-section property">
|
||||
<div class="label">Animation Last Frame</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-model-animation-last-frame">
|
||||
<input class="coord" type="number" id="property-model-animation-last-frame">
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-section property">
|
||||
|
@ -1591,37 +1597,43 @@
|
|||
<label>Light</label>
|
||||
</div>
|
||||
|
||||
<div class="light-section property">
|
||||
<div class="label">Color</div>
|
||||
<div class="value">
|
||||
<div class='color-picker' id="property-light-color"></div>
|
||||
<div class="input-area">R <input class="coord" type="number" id="property-light-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type="number" id="property-light-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type="number" id="property-light-color-blue"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="light-section property">
|
||||
<div class="label">Intensity</div>
|
||||
<div class="value">
|
||||
<input class="coord" type="number" id="property-light-intensity" min="0" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="light-section property">
|
||||
<div class="label">Falloff Radius</div>
|
||||
<div class="value">
|
||||
<input class="coord" type="number" id="property-light-falloff-radius" min="0" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="light-section property">
|
||||
<span class="label">Spot Light</span>
|
||||
<span class="value">
|
||||
<input type='checkbox' id="property-light-spot-light">
|
||||
</span>
|
||||
</div>
|
||||
<div class="light-section property">
|
||||
<div class="label">Color</div>
|
||||
<div class="value">
|
||||
<div class='color-picker' id="property-light-color"></div>
|
||||
<div class="input-area">R <input class="coord" type='number' id="property-light-color-red"></div>
|
||||
<div class="input-area">G <input class="coord" type='number' id="property-light-color-green"></div>
|
||||
<div class="input-area">B <input class="coord" type='number' id="property-light-color-blue"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="light-section property">
|
||||
<div class="label">Intensity</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-light-intensity">
|
||||
</div>
|
||||
</div>
|
||||
<div class="light-section property">
|
||||
<div class="label">Spot Light Exponent</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-light-exponent">
|
||||
<input class="coord" type="number" id="property-light-exponent" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
<div class="light-section property">
|
||||
<div class="label">Spot Light Cutoff (degrees)</div>
|
||||
<div class="value">
|
||||
<input class="coord" type='number' id="property-light-cutoff">
|
||||
<input class="coord" type="number" id="property-light-cutoff" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -43,10 +43,10 @@ var keysToAllow = [
|
|||
'emitSpeed',
|
||||
'speedSpread',
|
||||
'emitOrientation',
|
||||
'emitDimensios',
|
||||
'emitRadiusStart',
|
||||
'emitDimensions',
|
||||
'polarStart',
|
||||
'polarFinish',
|
||||
'azimuthStart',
|
||||
'azimuthFinish',
|
||||
'emitAcceleration',
|
||||
'accelerationSpread',
|
||||
|
|
229
examples/playa/fireworks/fireworksLaunchButtonEntityScript.js
Normal file
229
examples/playa/fireworks/fireworksLaunchButtonEntityScript.js
Normal file
|
@ -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();
|
||||
});
|
46
examples/playa/fireworks/fireworksLaunchButtonSpawner.js
Normal file
46
examples/playa/fireworks/fireworksLaunchButtonSpawner.js
Normal file
|
@ -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);
|
|
@ -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})
|
||||
|
|
|
@ -9,14 +9,21 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <QTimer>
|
||||
#include "IceServer.h"
|
||||
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include <QtNetwork/QNetworkRequest>
|
||||
|
||||
#include <LimitedNodeList.h>
|
||||
#include <NetworkingConstants.h>
|
||||
#include <udt/PacketHeaders.h>
|
||||
#include <SharedUtil.h>
|
||||
|
||||
#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<udt::Packet> 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<NetworkPeer>::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<NetworkPeer>::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<const unsigned char*>(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<const unsigned char*>(hashedPlaintext.constData()),
|
||||
hashedPlaintext.size(),
|
||||
reinterpret_cast<const unsigned char*>(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) {
|
||||
|
|
|
@ -16,13 +16,15 @@
|
|||
#include <QtCore/QSharedPointer>
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include <UUIDHasher.h>
|
||||
|
||||
#include <NetworkPeer.h>
|
||||
#include <HTTPConnection.h>
|
||||
#include <HTTPManager.h>
|
||||
#include <NLPacket.h>
|
||||
#include <udt/Socket.h>
|
||||
|
||||
typedef QHash<QUuid, SharedNetworkPeer> 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<udt::Packet> 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<QUuid, SharedNetworkPeer>;
|
||||
NetworkPeerHash _activePeers;
|
||||
|
||||
HTTPManager _httpManager;
|
||||
|
||||
using DomainPublicKeyHash = std::unordered_map<QUuid, QByteArray>;
|
||||
DomainPublicKeyHash _domainPublicKeys;
|
||||
};
|
||||
|
||||
#endif // hifi_IceServer_h
|
||||
|
|
|
@ -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<DiscoverabilityManager>();
|
||||
connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation);
|
||||
connect(&locationUpdateTimer, &QTimer::timeout,
|
||||
connect(&locationUpdateTimer, &QTimer::timeout,
|
||||
DependencyManager::get<AddressManager>().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<AddressManager>().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<EntityScriptingInterface>();
|
||||
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<ScriptEngines>()->saveScripts();
|
||||
DependencyManager::get<ScriptEngines>()->shutdownScripting(); // stop all currently running global scripts
|
||||
DependencyManager::destroy<ScriptEngines>();
|
||||
DependencyManager::destroy<ScriptEngines>();
|
||||
|
||||
// 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<AnimationCache>().data());
|
||||
rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance());
|
||||
rootContext->setContextProperty("Controller", DependencyManager::get<controller::ScriptingInterface>().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<QEvent::Type>(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<DeferredLightingEffect>()->setAmbientLightMode(-1);
|
||||
auto skyStage = DependencyManager::get<SceneScriptingInterface>()->getSkyStage();
|
||||
DependencyManager::get<DeferredLightingEffect>()->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<DeferredLightingEffect>()->setGlobalSkybox(skybox);
|
||||
auto stage = DependencyManager::get<SceneScriptingInterface>()->getSkyStage();
|
||||
DependencyManager::get<DeferredLightingEffect>()->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<ReceivedMessage> 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<NodeList>()->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<ScriptEngines>();
|
||||
QString fileNameString = OffscreenUi::getOpenFileName(
|
||||
|
@ -4782,7 +4723,7 @@ static void addDisplayPluginToMenu(DisplayPluginPointer displayPlugin, bool acti
|
|||
groupingMenu = "Developer";
|
||||
break;
|
||||
default:
|
||||
groupingMenu = "Standard";
|
||||
groupingMenu = "Standard";
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ReceivedMessage> message);
|
||||
|
||||
void notifyPacketVersionMismatch();
|
||||
|
||||
void loadSettings();
|
||||
|
@ -476,7 +472,6 @@ private:
|
|||
typedef bool (Application::* AcceptURLMethod)(const QString &);
|
||||
static const QHash<QString, AcceptURLMethod> _acceptedExtensions;
|
||||
|
||||
QList<QString> _domainConnectionRefusals;
|
||||
glm::uvec2 _renderResolution;
|
||||
|
||||
int _maxOctreePPS = DEFAULT_MAX_OCTREE_PPS;
|
||||
|
|
|
@ -22,11 +22,8 @@
|
|||
#include <QStandardPaths>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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<QString, DataServerAccountInfo> accounts;
|
||||
|
||||
if (action == CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) {
|
||||
// Read login and avatar info
|
||||
|
||||
qRegisterMetaType<DataServerAccountInfo>("DataServerAccountInfo");
|
||||
qRegisterMetaTypeStreamOperators<DataServerAccountInfo>("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<DataServerAccountInfo>());
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ public:
|
|||
private:
|
||||
enum Action {
|
||||
DELETE_INTERFACE_INI,
|
||||
RETAIN_LOGIN_AND_AVATAR_INFO,
|
||||
RETAIN_AVATAR_INFO,
|
||||
DO_NOTHING
|
||||
};
|
||||
|
||||
|
|
|
@ -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<OffscreenUi>()->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<OffscreenUi>()->toggle(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), "GeneralPreferencesDialog");
|
||||
});
|
||||
|
||||
|
||||
// Settings > Avatar...-- FIXME: needs implementation
|
||||
// Settings > Avatar...
|
||||
action = addActionToQMenuAndActionHash(settingsMenu, "Avatar...");
|
||||
connect(action, &QAction::triggered, [] {
|
||||
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), "AvatarPreferencesDialog");
|
||||
});
|
||||
|
||||
// Settings > Audio...-- FIXME: needs implementation
|
||||
// Settings > Audio...
|
||||
action = addActionToQMenuAndActionHash(settingsMenu, "Audio...");
|
||||
connect(action, &QAction::triggered, [] {
|
||||
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/AudioPreferencesDialog.qml"), "AudioPreferencesDialog");
|
||||
});
|
||||
|
||||
// Settings > LOD...-- FIXME: needs implementation
|
||||
// Settings > LOD...
|
||||
action = addActionToQMenuAndActionHash(settingsMenu, "LOD...");
|
||||
connect(action, &QAction::triggered, [] {
|
||||
DependencyManager::get<OffscreenUi>()->toggle(QString("hifi/dialogs/LodPreferencesDialog.qml"), "LodPreferencesDialog");
|
||||
|
|
|
@ -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<AvatarManager>()->getLocalLights()) {
|
||||
glm::vec3 direction = orientation * light.direction;
|
||||
DependencyManager::get<DeferredLightingEffect>()->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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
WindowScriptingInterface::WindowScriptingInterface() {
|
||||
const DomainHandler& domainHandler = DependencyManager::get<NodeList>()->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 =
|
||||
|
|
|
@ -134,13 +134,14 @@ void EntityTreeRenderer::update() {
|
|||
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_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<EntityTree>(_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<float>::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<ZoneEntityItem> zone) {
|
||||
auto textureCache = DependencyManager::get<TextureCache>();
|
||||
auto scene = DependencyManager::get<SceneScriptingInterface>();
|
||||
auto sceneStage = scene->getStage();
|
||||
auto skyStage = scene->getSkyStage();
|
||||
|
@ -264,7 +268,11 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
|
|||
_pendingSkyboxTexture = false;
|
||||
_skyboxTexture.clear();
|
||||
|
||||
_pendingAmbientTexture = false;
|
||||
_ambientTexture.clear();
|
||||
|
||||
if (_hasPreviousZone) {
|
||||
sceneKeyLight->resetAmbientSphere();
|
||||
sceneKeyLight->setColor(_previousKeyLightColor);
|
||||
sceneKeyLight->setIntensity(_previousKeyLightIntensity);
|
||||
sceneKeyLight->setAmbientIntensity(_previousKeyLightAmbientIntensity);
|
||||
|
@ -274,6 +282,7 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
|
|||
_previousStageAltitude);
|
||||
sceneTime->setHour(_previousStageHour);
|
||||
sceneTime->setDay(_previousStageDay);
|
||||
|
||||
_hasPreviousZone = false;
|
||||
}
|
||||
|
||||
|
@ -306,6 +315,23 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
|
|||
sceneTime->setHour(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<ProceduralSkybox>(skyStage->getSkybox());
|
||||
|
@ -326,12 +352,16 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
|
|||
_skyboxTexture.clear();
|
||||
} else {
|
||||
// Update the Texture of the Skybox with the one pointed by this zone
|
||||
auto textureCache = DependencyManager::get<TextureCache>();
|
||||
_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_ptr<ZoneEntityIt
|
|||
_skyboxTexture.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isAmbientTextureSet) {
|
||||
sceneKeyLight->resetAmbientSphere();
|
||||
}
|
||||
}
|
||||
|
||||
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<ZoneEntityItem>(zone);
|
||||
}
|
||||
}
|
||||
if (_bestZone && _bestZone->getID() == id) {
|
||||
applyZonePropertiesToScene(_bestZone);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<EntityItemID> _currentEntitiesInside;
|
||||
|
||||
bool _pendingSkyboxTexture { false };
|
||||
NetworkTexturePointer _skyboxTexture;
|
||||
|
||||
bool _pendingAmbientTexture { false };
|
||||
NetworkTexturePointer _ambientTexture;
|
||||
|
||||
bool _wantScripts;
|
||||
ScriptEngine* _entitiesScriptEngine;
|
||||
|
||||
|
|
|
@ -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<DeferredLightingEffect>()->addSpotLight(position, largestDiameter / 2.0f,
|
||||
color, intensity, rotation, exponent, cutoff);
|
||||
color, intensity, falloffRadius, rotation, exponent, cutoff);
|
||||
} else {
|
||||
DependencyManager::get<DeferredLightingEffect>()->addPointLight(position, largestDiameter / 2.0f,
|
||||
color, intensity);
|
||||
color, intensity, falloffRadius);
|
||||
}
|
||||
|
||||
#ifdef WANT_DEBUG
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include <GeometryCache.h>
|
||||
#include <PerfStat.h>
|
||||
|
||||
#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<EntityTreeRenderer>()->updateZone(_id);
|
||||
}
|
||||
|
||||
int RenderableZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead,
|
||||
ReadBitstreamToTreeParams& args,
|
||||
EntityPropertyFlags& propertyFlags, bool overwriteLocalData,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<QString> EntityItemProperties::listChangedProperties() {
|
|||
if (intensityChanged()) {
|
||||
out += "intensity";
|
||||
}
|
||||
if (falloffRadiusChanged()) {
|
||||
out += "falloffRadius";
|
||||
}
|
||||
if (exponentChanged()) {
|
||||
out += "exponent";
|
||||
}
|
||||
|
|
|
@ -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, "");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<EntityTreeElementExtraEncodeData*>(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<EntityTreeElementExtraEncodeData*>(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<EntityTreeElementExtraEncodeData*>(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<EntityTreeElementExtraEncodeData*>(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<uint16_t> 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<EntityItemID>& entityIdsToInclude,
|
||||
BoxFace& face, glm::vec3& surfaceNormal, const QVector<EntityItemID>& entityIdsToInclude,
|
||||
const QVector<EntityItemID>& 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() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -135,10 +135,15 @@ public:
|
|||
static uint32 getMaxNumRenderBuffers() { return MAX_NUM_RENDER_BUFFERS; }
|
||||
|
||||
const GPUObjectPointer gpuObject {};
|
||||
|
||||
|
||||
Stamp getDepthStamp() const { return _depthStamp; }
|
||||
const std::vector<Stamp>& getColorStamps() const { return _colorStamps; }
|
||||
|
||||
protected:
|
||||
SwapchainPointer _swapchain;
|
||||
|
||||
Stamp _depthStamp { 0 };
|
||||
std::vector<Stamp> _colorStamps;
|
||||
TextureViews _renderBuffers;
|
||||
TextureView _depthStencilBuffer;
|
||||
|
||||
|
|
|
@ -173,6 +173,9 @@ public:
|
|||
public:
|
||||
GLuint _fbo = 0;
|
||||
std::vector<GLenum> _colorBuffers;
|
||||
Stamp _depthStamp { 0 };
|
||||
std::vector<Stamp> _colorStamps;
|
||||
|
||||
|
||||
GLFramebuffer();
|
||||
~GLFramebuffer();
|
||||
|
|
|
@ -27,8 +27,15 @@ GLBackend::GLFramebuffer::~GLFramebuffer() {
|
|||
GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffer) {
|
||||
GLFramebuffer* object = Backend::getGPUObject<GLBackend::GLFramebuffer>(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<GLenum> 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;
|
||||
|
|
|
@ -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<gpu::Buffer>(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 <math.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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
#ifndef hifi_model_Stage_h
|
||||
#define hifi_model_Stage_h
|
||||
|
||||
#include "gpu/Pipeline.h"
|
||||
#include <gpu/Pipeline.h>
|
||||
|
||||
#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);
|
||||
|
|
|
@ -12,10 +12,12 @@
|
|||
#include <memory>
|
||||
|
||||
#include <QtCore/QDataStream>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QMap>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include <QtCore/QUrlQuery>
|
||||
#include <QtNetwork/QHttpMultiPart>
|
||||
#include <QtNetwork/QNetworkRequest>
|
||||
|
@ -60,13 +62,12 @@ JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, co
|
|||
updateReciever(updateReceiver),
|
||||
updateSlot(updateSlot)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
AccountManager::AccountManager() :
|
||||
_authURL(),
|
||||
_pendingCallbackMap(),
|
||||
_accountInfo(),
|
||||
_shouldPersistToSettingsFile(true)
|
||||
_pendingCallbackMap()
|
||||
{
|
||||
qRegisterMetaType<OAuthAccessToken>("OAuthAccessToken");
|
||||
qRegisterMetaTypeStreamOperators<OAuthAccessToken>("OAuthAccessToken");
|
||||
|
@ -80,9 +81,6 @@ AccountManager::AccountManager() :
|
|||
qRegisterMetaType<QHttpMultiPart*>("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<DataServerAccountInfo>(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<DataServerAccountInfo>();
|
||||
|
||||
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<DataServerAccountInfo>();
|
||||
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<QVariant>(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<RSAKeypairGenerator*>(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();
|
||||
}
|
||||
|
|
|
@ -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<QNetworkReply*, JSONCallbackParameters> _pendingCallbackMap;
|
||||
|
||||
DataServerAccountInfo _accountInfo;
|
||||
bool _shouldPersistToSettingsFile;
|
||||
bool _isAgent { false };
|
||||
|
||||
bool _isWaitingForKeypairResponse { false };
|
||||
QByteArray _pendingPrivateKey;
|
||||
};
|
||||
|
||||
#endif // hifi_AccountManager_h
|
||||
|
|
|
@ -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<const unsigned char**>(&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<const unsigned char*>(usernameWithToken.constData()),
|
||||
usernameWithToken.size(),
|
||||
reinterpret_cast<unsigned char*>(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<const unsigned char**>(&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<const unsigned char*>(hashedPlaintext.constData()),
|
||||
hashedPlaintext.size(),
|
||||
reinterpret_cast<unsigned char*>(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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
};
|
||||
|
|
|
@ -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<ReceivedMessage> mes
|
|||
emit icePeerSocketsReceived();
|
||||
}
|
||||
}
|
||||
|
||||
void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<ReceivedMessage> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,6 +92,7 @@ public slots:
|
|||
void processICEPingReplyPacket(QSharedPointer<ReceivedMessage> message);
|
||||
void processDTLSRequirementPacket(QSharedPointer<ReceivedMessage> dtlsRequirementPacket);
|
||||
void processICEResponsePacket(QSharedPointer<ReceivedMessage> icePacket);
|
||||
void processDomainServerConnectionDeniedPacket(QSharedPointer<ReceivedMessage> 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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<NLPacket> constructICEPingPacket(PingType_t pingType, const QUuid& iceID);
|
||||
std::unique_ptr<NLPacket> 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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,12 +85,12 @@ void RSAKeypairGenerator::generateKeypair() {
|
|||
// we can cleanup the RSA struct before we continue on
|
||||
RSA_free(keyPair);
|
||||
|
||||
QByteArray publicKeyArray(reinterpret_cast<char*>(publicKeyDER), publicKeyLength);
|
||||
QByteArray privateKeyArray(reinterpret_cast<char*>(privateKeyDER), privateKeyLength);
|
||||
_publicKey = QByteArray { reinterpret_cast<char*>(publicKeyDER), publicKeyLength };
|
||||
_privateKey = QByteArray { reinterpret_cast<char*>(privateKeyDER), privateKeyLength };
|
||||
|
||||
// cleanup the publicKeyDER and publicKeyDER data
|
||||
OPENSSL_free(publicKeyDER);
|
||||
OPENSSL_free(privateKeyDER);
|
||||
|
||||
emit generatedKeypair(publicKeyArray, privateKeyArray);
|
||||
emit generatedKeypair();
|
||||
}
|
||||
|
|
|
@ -12,17 +12,31 @@
|
|||
#ifndef hifi_RSAKeypairGenerator_h
|
||||
#define hifi_RSAKeypairGenerator_h
|
||||
|
||||
#include <qobject.h>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QUuid>
|
||||
|
||||
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
|
||||
#endif // hifi_RSAKeypairGenerator_h
|
||||
|
|
|
@ -30,7 +30,7 @@ const QSet<PacketType> NON_SOURCED_PACKETS = QSet<PacketType>()
|
|||
<< 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<PacketVersion>(AvatarMixerPacketVersion::SoftAttachmentSupport);
|
||||
case PacketType::ICEServerHeartbeat:
|
||||
return 18; // ICE Server Heartbeat signing
|
||||
default:
|
||||
return 17;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<QString, uint16_t> _mapSourceUUIDsToKeys;
|
||||
static std::map<uint16_t, QString> _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
|
||||
|
|
|
@ -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<unsigned char*>(queryPacket->getPayload()));
|
||||
queryPacket->setPayloadSize(querySize);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
#include <NumericalConstants.h>
|
||||
|
||||
#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<int> _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 {
|
||||
|
|
|
@ -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<GeometryCache>()->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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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<QString, int>::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)));
|
||||
|
||||
|
|
|
@ -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<QString> _repeatedMessageRegexes;
|
||||
QHash<QString, int> _repeatMessageCountHash;
|
||||
QHash<QString, QString> _lastRepeatedMessage;
|
||||
|
|
Loading…
Reference in a new issue