diff --git a/assignment-client/src/octree/OctreeQueryNode.cpp b/assignment-client/src/octree/OctreeQueryNode.cpp index 78a049edd6..55eddf9e13 100644 --- a/assignment-client/src/octree/OctreeQueryNode.cpp +++ b/assignment-client/src/octree/OctreeQueryNode.cpp @@ -153,7 +153,7 @@ bool OctreeQueryNode::updateCurrentViewFrustum() { newestViewFrustum.setPosition(getCameraPosition()); newestViewFrustum.setOrientation(getCameraOrientation()); - newestViewFrustum.setKeyholeRadius(getKeyholeRadius()); + newestViewFrustum.setCenterRadius(getCameraCenterRadius()); // Also make sure it's got the correct lens details from the camera float originalFOV = getCameraFov(); diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index b5699cb3b3..ee59f4a3ac 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -271,8 +271,9 @@ FunctionEnd @CPACK_NSIS_PAGE_COMPONENTS@ + Page custom PostInstallOptionsPage ReadPostInstallOptions + !insertmacro MUI_PAGE_INSTFILES - Page custom PostInstallOptionsPage HandlePostInstallOptions !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES @@ -341,6 +342,227 @@ FunctionEnd ;Only for solid compression (by default, solid compression is enabled for BZIP2 and LZMA) ReserveFile "@POST_INSTALL_OPTIONS_PATH@" + ; Make sure nsDialogs is included before we use it + !include "nsdialogs.nsh" + +;-------------------------------- +; Post Install Options + +Var PostInstallDialog +Var DesktopClientCheckbox +Var DesktopServerCheckbox +Var ServerStartupCheckbox +Var LaunchNowCheckbox +Var CurrentOffset +Var OffsetUnits +Var CopyFromProductionCheckbox + +!macro SetPostInstallOption Checkbox OptionName Default + ; reads the value for the given post install option to the registry + ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" + + ${If} $0 == "NO" + ; the value in the registry says it should not be checked + ${NSD_SetState} ${Checkbox} ${BST_UNCHECKED} + ${ElseIf} $0 == "YES" + ; the value in the registry says it should be checked + ${NSD_SetState} ${Checkbox} ${BST_CHECKED} + ${Else} + ; the value in the registry was not in the expected format, use default + ${NSD_SetState} ${Checkbox} ${Default} + ${EndIf} +!macroend + +Function PostInstallOptionsPage + !insertmacro MUI_HEADER_TEXT "Setup Options" "" + + nsDialogs::Create 1018 + Pop $PostInstallDialog + + ${If} $PostInstallDialog == error + Abort + ${EndIf} + + StrCpy $CurrentOffset 0 + StrCpy $OffsetUnits u + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_SHORTCUT_NAME@" + Pop $DesktopClientCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED} + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for High Fidelity @CONSOLE_SHORTCUT_NAME@" + Pop $DesktopServerCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED} + + IntOp $CurrentOffset $CurrentOffset + 15 + + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity @CONSOLE_SHORTCUT_NAME@ on startup" + Pop $ServerStartupCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} + + IntOp $CurrentOffset $CurrentOffset + 15 + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Server Console after install" + ${Else} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity after install" + ${EndIf} + + Pop $LaunchNowCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} + + ${If} @PR_BUILD@ == 1 + ; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED} + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED} + ${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED} + ${EndIf} + + ; push the offset + IntOp $CurrentOffset $CurrentOffset + 15 + + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install" + Pop $CopyFromProductionCheckbox + + ${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED} + ${EndIf} + + nsDialogs::Show +FunctionEnd + +!macro WritePostInstallOption OptionName Option + ; writes the value for the given post install option to the registry + WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option} +!macroend + +Var DesktopClientState +Var DesktopServerState +Var ServerStartupState +Var LaunchNowState +Var CopyFromProductionState + +Function ReadPostInstallOptions + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to High Fidelity + ${NSD_GetState} $DesktopClientCheckbox $DesktopClientState + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to Server Console + ${NSD_GetState} $DesktopServerCheckbox $DesktopServerState + + ; check if the user asked to have Server Console launched every startup + ${NSD_GetState} $ServerStartupCheckbox $ServerStartupState + ${EndIf} + + ${If} @PR_BUILD@ == 1 + ; check if we need to copy settings/content from production for this PR build + ${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState + ${EndIf} + + ; check if we need to launch an application post-install + ${NSD_GetState} $LaunchNowCheckbox $LaunchNowState +FunctionEnd + +Function HandlePostInstallOptions + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to High Fidelity + ${If} $DesktopClientState == ${BST_CHECKED} + CreateShortCut "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@" + !insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES + ${Else} + !insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO + ${EndIf} + + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ; check if the user asked for a desktop shortcut to Server Console + ${If} $DesktopServerState == ${BST_CHECKED} + CreateShortCut "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" + !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES + ${Else} + !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO + ${EndIf} + + ; check if the user asked to have Server Console launched every startup + ${If} $ServerStartupState == ${BST_CHECKED} + ; in case we added a shortcut in the global context, pull that now + SetShellVarContext all + Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" + + ; make a startup shortcut in this user's current context + SetShellVarContext current + CreateShortCut "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" + + ; reset the shell var context back + SetShellVarContext all + + !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES + ${Else} + !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO + ${EndIf} + ${EndIf} + + ${If} @PR_BUILD@ == 1 + + ; check if we need to copy settings/content from production for this PR build + ${If} $CopyFromProductionState == ${BST_CHECKED} + SetShellVarContext current + + StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@" + + ; we need to copy whatever is in the data folder for production build to the data folder for this build + CreateDirectory $0 + + ClearErrors + + ; copy the data from production build to this PR build + CopyFiles "$APPDATA\High Fidelity\*" $0 + + ; handle an error in copying files + IfErrors 0 NoError + + MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \ + "There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually." + + NoError: + + SetShellVarContext all + ${EndIf} + ${EndIf} + + ${If} $LaunchNowState == ${BST_CHECKED} + !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES + + ; both launches use the explorer trick in case the user has elevated permissions for the installer + ; it won't be possible to use this approach if either application should be launched with a command line param + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"' + ${Else} + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"' + ${EndIf} + ${Else} + !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO + ${EndIf} +FunctionEnd ;-------------------------------- ;Installer Sections @@ -469,233 +691,11 @@ Section "-Core installation" @CPACK_NSIS_EXTRA_INSTALL_COMMANDS@ + ; Handle whichever post install options were set + Call HandlePostInstallOptions + SectionEnd -; Make sure nsDialogs is included before we use it -!include "nsdialogs.nsh" - -Var PostInstallDialog -Var OptionsLabel -Var DesktopClientCheckbox -Var DesktopServerCheckbox -Var ServerStartupCheckbox -Var LaunchNowCheckbox -Var CurrentOffset -Var OffsetUnits -Var CopyFromProductionCheckbox - -!macro SetPostInstallOption Checkbox OptionName Default - ; reads the value for the given post install option to the registry - ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" - - ${If} $0 == "NO" - ; the value in the registry says it should not be checked - ${NSD_SetState} ${Checkbox} ${BST_UNCHECKED} - ${ElseIf} $0 == "YES" - ; the value in the registry says it should be checked - ${NSD_SetState} ${Checkbox} ${BST_CHECKED} - ${Else} - ; the value in the registry was not in the expected format, use default - ${NSD_SetState} ${Checkbox} ${Default} - ${EndIf} -!macroend - -Function PostInstallOptionsPage - ; Set the text on the dialog button to match finish, hide the back and cancel buttons - GetDlgItem $R1 $hwndparent 1 - SendMessage $R1 ${WM_SETTEXT} 0 "STR:&Finish" - - GetDlgItem $R3 $hwndparent 3 - ShowWindow $R3 0 - - nsDialogs::Create 1018 - Pop $PostInstallDialog - - ${If} $PostInstallDialog == error - Abort - ${EndIf} - - ${NSD_CreateLabel} 0 0 100% 12u "Setup Options" - Pop $OptionsLabel - - ; Set label to bold - CreateFont $R2 "Arial" 10 700 - SendMessage $OptionsLabel ${WM_SETFONT} $R2 0 - - ; Force label redraw - ShowWindow $OptionsLabel ${SW_HIDE} - ShowWindow $OptionsLabel ${SW_SHOW} - - StrCpy $CurrentOffset 15 - StrCpy $OffsetUnits u - - ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_SHORTCUT_NAME@" - Pop $DesktopClientCheckbox - IntOp $CurrentOffset $CurrentOffset + 15 - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED} - ${EndIf} - - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for High Fidelity @CONSOLE_SHORTCUT_NAME@" - Pop $DesktopServerCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 - - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity @CONSOLE_SHORTCUT_NAME@ on startup" - Pop $ServerStartupCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 - ${EndIf} - - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Server Console Now" - ${Else} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Now" - ${EndIf} - - Pop $LaunchNowCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} - - ${If} @PR_BUILD@ == 1 - ; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked - ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED} - ${EndIf} - - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED} - ${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED} - ${EndIf} - - ; push the offset - IntOp $CurrentOffset $CurrentOffset + 15 - - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install" - Pop $CopyFromProductionCheckbox - - ${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED} - ${EndIf} - - nsDialogs::Show -FunctionEnd - -!macro WritePostInstallOption OptionName Option - ; writes the value for the given post install option to the registry - WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option} -!macroend - -Var DesktopClientState -Var DesktopServerState -Var ServerStartupState -Var LaunchNowState -Var CopyFromProductionState - -Function HandlePostInstallOptions - ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ; check if the user asked for a desktop shortcut to High Fidelity - ${NSD_GetState} $DesktopClientCheckbox $DesktopClientState - - ${If} $DesktopClientState == ${BST_CHECKED} - CreateShortCut "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@" - !insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES - ${Else} - !insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO - ${EndIf} - - ${EndIf} - - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ; check if the user asked for a desktop shortcut to Server Console - ${NSD_GetState} $DesktopServerCheckbox $DesktopServerState - - ${If} $DesktopServerState == ${BST_CHECKED} - CreateShortCut "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" - !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES - ${Else} - !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO - ${EndIf} - - ; check if the user asked to have Server Console launched every startup - ${NSD_GetState} $ServerStartupCheckbox $ServerStartupState - - ${If} $ServerStartupState == ${BST_CHECKED} - ; in case we added a shortcut in the global context, pull that now - SetShellVarContext all - Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" - - ; make a startup shortcut in this user's current context - SetShellVarContext current - CreateShortCut "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" - - ; reset the shell var context back - SetShellVarContext all - - !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES - ${Else} - !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO - ${EndIf} - ${EndIf} - - ${If} @PR_BUILD@ == 1 - - ; check if we need to copy settings/content from production for this PR build - ${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState - - ${If} $CopyFromProductionState == ${BST_CHECKED} - SetShellVarContext current - - StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@" - - ; we need to copy whatever is in the data folder for production build to the data folder for this build - CreateDirectory $0 - - ClearErrors - - ; copy the data from production build to this PR build - CopyFiles "$APPDATA\High Fidelity\*" $0 - - ; handle an error in copying files - IfErrors 0 NoError - - MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \ - "There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually." - - NoError: - - SetShellVarContext all - ${EndIf} - ${EndIf} - - ; check if we need to launch an application post-install - ${NSD_GetState} $LaunchNowCheckbox $LaunchNowState - - ${If} $LaunchNowState == ${BST_CHECKED} - !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES - - ; both launches use the explorer trick in case the user has elevated permissions for the installer - ; it won't be possible to use this approach if either application should be launched with a command line param - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"' - ${Else} - Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"' - ${EndIf} - ${Else} - !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO - ${EndIf} - -FunctionEnd - !include nsProcess.nsh !macro PromptForRunningApplication applicationName displayName action prompter diff --git a/examples/acScripts/entitySpawnerAC.js b/examples/acScripts/entitySpawnerAC.js index 8fb2e259bb..2baf732258 100644 --- a/examples/acScripts/entitySpawnerAC.js +++ b/examples/acScripts/entitySpawnerAC.js @@ -95,9 +95,9 @@ EntityViewer.setPosition({ y: 0, z: 0 }); -EntityViewer.setKeyholeRadius(60000); +EntityViewer.setCenterRadius(60000); var octreeQueryInterval = Script.setInterval(function() { EntityViewer.queryOctree(); }, 1000); -Script.update.connect(update); \ No newline at end of file +Script.update.connect(update); diff --git a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js index f7e983d683..de7baba267 100644 --- a/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js +++ b/examples/audioExamples/acAudioSearching/ACAudioSearchAndInject.js @@ -1,218 +1,260 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true*/ +/*global AvatarList, Entities, EntityViewer, Script, SoundCache, Audio, print, randFloat*/ // // ACAudioSearchAndInject.js // audio // -// Created by Eric Levin 2/1/2016 +// Created by Eric Levin and Howard Stearns 2/1/2016 // Copyright 2016 High Fidelity, Inc. - -// This AC script searches for special sound entities nearby avatars and plays those sounds based off information specified in the entity's -// user data field ( see acAudioSearchAndCompatibilityEntitySpawner.js for an example) +// +// Keeps track of all sounds within QUERY_RADIUS of an avatar, where a "sound" is specified in entity userData. +// Inject as many as practical into the audio mixer. +// See acAudioSearchAndCompatibilityEntitySpawner.js. +// +// This implementation takes some precautions to scale well: +// - It doesn't hastle the entity server because it issues at most one octree query every UPDATE_TIME period, regardless of the number of avatars. +// - It does not load itself because it only gathers entities once every UPDATE_TIME period, and only +// checks entity properties for those small number of entities that are currently playing (plus a RECHECK_TIME period examination of all entities). +// This implementation tries to use all the available injectors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -Script.include("https://rawgit.com/highfidelity/hifi/master/examples/libraries/utils.js"); - -var SOUND_DATA_KEY = "soundKey"; - -var QUERY_RADIUS = 50; - -EntityViewer.setKeyholeRadius(QUERY_RADIUS); -Entities.setPacketsPerSecond(6000); - -Agent.isAvatar = true; +var MSEC_PER_SEC = 1000; +var SOUND_DATA_KEY = "io.highfidelity.soundKey"; // Sound data is specified in userData under this key. +var old_sound_data_key = "soundKey"; // For backwards compatibility. +var QUERY_RADIUS = 50; // meters +var UPDATE_TIME = 100; // ms. We'll update just one thing on this period. +var EXPIRATION_TIME = 5 * MSEC_PER_SEC; // ms. Remove sounds that have been out of range for this time. +var RECHECK_TIME = 10 * MSEC_PER_SEC; // ms. Check for new userData properties this often when not currently playing. +// (By not checking most of the time when not playing, we can efficiently go through all entities without getEntityProperties.) +var UPDATES_PER_STATS_LOG = RECHECK_TIME / UPDATE_TIME; // (It's nice to smooth out the results by straddling a recheck.) var DEFAULT_SOUND_DATA = { - volume: 0.5, - loop: false, - playbackGap: 1000, // in ms + volume: 0.5, // userData cannot specify zero volume with our current method of defaulting. + loop: false, // Default must be false with our current method of defaulting, else there's no way to get a false value. + playbackGap: MSEC_PER_SEC, // in ms playbackGapRange: 0 // in ms }; -var MIN_PLAYBACK_GAP = 0; -var UPDATE_TIME = 100; -var EXPIRATION_TIME = 5000; +Script.include("../../libraries/utils.js"); +Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList. +function ignore() {} +function debug() { // Display the arguments not just [Object object]. + //print.apply(null, [].map.call(arguments, JSON.stringify)); +} -var soundEntityMap = {}; -var soundUrls = {}; +EntityViewer.setCenterRadius(QUERY_RADIUS); -var avatarPositions = []; - - -function update() { - var avatars = AvatarList.getAvatarIdentifiers(); - for (var i = 0; i < avatars.length; i++) { - var avatar = AvatarList.getAvatar(avatars[i]); - var avatarPosition = avatar.position; - if (!avatarPosition) { - continue; +// ENTITY DATA CACHE +// +var entityCache = {}; // A dictionary of unexpired EntityData objects. +var examinationCount = 0; +function EntityDatum(entityIdentifier) { // Just the data of an entity that we need to know about. + // This data is only use for our sound injection. There is no need to store such info in the replicated entity on everyone's computer. + var that = this; + that.lastUserDataUpdate = 0; // new entity is in need of rechecking user data + // State Transitions: + // no data => no data | sound data | expired + // expired => stop => remove + // sound data => downloading + // downloading => downloading | waiting + // waiting => playing | waiting (if too many already playing) + // playing => update position etc | no data + that.stop = function stop() { + if (!that.sound) { + return; } - EntityViewer.setPosition(avatarPosition); - EntityViewer.queryOctree(); - avatarPositions.push(avatarPosition); + print("stopping sound", entityIdentifier, that.url); + delete that.sound; + delete that.url; + if (!that.injector) { + return; + } + that.injector.stop(); + delete that.injector; + }; + this.update = function stateTransitions(expirationCutoff, userDataCutoff, now) { + if (that.timestamp < expirationCutoff) { // EXPIRED => STOP => REMOVE + that.stop(); // Alternatively, we could fade out and then stop... + delete entityCache[entityIdentifier]; + return; + } + var properties, soundData; // Latest data, pulled from local octree. + // getEntityProperties locks the tree, which competes with the asynchronous processing of queryOctree results. + // Most entity updates are fast and only a very few do getEntityProperties. + function ensureSoundData() { // We only getEntityProperities when we need to. + if (properties) { + return; + } + properties = Entities.getEntityProperties(entityIdentifier, ['userData', 'position']); + examinationCount++; // Collect statistics on how many getEntityProperties we do. + debug("updating", that, properties); + try { + var userData = properties.userData && JSON.parse(properties.userData); + soundData = userData && (userData[SOUND_DATA_KEY] || userData[old_sound_data_key]); // Don't store soundData yet. Let state changes compare. + that.lastUserDataUpdate = now; // But do update these ... + that.url = soundData && soundData.url; + that.playAfter = that.url && now; + } catch (err) { + print(err, properties.userData); + } + } + // Stumbling on big new pile of entities will do a lot of getEntityProperties. Once. + if (that.lastUserDataUpdate < userDataCutoff) { // NO DATA => SOUND DATA + ensureSoundData(); + } + if (!that.url) { // NO DATA => NO DATA + return that.stop(); + } + if (!that.sound) { // SOUND DATA => DOWNLOADING + that.sound = SoundCache.getSound(soundData.url); // SoundCache can manage duplicates better than we can. + } + if (!that.sound.downloaded) { // DOWNLOADING => DOWNLOADING + return; + } + if (that.playAfter > now) { // DOWNLOADING | WAITING => WAITING + return; + } + ensureSoundData(); // We'll try to play/setOptions and will need position, so we might as well get soundData, too. + if (soundData.url !== that.url) { // WAITING => NO DATA (update next time around) + return that.stop(); + } + var options = { + position: properties.position, + loop: soundData.loop || DEFAULT_SOUND_DATA.loop, + volume: soundData.volume || DEFAULT_SOUND_DATA.volume + }; + function repeat() { + return !options.loop && (soundData.playbackGap >= 0); + } + function randomizedNextPlay() { // time of next play or recheck, randomized to distribute the work + var range = soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange, + base = repeat() ? ((that.sound.duration * MSEC_PER_SEC) + (soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap)) : RECHECK_TIME; + return now + base + randFloat(-Math.min(base, range), range); + } + if (!that.injector) { // WAITING => PLAYING | WAITING + debug("starting", that, options); + that.injector = Audio.playSound(that.sound, options); // Might be null if at at injector limit. Will try again later. + if (that.injector) { + print("started", entityIdentifier, that.url); + } else { // Don't hammer ensureSoundData or injector manager. + that.playAfter = randomizedNextPlay(); + } + return; + } + that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC + if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap. + if (repeat()) { // WAITING => PLAYING + // Setup next play just once, now. Changes won't be looked at while we wait. + that.playAfter = randomizedNextPlay(); + // Subtle: if the restart fails b/c we're at injector limit, we won't try again until next playAfter. + that.injector.restart(); + } else { // PLAYING => NO DATA + that.playAfter = Infinity; // was one-shot and we're finished + } + } + }; +} +function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar) { + ignore(avatarPosition, avatar); // We could use avatars and/or avatarPositions to prioritize which ones to play. + var entitySound = entityCache[entityIdentifier]; + if (!entitySound) { + entitySound = entityCache[entityIdentifier] = new EntityDatum(entityIdentifier); } - Script.setTimeout(function() { - avatarPositions.forEach(function(avatarPosition) { - var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS); - handleFoundSoundEntities(entities); + entitySound.timestamp = timestamp; // Might be updated for multiple avatars. That's fine. +} +var nUpdates = UPDATES_PER_STATS_LOG, lastStats = Date.now(); +function updateAllEntityData() { // A fast update of all entities we know about. A few make sounds. + var now = Date.now(), + expirationCutoff = now - EXPIRATION_TIME, + userDataRecheckCutoff = now - RECHECK_TIME; + Object.keys(entityCache).forEach(function (entityIdentifier) { + entityCache[entityIdentifier].update(expirationCutoff, userDataRecheckCutoff, now); + }); + if (nUpdates-- <= 0) { // Report statistics. + // For example, with: + // injector-limit = 40 (in C++ code) + // N_SOUNDS = 1000 (from userData in, e.g., acAudioSearchCompatibleEntitySpawner.js) + // replay-period = 3 + 20 = 23 (seconds, ditto) + // stats-period = UPDATES_PER_STATS_LOG * UPDATE_TIME / MSEC_PER_SEC = 10 seconds + // The log should show between each stats report: + // "start" lines ~= injector-limit * P(finish) = injector-limit * stats-period/replay-period = 17 ? + // total attempts at starting ("start" lines + "could not thread" lines) ~= N_SOUNDS = 1000 ? + // entities > N_SOUNDS * (1+ N_SILENT_ENTITIES_PER_SOUND) = 11000 + whatever was in the scene before running spawner + // sounds = N_SOUNDS = 1000 + // getEntityPropertiesPerUpdate ~= playing + failed-starts/UPDATES_PER_STATS_LOG + other-rechecks-each-update + // = injector-limit + (total attempts - "start" lines)/UPDATES_PER_STATS__LOG + // + (entities - playing - failed-starts/UPDATES_PER_STATS_LOG) * P(recheck-in-update) + // where failed-starts/UPDATES_PER_STATS_LOG = (1000-17)/100 = 10 + // = 40 + 10 + (11000 - 40 - 10)*UPDATE_TIME/RECHECK_TIME + // = 40 + 10 + 10950*0.01 = 159 (mostly proportional to enties/RECHECK_TIME) + // millisecondsPerUpdate ~= UPDATE_TIME = 100 (+ some timer machinery time) + // this assignment client activity monitor < 100% cpu + var stats = { + entities: 0, + sounds: 0, + playing: 0, + getEntityPropertiesPerUpdate: examinationCount / UPDATES_PER_STATS_LOG, + millisecondsPerUpdate: (now - lastStats) / UPDATES_PER_STATS_LOG + }; + nUpdates = UPDATES_PER_STATS_LOG; + lastStats = now; + examinationCount = 0; + Object.keys(entityCache).forEach(function (entityIdentifier) { + var datum = entityCache[entityIdentifier]; + stats.entities++; + if (datum.url) { + stats.sounds++; + if (datum.injector && datum.injector.isPlaying) { + stats.playing++; + } + } }); - //Now wipe list for next query; - avatarPositions = []; - }, UPDATE_TIME); - handleActiveSoundEntities(); -} - -function handleActiveSoundEntities() { - // Go through all our sound entities, if they have passed expiration time, remove them from map - for (var potentialSoundEntity in soundEntityMap) { - if (!soundEntityMap.hasOwnProperty(potentialSoundEntity)) { - // The current property is not a direct property of soundEntityMap so ignore it - continue; - } - var soundEntity = potentialSoundEntity; - var soundProperties = soundEntityMap[soundEntity]; - soundProperties.timeWithoutAvatarInRange += UPDATE_TIME; - if (soundProperties.timeWithoutAvatarInRange > EXPIRATION_TIME && soundProperties.soundInjector) { - // An avatar hasn't been within range of this sound entity recently, so remove it from map - soundProperties.soundInjector.stop(); - delete soundEntityMap[soundEntity]; - } else if (soundProperties.isDownloaded) { - // If this sound hasn't expired yet, we want to potentially play it! - if (soundProperties.readyToPlay) { - var newPosition = Entities.getEntityProperties(soundEntity, "position").position; - if (!soundProperties.soundInjector) { - soundProperties.soundInjector = Audio.playSound(soundProperties.sound, { - volume: soundProperties.volume, - position: newPosition, - loop: soundProperties.loop - }); - } else { - soundProperties.soundInjector.restart(); - } - soundProperties.readyToPlay = false; - } else if (soundProperties.sound && soundProperties.loop === false) { - // We need to check all of our entities that are not looping but have an interval associated with them - // to see if it's time for them to play again - soundProperties.timeSinceLastPlay += UPDATE_TIME; - if (soundProperties.timeSinceLastPlay > soundProperties.clipDuration + soundProperties.currentPlaybackGap) { - soundProperties.readyToPlay = true; - soundProperties.timeSinceLastPlay = 0; - // Now let's get our new current interval - soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange); - soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap); - } - } - } + print(JSON.stringify(stats)); } } - -function handleFoundSoundEntities(entities) { - entities.forEach(function(entity) { - var soundData = getEntityCustomData(SOUND_DATA_KEY, entity); - if (soundData && soundData.url) { - //check sound entities list- if it's not in, add it - if (!soundEntityMap[entity]) { - var soundProperties = { - url: soundData.url, - volume: soundData.volume || DEFAULT_SOUND_DATA.volume, - loop: soundData.loop || DEFAULT_SOUND_DATA.loop, - playbackGap: soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap, - playbackGapRange: soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange, - readyToPlay: false, - position: Entities.getEntityProperties(entity, "position").position, - timeSinceLastPlay: 0, - timeWithoutAvatarInRange: 0, - isDownloaded: false - }; - - - soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange); - soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap); - - - soundEntityMap[entity] = soundProperties; - if (!soundUrls[soundData.url]) { - // We need to download sound before we add it to our map - var sound = SoundCache.getSound(soundData.url); - // Only add it to map once it's downloaded - soundUrls[soundData.url] = sound; - sound.ready.connect(function() { - soundProperties.sound = sound; - soundProperties.readyToPlay = true; - soundProperties.isDownloaded = true; - soundProperties.clipDuration = sound.duration * 1000; - soundEntityMap[entity] = soundProperties; - - }); - } else { - // We already have sound downloaded, so just add it to map right away - soundProperties.sound = soundUrls[soundData.url]; - soundProperties.clipDuration = soundProperties.sound.duration * 1000; - soundProperties.readyToPlay = true; - soundProperties.isDownloaded = true; - soundEntityMap[entity] = soundProperties; - } - } else { - //If this sound is in our map already, we want to reset timeWithoutAvatarInRange - // Also we want to check to see if the entity has been updated with new sound data- if so we want to update! - soundEntityMap[entity].timeWithoutAvatarInRange = 0; - checkForSoundPropertyChanges(soundEntityMap[entity], soundData); - } - } +// Update the set of which EntityData we know about. +// +function updateEntiesForAvatar(avatarIdentifier) { // Just one piece of update work. + // This does at most: + // one queryOctree request of the entity server, and + // one findEntities geometry query of our own octree, and + // a quick internEntityDatum of each of what may be a large number of entityIdentifiers. + // The idea is that this is a nice bounded piece of work that should not be done too frequently. + // However, it means that we won't learn about new entities until, on average (nAvatars * UPDATE_TIME) + query round trip. + var avatar = AvatarList.getAvatar(avatarIdentifier), avatarPosition = avatar && avatar.position; + if (!avatarPosition) { // No longer here. + return; + } + var timestamp = Date.now(); + EntityViewer.setPosition(avatarPosition); + EntityViewer.queryOctree(); // Requests an update, but there's no telling when we'll actually see different results. + var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS); + debug("found", entities.length, "entities near", avatar.name || "unknown", "at", avatarPosition); + entities.forEach(function (entityIdentifier) { + internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar); }); } -function checkForSoundPropertyChanges(currentProps, newProps) { - var needsNewInjector = false; - - if (currentProps.playbackGap !== newProps.playbackGap && !currentProps.loop) { - // playbackGap only applies to non looping sounds - currentProps.playbackGap = newProps.playbackGap; - currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange); - currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap); - currentProps.readyToPlay = true; - } - - if (currentProps.playbackGapRange !== currentProps.playbackGapRange) { - currentProps.playbackGapRange = newProps.playbackGapRange; - currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange); - currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap); - currentProps.readyToPlay = true; - } - if (currentProps.volume !== newProps.volume) { - currentProps.volume = newProps.volume; - needsNewInjector = true; - } - if (currentProps.url !== newProps.url) { - currentProps.url = newProps.url; - currentProps.sound = null; - if (!soundUrls[currentProps.url]) { - var sound = SoundCache.getSound(currentProps.url); - currentProps.isDownloaded = false; - sound.ready.connect(function() { - currentProps.sound = sound; - currentProps.clipDuration = sound.duration * 1000; - currentProps.isDownloaded = true; - }); - } else { - currentProps.sound = sound; - currentProps.clipDuration = sound.duration * 1000; - } - needsNewInjector = true; - } - - if (currentProps.loop !== newProps.loop) { - currentProps.loop = newProps.loop; - needsNewInjector = true; - } - if (needsNewInjector) { - // If we were looping we need to stop that so new changes are applied - currentProps.soundInjector.stop(); - currentProps.soundInjector = null; - currentProps.readyToPlay = true; - } - +// Slowly update the set of data we have to work with. +// +var workQueue = []; +function updateWorkQueueForAvatarsPresent() { // when nothing else to do, fill queue with individual avatar updates + workQueue = AvatarList.getAvatarIdentifiers().map(function (avatarIdentifier) { + return function () { + updateEntiesForAvatar(avatarIdentifier); + }; + }); } - -Script.setInterval(update, UPDATE_TIME); +Script.setInterval(function () { + // There might be thousands of EntityData known to us, but only a few will require any work to update. + updateAllEntityData(); // i.e., this better be pretty fast. + // Each interval, we do no more than one updateEntitiesforAvatar. + if (!workQueue.length) { + workQueue = [updateWorkQueueForAvatarsPresent]; + } + workQueue.pop()(); // There's always one +}, UPDATE_TIME); diff --git a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js index 126635ee7a..2a80a712b6 100644 --- a/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js +++ b/examples/audioExamples/acAudioSearching/acAudioSearchCompatibleEntitySpawner.js @@ -1,4 +1,6 @@ -// +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true*/ +/*global Entities, Script, Quat, Vec3, Camera, MyAvatar, print, randFloat*/ // acAudioSearchCompatibleEntitySpawner.js // audio/acAudioSearching // @@ -13,6 +15,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +var N_SOUNDS = 1000; +var N_SILENT_ENTITIES_PER_SOUND = 10; +var ADD_PERIOD = 50; // ms between adding 1 sound + N_SILENT_ENTITIES_PER_SOUND, to not overrun entity server. +var SPATIAL_DISTRIBUTION = 10; // meters spread over how far to randomly distribute enties. Script.include("../../libraries/utils.js"); var orientation = Camera.getOrientation(); orientation = Quat.safeEulerAngles(orientation); @@ -20,20 +26,21 @@ orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); // http://hifi-public.s3.amazonaws.com/ryan/demo/0619_Fireplace__Tree_B.L.wav -var SOUND_DATA_KEY = "soundKey"; +var SOUND_DATA_KEY = "io.highfidelity.soundKey"; var userData = { soundKey: { url: "http://hifi-content.s3.amazonaws.com/DomainContent/Junkyard/Sounds/ClothSail/cloth_sail3.L.wav", volume: 0.3, loop: false, - playbackGap: 2000, // In ms - time to wait in between clip plays - playbackGapRange: 500 // In ms - the range to wait in between clip plays + playbackGap: 20000, // In ms - time to wait in between clip plays + playbackGapRange: 5000 // In ms - the range to wait in between clip plays } -} +}; +var userDataString = JSON.stringify(userData); var entityProps = { type: "Box", - position: center, + name: 'audioSearchEntity', color: { red: 200, green: 10, @@ -43,15 +50,41 @@ var entityProps = { x: 0.1, y: 0.1, z: 0.1 - }, - userData: JSON.stringify(userData) + } +}; + +var entities = [], nSounds = 0; +Script.include("../../libraries/utils.js"); +function addOneSet() { + function randomizeDimension(coordinate) { + return coordinate + randFloat(-SPATIAL_DISTRIBUTION / 2, SPATIAL_DISTRIBUTION / 2); + } + function randomize() { + return {x: randomizeDimension(center.x), y: randomizeDimension(center.y), z: randomizeDimension(center.z)}; + } + function addOne() { + entityProps.position = randomize(); + entities.push(Entities.addEntity(entityProps)); + } + var i; + entityProps.userData = userDataString; + entityProps.color.red = 200; + addOne(); + delete entityProps.userData; + entityProps.color.red = 10; + for (i = 0; i < N_SILENT_ENTITIES_PER_SOUND; i++) { + addOne(); + } + if (++nSounds < N_SOUNDS) { + Script.setTimeout(addOneSet, ADD_PERIOD); + } } - -var soundEntity = Entities.addEntity(entityProps); - +addOneSet(); function cleanup() { - Entities.deleteEntity(soundEntity); + entities.forEach(Entities.deleteEntity); } +// In console: +// Entities.findEntities(MyAvatar.position, 100).forEach(function (id) { if (Entities.getEntityProperties(id).name === 'audioSearchEntity') Entities.deleteEntity(id); }) -Script.scriptEnding.connect(cleanup); \ No newline at end of file +Script.scriptEnding.connect(cleanup); diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index f2acfb9b47..04e93334cb 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -171,9 +171,8 @@ 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; - +var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC; +var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC; function stateToName(state) { switch (state) { diff --git a/examples/controllers/leap/leapHands.js b/examples/controllers/leap/leapHands.js index a77fa44e1c..1be0b1e5f6 100644 --- a/examples/controllers/leap/leapHands.js +++ b/examples/controllers/leap/leapHands.js @@ -11,6 +11,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +var leftTriggerValue = 0; +var rightTriggerValue = 0; + +var LEAP_TRIGGER_START_ANGLE = 15.0; +var LEAP_TRIGGER_END_ANGLE = 40.0; + +function getLeapMotionLeftTrigger() { + //print("left trigger = " + leftTriggerValue); + return leftTriggerValue; +} +function getLeapMotionRightTrigger() { + //print("right trigger = " + rightTriggerValue); + return rightTriggerValue; +} + var leapHands = (function () { var isOnHMD, @@ -61,78 +76,6 @@ var leapHands = (function () { print(i + ": " + jointNames[i]); } print("... skeleton joint names"); - - /* - http://public.highfidelity.io/models/skeletons/ron_standing.fst - Skeleton joint names ... - 0: Hips - 1: RightUpLeg - 2: RightLeg - 3: RightFoot - 4: RightToeBase - 5: RightToe_End - 6: LeftUpLeg - 7: LeftLeg - 8: LeftFoot - 9: LeftToeBase - 10: LeftToe_End - 11: Spine - 12: Spine1 - 13: Spine2 - 14: RightShoulder - 15: RightArm - 16: RightForeArm - 17: RightHand - 18: RightHandPinky1 - 19: RightHandPinky2 - 20: RightHandPinky3 - 21: RightHandPinky4 - 22: RightHandRing1 - 23: RightHandRing2 - 24: RightHandRing3 - 25: RightHandRing4 - 26: RightHandMiddle1 - 27: RightHandMiddle2 - 28: RightHandMiddle3 - 29: RightHandMiddle4 - 30: RightHandIndex1 - 31: RightHandIndex2 - 32: RightHandIndex3 - 33: RightHandIndex4 - 34: RightHandThumb1 - 35: RightHandThumb2 - 36: RightHandThumb3 - 37: RightHandThumb4 - 38: LeftShoulder - 39: LeftArm - 40: LeftForeArm - 41: LeftHand - 42: LeftHandPinky1 - 43: LeftHandPinky2 - 44: LeftHandPinky3 - 45: LeftHandPinky4 - 46: LeftHandRing1 - 47: LeftHandRing2 - 48: LeftHandRing3 - 49: LeftHandRing4 - 50: LeftHandMiddle1 - 51: LeftHandMiddle2 - 52: LeftHandMiddle3 - 53: LeftHandMiddle4 - 54: LeftHandIndex1 - 55: LeftHandIndex2 - 56: LeftHandIndex3 - 57: LeftHandIndex4 - 58: LeftHandThumb1 - 59: LeftHandThumb2 - 60: LeftHandThumb3 - 61: LeftHandThumb4 - 62: Neck - 63: Head - 64: HeadTop_End - 65: body - ... skeleton joint names - */ } function animateLeftHand() { @@ -357,6 +300,13 @@ var leapHands = (function () { settingsTimer = Script.setInterval(checkSettings, 2000); calibrationStatus = UNCALIBRATED; + + { + var mapping = Controller.newMapping("LeapmotionTrigger"); + mapping.from(getLeapMotionLeftTrigger).to(Controller.Standard.LT); + mapping.from(getLeapMotionRightTrigger).to(Controller.Standard.RT); + mapping.enable(); + } } function moveHands() { @@ -469,10 +419,17 @@ var leapHands = (function () { hands[h].rotation = handRotation; // Set finger joints ... + var summed = 0; + var closeAngle = 0; for (i = 0; i < NUM_FINGERS; i += 1) { for (j = 0; j < NUM_FINGER_JOINTS; j += 1) { if (fingers[h][i][j].controller !== null) { locRotation = fingers[h][i][j].controller.getLocRotation(); + var eulers = Quat.safeEulerAngles(locRotation); + closeAngle += eulers.x; + + summed++; + if (i === THUMB) { locRotation = { x: side * locRotation.y, @@ -496,8 +453,21 @@ var leapHands = (function () { } } } - + hands[h].inactiveCount = 0; + if (summed > 0) { + closeAngle /= summed; + } + + var triggerValue = (-closeAngle - LEAP_TRIGGER_START_ANGLE) / (LEAP_TRIGGER_END_ANGLE - LEAP_TRIGGER_START_ANGLE); + triggerValue = Math.max(0.0, Math.min(triggerValue, 1.0)); + + if (h == 0) { + leftTriggerValue = triggerValue; + } else { + rightTriggerValue = triggerValue; + + } } else { @@ -509,6 +479,8 @@ var leapHands = (function () { if (handAnimationStateHandlers[h] !== null) { MyAvatar.removeAnimationStateHandler(handAnimationStateHandlers[h]); handAnimationStateHandlers[h] = null; + leftTriggerValue = 0.0; + rightTriggerValue = 0.0; } } } diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index e3ad77870d..ccbbc5557a 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -72,10 +72,13 @@ }; } - function createEmitNumberPropertyUpdateFunction(propertyName) { + function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { + decimals = decimals == undefined ? 4 : decimals; return function() { + var value = parseFloat(this.value).toFixed(decimals); + EventBridge.emitWebEvent( - '{ "type":"update", "properties":{"' + propertyName + '":' + parseFloat(this.value).toFixed(4) + '}}' + '{ "type":"update", "properties":{"' + propertyName + '":' + value + '}}' ); }; } @@ -323,6 +326,7 @@ var elLightColorBlue = document.getElementById("property-light-color-blue"); var elLightIntensity = document.getElementById("property-light-intensity"); + var elLightFalloffRadius = document.getElementById("property-light-falloff-radius"); var elLightExponent = document.getElementById("property-light-exponent"); var elLightCutoff = document.getElementById("property-light-cutoff"); @@ -604,9 +608,10 @@ elLightColorGreen.value = properties.color.green; elLightColorBlue.value = properties.color.blue; - elLightIntensity.value = properties.intensity; - elLightExponent.value = properties.exponent; - elLightCutoff.value = properties.cutoff; + elLightIntensity.value = properties.intensity.toFixed(1); + elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); + elLightExponent.value = properties.exponent.toFixed(2); + elLightCutoff.value = properties.cutoff.toFixed(2); } else if (properties.type == "Zone") { for (var i = 0; i < elZoneSections.length; i++) { elZoneSections[i].style.display = 'block'; @@ -795,9 +800,10 @@ } }) - elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity')); - elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent')); - elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff')); + elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1)); + elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1)); + elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2)); + elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2)); elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); @@ -1043,25 +1049,25 @@
Line Height
- +
Text Color
-
R
-
G
-
B
+
R
+
G
+
B
Background Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1085,32 +1091,32 @@
Light Color
-
R
-
G
-
B
+
R
+
G
+
B
Light Intensity
- +
Light Direction
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
Ambient Intensity
- +
@@ -1129,19 +1135,19 @@
Stage Latitude
- +
Stage Longitude
- +
Stage Altitude
- +
@@ -1155,13 +1161,13 @@
Stage Day
- +
Stage Hour
- +
@@ -1186,9 +1192,9 @@
Skybox Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1264,23 +1270,23 @@
Registration
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Dimensions
-
X
-
Y
-
Z
+
X
+
Y
+
Z
- % + %
@@ -1291,9 +1297,9 @@
Voxel Volume Size
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Surface Extractor
@@ -1325,9 +1331,9 @@
Rotation
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
@@ -1339,66 +1345,66 @@
Linear Velocity
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Linear Damping
- +
Angular Velocity
-
Pitch
-
Yaw
-
Roll
+
Pitch
+
Yaw
+
Roll
Angular Damping
- +
Restitution
- +
Friction
- +
Gravity
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Acceleration
-
X
-
Y
-
Z
+
X
+
Y
+
Z
Density
- +
@@ -1406,9 +1412,9 @@
Color
-
R
-
G
-
B
+
R
+
G
+
B
@@ -1483,7 +1489,7 @@
Lifetime
- +
@@ -1541,25 +1547,25 @@
Animation FPS
- +
Animation Frame
- +
Animation First Frame
- +
Animation Last Frame
- +
@@ -1591,37 +1597,43 @@
+
+
Color
+
+
+
R
+
G
+
B
+
+
+
+
Intensity
+
+ +
+
+
+
Falloff Radius
+
+ +
+
Spot Light
-
-
Color
-
-
-
R
-
G
-
B
-
-
-
-
Intensity
-
- -
-
Spot Light Exponent
- +
Spot Light Cutoff (degrees)
- +
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 29c8e136af..f286085092 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -425,11 +425,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _maxOctreePPS(maxOctreePacketsPerSecond.get()), _lastFaceTrackerUpdate(0) { - // FIXME this may be excessivly conservative. On the other hand + // FIXME this may be excessivly conservative. On the other hand // maybe I'm used to having an 8-core machine - // Perhaps find the ideal thread count and subtract 2 or 3 + // Perhaps find the ideal thread count and subtract 2 or 3 // (main thread, present thread, random OS load) - // More threads == faster concurrent loads, but also more concurrent + // More threads == faster concurrent loads, but also more concurrent // load on the GPU until we can serialize GPU transfers (off the main thread) QThreadPool::globalInstance()->setMaxThreadCount(2); thread()->setPriority(QThread::HighPriority); @@ -560,7 +560,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : auto discoverabilityManager = DependencyManager::get(); connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); - connect(&locationUpdateTimer, &QTimer::timeout, + connect(&locationUpdateTimer, &QTimer::timeout, DependencyManager::get().data(), &AddressManager::storeCurrentAddress); locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); @@ -604,7 +604,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); - + // Save avatar location immediately after a teleport. connect(getMyAvatar(), &MyAvatar::positionGoneTo, DependencyManager::get().data(), &AddressManager::storeCurrentAddress); @@ -625,7 +625,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : getEntities()->reloadEntityScripts(); }, Qt::QueuedConnection); - connect(scriptEngines, &ScriptEngines::scriptLoadError, + connect(scriptEngines, &ScriptEngines::scriptLoadError, scriptEngines, [](const QString& filename, const QString& error){ OffscreenUi::warning(nullptr, "Error Loading Script", filename + " failed to load."); }, Qt::QueuedConnection); @@ -975,7 +975,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); } @@ -1023,7 +1023,7 @@ void Application::cleanupBeforeQuit() { getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts DependencyManager::get()->saveScripts(); DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts - DependencyManager::destroy(); + DependencyManager::destroy(); // first stop all timers directly or by invokeMethod // depending on what thread they run in @@ -1212,10 +1212,10 @@ void Application::initializeUi() { setupPreferences(); - // For some reason there is already an "Application" object in the QML context, + // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("SnapshotUploader", new SnapshotUploader()); - rootContext->setContextProperty("ApplicationInterface", this); + rootContext->setContextProperty("ApplicationInterface", this); rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); rootContext->setContextProperty("Controller", DependencyManager::get().data()); @@ -2435,7 +2435,7 @@ void Application::idle(uint64_t now) { if (_aboutToQuit) { return; // bail early, nothing to do here. } - + 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 @@ -2459,7 +2459,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 @@ -2469,14 +2469,14 @@ void Application::idle(uint64_t now) { // But when we DO request a paint, get to it as soon as possible: high priority postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); } - } - - // For the rest of idle, we want to cap at the max sim rate, so we might not call + } + + // For the rest of idle, we want to cap at the max sim rate, so we might not call // the remaining idle work every paint frame, or vice versa // In theory this means we could call idle processing more often than painting, // but in practice, when the paintGL calls aren't keeping up, there's no room left // in the main thread to call idle more often than paint. - // This check is mostly to keep idle from burning up CPU cycles by running at + // This check is mostly to keep idle from burning up CPU cycles by running at // hundreds of idles per second when the rendering is that fast if ((timeSinceLastUpdateUs / USECS_PER_MSEC) < CAPPED_SIM_FRAME_PERIOD_MS) { // No paint this round, but might be time for a new idle, otherwise return @@ -3387,7 +3387,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node _octreeQuery.setCameraNearClip(_viewFrustum.getNearClip()); _octreeQuery.setCameraFarClip(_viewFrustum.getFarClip()); _octreeQuery.setCameraEyeOffsetPosition(glm::vec3()); - _octreeQuery.setKeyholeRadius(_viewFrustum.getKeyholeRadius()); + _octreeQuery.setCameraCenterRadius(_viewFrustum.getCenterRadius()); auto lodManager = DependencyManager::get(); _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); @@ -3423,9 +3423,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); - - if (serverFrustumLocation != ViewFrustum::OUTSIDE) { + if (_viewFrustum.cubeIntersectsKeyhole(serverBounds)) { inViewServers++; } } @@ -3491,12 +3489,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node rootDetails.s * TREE_SCALE); - ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds); - if (serverFrustumLocation != ViewFrustum::OUTSIDE) { - inView = true; - } else { - inView = false; - } + inView = _viewFrustum.cubeIntersectsKeyhole(serverBounds); } else { if (wantExtraDebugging) { qCDebug(interfaceapp) << "Jurisdiction without RootCode for node " << *node << ". That's unusual!"; @@ -4752,7 +4745,7 @@ static void addDisplayPluginToMenu(DisplayPluginPointer displayPlugin, bool acti groupingMenu = "Developer"; break; default: - groupingMenu = "Standard"; + groupingMenu = "Standard"; break; } diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ee0ef21ae0..83351d5188 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -136,7 +136,7 @@ glm::quat Avatar::getWorldAlignedOrientation () const { AABox Avatar::getBounds() const { // Our skeleton models are rigged, and this method call safely produces the static bounds of the model. - // Except, that getPartBounds produces an infinite, uncentered bounding box when the model is not yet parsed, + // Except, that getPartBounds produces an infinite, uncentered bounding box when the model is not yet parsed, // and we want a centered one. NOTE: There is code that may never try to render, and thus never load and get the // real model bounds, if this is unrealistically small. if (!_skeletonModel.isRenderable()) { @@ -188,15 +188,14 @@ void Avatar::simulate(float deltaTime) { // simple frustum check float boundingRadius = getBoundingRadius(); - bool inViewFrustum = qApp->getViewFrustum()->sphereInFrustum(getPosition(), boundingRadius) != - ViewFrustum::OUTSIDE; + bool inView = qApp->getViewFrustum()->sphereIntersectsFrustum(getPosition(), boundingRadius); { PerformanceTimer perfTimer("hand"); getHand()->simulate(deltaTime, false); } - if (_shouldAnimate && !_shouldSkipRender && inViewFrustum) { + if (_shouldAnimate && !_shouldSkipRender && inView) { { PerformanceTimer perfTimer("skeleton"); _skeletonModel.getRig()->copyJointsFromJointData(_jointData); @@ -401,7 +400,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { frustum = qApp->getDisplayViewFrustum(); } - if (frustum->sphereInFrustum(getPosition(), boundingRadius) == ViewFrustum::OUTSIDE) { + if (frustum->sphereIntersectsFrustum(getPosition(), boundingRadius)) { endRender(); return; } @@ -430,6 +429,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 +438,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { foreach (const AvatarManager::LocalLight& light, DependencyManager::get()->getLocalLights()) { glm::vec3 direction = orientation * light.direction; DependencyManager::get()->addSpotLight(position - direction * distance, - distance * 2.0f, light.color, 0.5f, orientation, LIGHT_EXPONENT, LIGHT_CUTOFF); + distance * 2.0f, light.color, 0.5f, LIGHT_FALLOFF_RADIUS, orientation, LIGHT_EXPONENT, LIGHT_CUTOFF); } } @@ -516,7 +516,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.pointIntersectsFrustum(textPosition)) { renderDisplayName(batch, frustum, textPosition); } } @@ -669,10 +669,10 @@ glm::vec3 Avatar::getDisplayNamePosition() const { return namePosition; } -Transform Avatar::calculateDisplayNameTransform(const ViewFrustum& frustum, const glm::vec3& textPosition) const { - Q_ASSERT_X(frustum.pointInFrustum(textPosition, true) == ViewFrustum::INSIDE, +Transform Avatar::calculateDisplayNameTransform(const ViewFrustum& view, const glm::vec3& textPosition) const { + Q_ASSERT_X(view.pointIntersectsFrustum(textPosition), "Avatar::calculateDisplayNameTransform", "Text not in viewfrustum."); - glm::vec3 toFrustum = frustum.getPosition() - textPosition; + glm::vec3 toFrustum = view.getPosition() - textPosition; // Compute orientation // If x and z are 0, atan(x, z) adais undefined, so default to 0 degrees @@ -694,7 +694,7 @@ Transform Avatar::calculateDisplayNameTransform(const ViewFrustum& frustum, cons return result; } -void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& frustum, const glm::vec3& textPosition) const { +void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const glm::vec3& textPosition) const { PROFILE_RANGE_BATCH(batch, __FUNCTION__); bool shouldShowReceiveStats = DependencyManager::get()->shouldShowReceiveStats() && !isMyAvatar(); @@ -702,7 +702,7 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& frustum, co // If we have nothing to draw, or it's totally transparent, or it's too close or behind the camera, return static const float CLIP_DISTANCE = 0.2f; if ((_displayName.isEmpty() && !shouldShowReceiveStats) || _displayNameAlpha == 0.0f - || (glm::dot(frustum.getDirection(), getDisplayNamePosition() - frustum.getPosition()) <= CLIP_DISTANCE)) { + || (glm::dot(view.getDirection(), getDisplayNamePosition() - view.getPosition()) <= CLIP_DISTANCE)) { return; } auto renderer = textRenderer(DISPLAYNAME); @@ -743,7 +743,7 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& frustum, co (_displayNameAlpha / DISPLAYNAME_ALPHA) * DISPLAYNAME_BACKGROUND_ALPHA); // Compute display name transform - auto textTransform = calculateDisplayNameTransform(frustum, textPosition); + auto textTransform = calculateDisplayNameTransform(view, textPosition); // Test on extent above insures abs(height) > 0.0f textTransform.postScale(1.0f / height); batch.setModelTransform(textTransform); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 766a4734eb..cd9fc86e1e 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -231,8 +231,8 @@ protected: float getPelvisFloatingHeight() const; glm::vec3 getDisplayNamePosition() const; - Transform calculateDisplayNameTransform(const ViewFrustum& frustum, const glm::vec3& textPosition) const; - void renderDisplayName(gpu::Batch& batch, const ViewFrustum& frustum, const glm::vec3& textPosition) const; + Transform calculateDisplayNameTransform(const ViewFrustum& view, const glm::vec3& textPosition) const; + void renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const glm::vec3& textPosition) const; virtual void renderBody(RenderArgs* renderArgs, ViewFrustum* renderFrustum, float glowLevel = 0.0f); virtual bool shouldRenderHead(const RenderArgs* renderArgs) const; virtual void fixupModelsInScene(); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6318a9bb1e..fc715eebe9 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -656,7 +656,7 @@ void MyAvatar::saveData() { settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); - settings.setValue("snapTurn", _useSnapTurn); + settings.setValue("useSnapTurn", _useSnapTurn); settings.endGroup(); } @@ -750,7 +750,7 @@ void MyAvatar::loadData() { setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); - setSnapTurn(settings.value("snapTurn").toBool()); + setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); settings.endGroup(); diff --git a/interface/src/ui/ApplicationCompositor.cpp b/interface/src/ui/ApplicationCompositor.cpp index 5c6e4c9819..99f0b4fdc4 100644 --- a/interface/src/ui/ApplicationCompositor.cpp +++ b/interface/src/ui/ApplicationCompositor.cpp @@ -403,6 +403,22 @@ glm::vec2 ApplicationCompositor::getReticlePosition() const { return toGlm(QCursor::pos()); } +bool ApplicationCompositor::getReticleOverDesktop() const { + // if the QML/Offscreen UI thinks we're over the desktop, then we are... + // but... if we're outside of the overlay area, we also want to call ourselves + // as being over the desktop. + if (qApp->isHMDMode()) { + QMutexLocker locker(&_reticleLock); + glm::vec2 maxOverlayPosition = qApp->getUiSize(); + if (_reticlePositionInHMD.x < 0 || _reticlePositionInHMD.y < 0 || + _reticlePositionInHMD.x > maxOverlayPosition.x || _reticlePositionInHMD.y > maxOverlayPosition.y) { + return true; // we are outside the overlay area, consider ourselves over the desktop + } + } + return _isOverDesktop; +} + + void ApplicationCompositor::setReticlePosition(glm::vec2 position, bool sendFakeEvent) { if (qApp->isHMDMode()) { QMutexLocker locker(&_reticleLock); diff --git a/interface/src/ui/ApplicationCompositor.h b/interface/src/ui/ApplicationCompositor.h index 7bdb97727f..a3735a9766 100644 --- a/interface/src/ui/ApplicationCompositor.h +++ b/interface/src/ui/ApplicationCompositor.h @@ -107,7 +107,7 @@ public: bool shouldCaptureMouse() const; /// if the reticle is pointing to a system overlay (a dialog box for example) then the function returns true otherwise false - bool getReticleOverDesktop() const { return _isOverDesktop; } + bool getReticleOverDesktop() const; void setReticleOverDesktop(bool value) { _isOverDesktop = value; } private: diff --git a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp index 39182f322c..fb6061e94f 100644 --- a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp @@ -36,15 +36,16 @@ void RenderableLightEntityItem::render(RenderArgs* args) { glm::vec3 color = toGlm(getXColor()); float intensity = getIntensity(); + float falloffRadius = getFalloffRadius(); float exponent = getExponent(); float cutoff = glm::radians(getCutoff()); if (_isSpotlight) { DependencyManager::get()->addSpotLight(position, largestDiameter / 2.0f, - color, intensity, rotation, exponent, cutoff); + color, intensity, falloffRadius, rotation, exponent, cutoff); } else { DependencyManager::get()->addPointLight(position, largestDiameter / 2.0f, - color, intensity); + color, intensity, falloffRadius); } #ifdef WANT_DEBUG diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 8e0983f62a..550ec205c0 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -245,6 +245,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_DYNAMIC, dynamic); CHECK_PROPERTY_CHANGE(PROP_IS_SPOTLIGHT, isSpotlight); CHECK_PROPERTY_CHANGE(PROP_INTENSITY, intensity); + CHECK_PROPERTY_CHANGE(PROP_FALLOFF_RADIUS, falloffRadius); CHECK_PROPERTY_CHANGE(PROP_EXPONENT, exponent); CHECK_PROPERTY_CHANGE(PROP_CUTOFF, cutoff); CHECK_PROPERTY_CHANGE(PROP_LOCKED, locked); @@ -445,6 +446,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool if (_type == EntityTypes::Light) { COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_SPOTLIGHT, isSpotlight); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_INTENSITY, intensity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FALLOFF_RADIUS, falloffRadius); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EXPONENT, exponent); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CUTOFF, cutoff); } @@ -597,6 +599,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(dynamic, bool, setDynamic); COPY_PROPERTY_FROM_QSCRIPTVALUE(isSpotlight, bool, setIsSpotlight); COPY_PROPERTY_FROM_QSCRIPTVALUE(intensity, float, setIntensity); + COPY_PROPERTY_FROM_QSCRIPTVALUE(falloffRadius, float, setFalloffRadius); COPY_PROPERTY_FROM_QSCRIPTVALUE(exponent, float, setExponent); COPY_PROPERTY_FROM_QSCRIPTVALUE(cutoff, float, setCutoff); COPY_PROPERTY_FROM_QSCRIPTVALUE(locked, bool, setLocked); @@ -762,6 +765,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, dynamic, unused); ADD_PROPERTY_TO_MAP(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool); ADD_PROPERTY_TO_MAP(PROP_INTENSITY, Intensity, intensity, float); + ADD_PROPERTY_TO_MAP(PROP_FALLOFF_RADIUS, FalloffRadius, falloffRadius, float); ADD_PROPERTY_TO_MAP(PROP_EXPONENT, Exponent, exponent, float); ADD_PROPERTY_TO_MAP(PROP_CUTOFF, Cutoff, cutoff, float); ADD_PROPERTY_TO_MAP(PROP_LOCKED, Locked, locked, bool); @@ -1043,6 +1047,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_IS_SPOTLIGHT, properties.getIsSpotlight()); APPEND_ENTITY_PROPERTY(PROP_COLOR, properties.getColor()); APPEND_ENTITY_PROPERTY(PROP_INTENSITY, properties.getIntensity()); + APPEND_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, properties.getFalloffRadius()); APPEND_ENTITY_PROPERTY(PROP_EXPONENT, properties.getExponent()); APPEND_ENTITY_PROPERTY(PROP_CUTOFF, properties.getCutoff()); } @@ -1332,6 +1337,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_IS_SPOTLIGHT, bool, setIsSpotlight); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLOR, xColor, setColor); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_INTENSITY, float, setIntensity); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FALLOFF_RADIUS, float, setFalloffRadius); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_EXPONENT, float, setExponent); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CUTOFF, float, setCutoff); } @@ -1477,6 +1483,7 @@ void EntityItemProperties::markAllChanged() { _dynamicChanged = true; _intensityChanged = true; + _falloffRadiusChanged = true; _exponentChanged = true; _cutoffChanged = true; _lockedChanged = true; @@ -1719,6 +1726,9 @@ QList EntityItemProperties::listChangedProperties() { if (intensityChanged()) { out += "intensity"; } + if (falloffRadiusChanged()) { + out += "falloffRadius"; + } if (exponentChanged()) { out += "exponent"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 2cfef17c1b..c732d01fa5 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -35,6 +35,7 @@ #include "EntityItemPropertiesMacros.h" #include "EntityTypes.h" #include "EntityPropertyFlags.h" +#include "LightEntityItem.h" #include "LineEntityItem.h" #include "ParticleEffectEntityItem.h" #include "PolyVoxEntityItem.h" @@ -129,10 +130,11 @@ public: DEFINE_PROPERTY(PROP_COLLISIONLESS, Collisionless, collisionless, bool, ENTITY_ITEM_DEFAULT_COLLISIONLESS); DEFINE_PROPERTY(PROP_COLLISION_MASK, CollisionMask, collisionMask, uint8_t, ENTITY_COLLISION_MASK_DEFAULT); DEFINE_PROPERTY(PROP_DYNAMIC, Dynamic, dynamic, bool, ENTITY_ITEM_DEFAULT_DYNAMIC); - DEFINE_PROPERTY(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool, false); - DEFINE_PROPERTY(PROP_INTENSITY, Intensity, intensity, float, 1.0f); - DEFINE_PROPERTY(PROP_EXPONENT, Exponent, exponent, float, 0.0f); - DEFINE_PROPERTY(PROP_CUTOFF, Cutoff, cutoff, float, ENTITY_ITEM_DEFAULT_CUTOFF); + DEFINE_PROPERTY(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool, LightEntityItem::DEFAULT_IS_SPOTLIGHT); + DEFINE_PROPERTY(PROP_INTENSITY, Intensity, intensity, float, LightEntityItem::DEFAULT_INTENSITY); + DEFINE_PROPERTY(PROP_FALLOFF_RADIUS, FalloffRadius, falloffRadius, float, LightEntityItem::DEFAULT_FALLOFF_RADIUS); + DEFINE_PROPERTY(PROP_EXPONENT, Exponent, exponent, float, LightEntityItem::DEFAULT_EXPONENT); + DEFINE_PROPERTY(PROP_CUTOFF, Cutoff, cutoff, float, LightEntityItem::DEFAULT_CUTOFF); DEFINE_PROPERTY(PROP_LOCKED, Locked, locked, bool, ENTITY_ITEM_DEFAULT_LOCKED); DEFINE_PROPERTY_REF(PROP_TEXTURES, Textures, textures, QString, ""); DEFINE_PROPERTY_REF(PROP_USER_DATA, UserData, userData, QString, ENTITY_ITEM_DEFAULT_USER_DATA); @@ -359,6 +361,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, Dynamic, dynamic, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, IsSpotlight, isSpotlight, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Intensity, intensity, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, FalloffRadius, falloffRadius, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Exponent, exponent, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Cutoff, cutoff, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Locked, locked, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index 5375b1bc3a..aa4fb5c619 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -72,8 +72,6 @@ const bool ENTITY_ITEM_DEFAULT_COLLISIONLESS = false; const bool ENTITY_ITEM_DEFAULT_DYNAMIC = false; const bool ENTITY_ITEM_DEFAULT_BILLBOARDED = false; -const float ENTITY_ITEM_DEFAULT_CUTOFF = PI / 2; - const QString ENTITY_ITEM_DEFAULT_NAME = QString(""); #endif // hifi_EntityItemPropertiesDefaults_h diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index b60fc6174c..90a7c1e2f7 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -167,6 +167,8 @@ enum EntityPropertyList { PROP_COLLISION_MASK, // one byte of collision group flags + PROP_FALLOFF_RADIUS, // for Light entity + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 4abbe3c3fa..15ff531b06 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -85,7 +85,7 @@ void EntityTreeElement::initializeExtraEncodeData(EncodeBitstreamParams& params) forEachEntity([&](EntityItemPointer entity) { entityTreeElementExtraEncodeData->entities.insert(entity->getEntityItemID(), entity->getEntityProperties(params)); }); - + // TODO: some of these inserts might be redundant!!! extraEncodeData->insert(this, entityTreeElementExtraEncodeData); } @@ -96,39 +96,39 @@ bool EntityTreeElement::shouldIncludeChildData(int childIndex, EncodeBitstreamPa assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData + EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = static_cast(extraEncodeData->value(this)); - + bool childCompleted = entityTreeElementExtraEncodeData->childCompleted[childIndex]; - + // If we haven't completely sent the child yet, then we should include it return !childCompleted; } - + // I'm not sure this should ever happen, since we should have the extra encode data if we're considering // the child data for this element assert(false); return false; } -bool EntityTreeElement::shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { +bool EntityTreeElement::shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { EntityTreeElementPointer childElement = getChildAtIndex(childIndex); if (childElement->alreadyFullyEncoded(params)) { return false; } - + return true; // if we don't know otherwise than recurse! } -bool EntityTreeElement::alreadyFullyEncoded(EncodeBitstreamParams& params) const { +bool EntityTreeElement::alreadyFullyEncoded(EncodeBitstreamParams& params) const { OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData; assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData + EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = static_cast(extraEncodeData->value(this)); - // If we know that ALL subtrees below us have already been recursed, then we don't + // If we know that ALL subtrees below us have already been recursed, then we don't // need to recurse this child. return entityTreeElementExtraEncodeData->subtreeCompleted; } @@ -139,7 +139,7 @@ void EntityTreeElement::updateEncodedData(int childIndex, AppendState childAppen OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData; assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData + EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = static_cast(extraEncodeData->value(this)); if (childAppendState == OctreeElement::COMPLETED) { @@ -155,7 +155,7 @@ void EntityTreeElement::updateEncodedData(int childIndex, AppendState childAppen void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) const { const bool wantDebug = false; - + if (wantDebug) { qCDebug(entities) << "EntityTreeElement::elementEncodeComplete() element:" << _cube; } @@ -188,7 +188,7 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con // If we've encoding this element before... but we're coming back a second time in an attempt to // encoud our parent... this might happen. if (extraEncodeData->contains(childElement.get())) { - EntityTreeElementExtraEncodeData* childExtraEncodeData + EntityTreeElementExtraEncodeData* childExtraEncodeData = static_cast(extraEncodeData->value(childElement.get())); if (wantDebug) { @@ -197,7 +197,7 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con qCDebug(entities) << " childExtraEncodeData->elementCompleted:" << childExtraEncodeData->elementCompleted; qCDebug(entities) << " childExtraEncodeData->subtreeCompleted:" << childExtraEncodeData->subtreeCompleted; } - + if (childElement->isLeaf() && childExtraEncodeData->elementCompleted) { if (wantDebug) { qCDebug(entities) << " CHILD IS LEAF -- AND CHILD ELEMENT DATA COMPLETED!!!"; @@ -217,24 +217,24 @@ void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) con qCDebug(entities) << " WAS elementCompleted:" << thisExtraEncodeData->elementCompleted; qCDebug(entities) << " WAS subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; } - + thisExtraEncodeData->subtreeCompleted = !someChildTreeNotComplete; if (wantDebug) { qCDebug(entities) << " NOW elementCompleted:" << thisExtraEncodeData->elementCompleted; qCDebug(entities) << " NOW subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; - + if (thisExtraEncodeData->subtreeCompleted) { qCDebug(entities) << " YEAH!!!!! >>>>>>>>>>>>>> NOW subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; } } } -OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData* packetData, +OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const { OctreeElement::AppendState appendElementState = OctreeElement::COMPLETED; // assume the best... - + // first, check the params.extraEncodeData to see if there's any partial re-encode data for this element OctreeElementExtraEncodeData* extraEncodeData = params.extraEncodeData; EntityTreeElementExtraEncodeData* entityTreeElementExtraEncodeData = NULL; @@ -280,7 +280,7 @@ OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData QVector indexesOfEntitiesToInclude; // It's possible that our element has been previous completed. In this case we'll simply not include any of our - // entities for encoding. This is needed because we encode the element data at the "parent" level, and so we + // entities for encoding. This is needed because we encode the element data at the "parent" level, and so we // need to handle the case where our sibling elements need encoding but we don't. if (!entityTreeElementExtraEncodeData->elementCompleted) { for (uint16_t i = 0; i < _entityItems.size(); i++) { @@ -304,15 +304,13 @@ 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->cubeIntersectsKeyhole(entityCube)) { includeThisEntity = false; // out of view, don't include it - } - - // Now check the size of the entity, it's possible that a "too small to see" entity is included in a - // larger octree cell because of its position (for example if it crosses the boundary of a cell it - // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen - // before we consider including it. - if (includeThisEntity) { + } else { + // Check the size of the entity, it's possible that a "too small to see" entity is included in a + // larger octree cell because of its position (for example if it crosses the boundary of a cell it + // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen + // before we consider including it. success = true; // we can't cull a parent-entity by its dimensions because the child may be larger. we need to // avoid sending details about a child but not the parent. the parent's queryAACube should have @@ -397,7 +395,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 +410,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 +424,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 +502,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 +522,7 @@ bool EntityTreeElement::bestFitBounds(const glm::vec3& minPoint, const glm::vec3 bool EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, + BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard, void** intersectedObject, bool precisionPicking) { keepSearching = true; // assume that we will continue searching after this. @@ -607,7 +605,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 +860,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 +945,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 +957,7 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int } } } - + return bytesRead; } @@ -990,7 +988,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 +1038,4 @@ void EntityTreeElement::debugDump() { } }); } - + diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index ac56fc9c1f..852b37a751 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -21,6 +21,12 @@ #include "EntityTreeElement.h" #include "LightEntityItem.h" +const bool LightEntityItem::DEFAULT_IS_SPOTLIGHT = false; +const float LightEntityItem::DEFAULT_INTENSITY = 1.0f; +const float LightEntityItem::DEFAULT_FALLOFF_RADIUS = 0.1f; +const float LightEntityItem::DEFAULT_EXPONENT = 0.0f; +const float LightEntityItem::DEFAULT_CUTOFF = PI / 2.0f; + bool LightEntityItem::_lightsArePickable = false; EntityItemPointer LightEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { @@ -32,12 +38,7 @@ EntityItemPointer LightEntityItem::factory(const EntityItemID& entityID, const E // our non-pure virtual subclass for now... LightEntityItem::LightEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { _type = EntityTypes::Light; - - // default property values _color[RED_INDEX] = _color[GREEN_INDEX] = _color[BLUE_INDEX] = 0; - _intensity = 1.0f; - _exponent = 0.0f; - _cutoff = PI; } void LightEntityItem::setDimensions(const glm::vec3& value) { @@ -62,10 +63,15 @@ EntityItemProperties LightEntityItem::getProperties(EntityPropertyFlags desiredP COPY_ENTITY_PROPERTY_TO_PROPERTIES(intensity, getIntensity); COPY_ENTITY_PROPERTY_TO_PROPERTIES(exponent, getExponent); COPY_ENTITY_PROPERTY_TO_PROPERTIES(cutoff, getCutoff); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(falloffRadius, getFalloffRadius); return properties; } +void LightEntityItem::setFalloffRadius(float value) { + _falloffRadius = glm::max(value, 0.0f); +} + void LightEntityItem::setIsSpotlight(bool value) { if (value != _isSpotlight) { _isSpotlight = value; @@ -101,6 +107,7 @@ bool LightEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(intensity, setIntensity); SET_ENTITY_PROPERTY_FROM_PROPERTIES(exponent, setExponent); SET_ENTITY_PROPERTY_FROM_PROPERTIES(cutoff, setCutoff); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(falloffRadius, setFalloffRadius); if (somethingChanged) { bool wantDebug = false; @@ -150,6 +157,7 @@ int LightEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_INTENSITY, float, setIntensity); READ_ENTITY_PROPERTY(PROP_EXPONENT, float, setExponent); READ_ENTITY_PROPERTY(PROP_CUTOFF, float, setCutoff); + READ_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, float, setFalloffRadius); } return bytesRead; @@ -164,6 +172,7 @@ EntityPropertyFlags LightEntityItem::getEntityProperties(EncodeBitstreamParams& requestedProperties += PROP_INTENSITY; requestedProperties += PROP_EXPONENT; requestedProperties += PROP_CUTOFF; + requestedProperties += PROP_FALLOFF_RADIUS; return requestedProperties; } @@ -181,4 +190,5 @@ void LightEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_INTENSITY, getIntensity()); APPEND_ENTITY_PROPERTY(PROP_EXPONENT, getExponent()); APPEND_ENTITY_PROPERTY(PROP_CUTOFF, getCutoff()); + APPEND_ENTITY_PROPERTY(PROP_FALLOFF_RADIUS, getFalloffRadius()); } diff --git a/libraries/entities/src/LightEntityItem.h b/libraries/entities/src/LightEntityItem.h index 103c462809..4c84d3204c 100644 --- a/libraries/entities/src/LightEntityItem.h +++ b/libraries/entities/src/LightEntityItem.h @@ -16,6 +16,12 @@ class LightEntityItem : public EntityItem { public: + static const bool DEFAULT_IS_SPOTLIGHT; + static const float DEFAULT_INTENSITY; + static const float DEFAULT_FALLOFF_RADIUS; + static const float DEFAULT_EXPONENT; + static const float DEFAULT_CUTOFF; + static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); LightEntityItem(const EntityItemID& entityItemID); @@ -65,6 +71,9 @@ public: float getIntensity() const { return _intensity; } void setIntensity(float value) { _intensity = value; } + float getFalloffRadius() const { return _falloffRadius; } + void setFalloffRadius(float value); + float getExponent() const { return _exponent; } void setExponent(float value) { _exponent = value; } @@ -78,10 +87,11 @@ protected: // properties of a light rgbColor _color; - bool _isSpotlight; - float _intensity; - float _exponent; - float _cutoff; + bool _isSpotlight { DEFAULT_IS_SPOTLIGHT }; + float _intensity { DEFAULT_INTENSITY }; + float _falloffRadius { DEFAULT_FALLOFF_RADIUS }; + float _exponent { DEFAULT_EXPONENT }; + float _cutoff { DEFAULT_CUTOFF }; static bool _lightsArePickable; }; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index 779d70cc88..abaa0f9ef1 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -21,6 +21,7 @@ Framebuffer::~Framebuffer() { Framebuffer* Framebuffer::create() { auto framebuffer = new Framebuffer(); framebuffer->_renderBuffers.resize(MAX_NUM_RENDER_BUFFERS); + framebuffer->_colorStamps.resize(MAX_NUM_RENDER_BUFFERS, 0); return framebuffer; } @@ -174,6 +175,8 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin } } + ++_colorStamps[slot]; + updateSize(texture); // assign the new one @@ -190,6 +193,7 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin } void Framebuffer::removeRenderBuffers() { + if (isSwapchain()) { return; } @@ -230,6 +234,7 @@ uint32 Framebuffer::getRenderBufferSubresource(uint32 slot) const { } bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const Format& format, uint32 subresource) { + ++_depthStamp; if (isSwapchain()) { return false; } diff --git a/libraries/gpu/src/gpu/Framebuffer.h b/libraries/gpu/src/gpu/Framebuffer.h index e986e4a481..5d016645fe 100755 --- a/libraries/gpu/src/gpu/Framebuffer.h +++ b/libraries/gpu/src/gpu/Framebuffer.h @@ -135,10 +135,15 @@ public: static uint32 getMaxNumRenderBuffers() { return MAX_NUM_RENDER_BUFFERS; } const GPUObjectPointer gpuObject {}; - + + Stamp getDepthStamp() const { return _depthStamp; } + const std::vector& getColorStamps() const { return _colorStamps; } + protected: SwapchainPointer _swapchain; + Stamp _depthStamp { 0 }; + std::vector _colorStamps; TextureViews _renderBuffers; TextureView _depthStencilBuffer; diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 7f37a73c57..15338d7587 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -173,6 +173,9 @@ public: public: GLuint _fbo = 0; std::vector _colorBuffers; + Stamp _depthStamp { 0 }; + std::vector _colorStamps; + GLFramebuffer(); ~GLFramebuffer(); diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp index 5f226141a8..37a10e670b 100755 --- a/libraries/gpu/src/gpu/GLBackendOutput.cpp +++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp @@ -27,8 +27,15 @@ GLBackend::GLFramebuffer::~GLFramebuffer() { GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffer) { GLFramebuffer* object = Backend::getGPUObject(framebuffer); + bool needsUpate { false }; + if (!object || + framebuffer.getDepthStamp() != object->_depthStamp || + framebuffer.getColorStamps() != object->_colorStamps) { + needsUpate = true; + } + // If GPU object already created and in sync - if (object) { + if (!needsUpate) { return object; } else if (framebuffer.isEmpty()) { // NO framebuffer definition yet so let's avoid thinking @@ -37,94 +44,112 @@ GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffe // need to have a gpu object? if (!object) { - GLint currentFBO; + // All is green, assign the gpuobject to the Framebuffer + object = new GLFramebuffer(); + Backend::setGPUObject(framebuffer, object); + glGenFramebuffers(1, &object->_fbo); + (void)CHECK_GL_ERROR(); + } + + if (needsUpate) { + GLint currentFBO = -1; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤tFBO); - - GLuint fbo; - glGenFramebuffers(1, &fbo); - (void) CHECK_GL_ERROR(); + glBindFramebuffer(GL_FRAMEBUFFER, object->_fbo); - glBindFramebuffer(GL_FRAMEBUFFER, fbo); + GLTexture* gltexture = nullptr; + TexturePointer surface; + if (framebuffer.getColorStamps() != object->_colorStamps) { + if (framebuffer.hasColor()) { + object->_colorBuffers.clear(); + static const GLenum colorAttachments[] = { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1, + GL_COLOR_ATTACHMENT2, + GL_COLOR_ATTACHMENT3, + GL_COLOR_ATTACHMENT4, + GL_COLOR_ATTACHMENT5, + GL_COLOR_ATTACHMENT6, + GL_COLOR_ATTACHMENT7, + GL_COLOR_ATTACHMENT8, + GL_COLOR_ATTACHMENT9, + GL_COLOR_ATTACHMENT10, + GL_COLOR_ATTACHMENT11, + GL_COLOR_ATTACHMENT12, + GL_COLOR_ATTACHMENT13, + GL_COLOR_ATTACHMENT14, + GL_COLOR_ATTACHMENT15 }; - std::vector colorBuffers; - if (framebuffer.hasColor()) { - static const GLenum colorAttachments[] = { - GL_COLOR_ATTACHMENT0, - GL_COLOR_ATTACHMENT1, - GL_COLOR_ATTACHMENT2, - GL_COLOR_ATTACHMENT3, - GL_COLOR_ATTACHMENT4, - GL_COLOR_ATTACHMENT5, - GL_COLOR_ATTACHMENT6, - GL_COLOR_ATTACHMENT7, - GL_COLOR_ATTACHMENT8, - GL_COLOR_ATTACHMENT9, - GL_COLOR_ATTACHMENT10, - GL_COLOR_ATTACHMENT11, - GL_COLOR_ATTACHMENT12, - GL_COLOR_ATTACHMENT13, - GL_COLOR_ATTACHMENT14, - GL_COLOR_ATTACHMENT15 }; + int unit = 0; + for (auto& b : framebuffer.getRenderBuffers()) { + surface = b._texture; + if (surface) { + gltexture = GLBackend::syncGPUObject(*surface); + } else { + gltexture = nullptr; + } - int unit = 0; - for (auto& b : framebuffer.getRenderBuffers()) { - auto surface = b._texture; - if (surface) { - auto gltexture = GLBackend::syncGPUObject(*surface); if (gltexture) { glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); + object->_colorBuffers.push_back(colorAttachments[unit]); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, 0, 0); } - colorBuffers.push_back(colorAttachments[unit]); unit++; } } - } -#if (GPU_FEATURE_PROFILE == GPU_LEGACY) - // for reasons that i don't understand yet, it seems that on mac gl, a fbo must have a color buffer... - else { - GLuint renderBuffer = 0; - glGenRenderbuffers(1, &renderBuffer); - glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, framebuffer.getWidth(), framebuffer.getHeight()); - glBindRenderbuffer(GL_RENDERBUFFER, 0); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer); - (void) CHECK_GL_ERROR(); - } - - // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); -#endif - - - if (framebuffer.hasDepthStencil()) { - auto surface = framebuffer.getDepthStencilBuffer(); - if (surface) { - auto gltexture = GLBackend::syncGPUObject(*surface); - if (gltexture) { - GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; - if (!framebuffer.hasStencil()) { - attachement = GL_DEPTH_ATTACHMENT; - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); - } else if (!framebuffer.hasDepth()) { - attachement = GL_STENCIL_ATTACHMENT; - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); - } else { - attachement = GL_DEPTH_STENCIL_ATTACHMENT; - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); - } - (void) CHECK_GL_ERROR(); - } + #if (GPU_FEATURE_PROFILE == GPU_LEGACY) + // for reasons that i don't understand yet, it seems that on mac gl, a fbo must have a color buffer... + else { + GLuint renderBuffer = 0; + glGenRenderbuffers(1, &renderBuffer); + glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, framebuffer.getWidth(), framebuffer.getHeight()); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer); + (void) CHECK_GL_ERROR(); } + // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + #endif + object->_colorStamps = framebuffer.getColorStamps(); } + GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; + if (!framebuffer.hasStencil()) { + attachement = GL_DEPTH_ATTACHMENT; + } else if (!framebuffer.hasDepth()) { + attachement = GL_STENCIL_ATTACHMENT; + } + + if (framebuffer.getDepthStamp() != object->_depthStamp) { + auto surface = framebuffer.getDepthStencilBuffer(); + if (framebuffer.hasDepthStencil() && surface) { + gltexture = GLBackend::syncGPUObject(*surface); + } + + if (gltexture) { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); + } else { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, 0, 0); + } + object->_depthStamp = framebuffer.getDepthStamp(); + } + + // Last but not least, define where we draw - if (!colorBuffers.empty()) { - glDrawBuffers((GLsizei)colorBuffers.size(), colorBuffers.data()); + if (!object->_colorBuffers.empty()) { + glDrawBuffers((GLsizei)object->_colorBuffers.size(), object->_colorBuffers.data()); } else { glDrawBuffer( GL_NONE ); } // Now check for completness GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + // restore the current framebuffer + if (currentFBO != -1) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); + } + bool result = false; switch (status) { case GL_FRAMEBUFFER_COMPLETE : @@ -147,20 +172,10 @@ GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffe qCDebug(gpulogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; break; } - if (!result && fbo) { - glDeleteFramebuffers( 1, &fbo ); + if (!result && object->_fbo) { + glDeleteFramebuffers(1, &object->_fbo); return nullptr; } - - - // All is green, assign the gpuobject to the Framebuffer - object = new GLFramebuffer(); - object->_fbo = fbo; - object->_colorBuffers = colorBuffers; - Backend::setGPUObject(framebuffer, object); - - // restore the current framebuffer - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); } return object; diff --git a/libraries/model/src/model/Light.cpp b/libraries/model/src/model/Light.cpp index 7ab8b77f5e..8a016c4d77 100755 --- a/libraries/model/src/model/Light.cpp +++ b/libraries/model/src/model/Light.cpp @@ -12,19 +12,18 @@ using namespace model; -Light::Light() : - _flags(0), - _schemaBuffer(), - _transform() { +Light::Light() { // only if created from nothing shall we create the Buffer to store the properties Schema schema; _schemaBuffer = std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema); + updateLightRadius(); } Light::Light(const Light& light) : _flags(light._flags), _schemaBuffer(light._schemaBuffer), - _transform(light._transform) { + _transform(light._transform) +{ } Light& Light::operator= (const Light& light) { @@ -70,18 +69,36 @@ void Light::setAmbientIntensity(float intensity) { editSchema()._ambientIntensity = intensity; } +void Light::setFalloffRadius(float radius) { + if (radius <= 0.0f) { + radius = 0.1f; + } + editSchema()._attenuation.x = radius; + updateLightRadius(); +} void Light::setMaximumRadius(float radius) { if (radius <= 0.f) { radius = 1.0f; } - editSchema()._attenuation.w = radius; + editSchema()._attenuation.y = radius; updateLightRadius(); } void Light::updateLightRadius() { - float CutOffIntensityRatio = 0.05f; - float surfaceRadius = getMaximumRadius() / (sqrtf((getIntensity() * std::max(std::max(getColor().x, getColor().y), getColor().z)) / CutOffIntensityRatio) - 1.0f); - editSchema()._attenuation = Vec4(surfaceRadius, 1.0f/surfaceRadius, CutOffIntensityRatio, getMaximumRadius()); + // This function relies on the attenuation equation: + // I = Li / (1 + (d + Lr)/Lr)^2 + // where I = calculated intensity, Li = light intensity, Lr = light falloff radius, d = distance from surface + // see: https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/ + // note that falloff radius replaces surface radius in linked example + // This equation is biased back by Lr so that all lights act as true points, regardless of surface radii + + const float MIN_CUTOFF_INTENSITY = 0.001f; + // Get cutoff radius at minimum intensity + float intensity = getIntensity() * std::max(std::max(getColor().x, getColor().y), getColor().z); + float cutoffRadius = getFalloffRadius() * ((glm::sqrt(intensity / MIN_CUTOFF_INTENSITY) - 1) - 1); + + // If it is less than max radius, store it to buffer to avoid extra shading + editSchema()._attenuation.z = std::min(getMaximumRadius(), cutoffRadius); } #include diff --git a/libraries/model/src/model/Light.h b/libraries/model/src/model/Light.h index 96bf7fc427..dafbf90397 100755 --- a/libraries/model/src/model/Light.h +++ b/libraries/model/src/model/Light.h @@ -74,8 +74,17 @@ public: bool isRanged() const { return (getType() == POINT) || (getType() == SPOT ); } + // FalloffRradius is the physical radius of the light sphere through which energy shines, + // expressed in meters. It is used only to calculate the falloff curve of the light. + // Actual rendered lights will all have surface radii approaching 0. + void setFalloffRadius(float radius); + float getFalloffRadius() const { return getSchema()._attenuation.x; } + + // Maximum radius is the cutoff radius of the light energy, expressed in meters. + // It is used to bound light entities, and *will not* affect the falloff curve of the light. + // Setting it low will result in a noticeable cutoff. void setMaximumRadius(float radius); - float getMaximumRadius() const { return getSchema()._attenuation.w; } + float getMaximumRadius() const { return getSchema()._attenuation.y; } // Spot properties bool isSpot() const { return getType() == SPOT; } @@ -107,7 +116,7 @@ public: float _ambientIntensity{0.0f}; Color _color{1.0f}; float _intensity{1.0f}; - Vec4 _attenuation{1.0f}; + Vec4 _attenuation{0.1f, 1.0f, 0.0f, 0.0f}; Vec4 _spot{0.0f, 0.0f, 0.0f, 0.0f}; Vec4 _shadow{0.0f}; @@ -120,7 +129,7 @@ public: protected: - Flags _flags; + Flags _flags{ 0 }; UniformBufferView _schemaBuffer; Transform _transform; diff --git a/libraries/model/src/model/Light.slh b/libraries/model/src/model/Light.slh index 6529da1b61..af1c251ccb 100644 --- a/libraries/model/src/model/Light.slh +++ b/libraries/model/src/model/Light.slh @@ -66,14 +66,6 @@ vec3 getLightColor(Light l) { return l._color.rgb; } float getLightIntensity(Light l) { return l._color.w; } float getLightAmbientIntensity(Light l) { return l._direction.w; } -float evalLightAttenuation(Light l, float r) { - float d = max(r - l._attenuation.x, 0.0); - float denom = d * l._attenuation.y + 1.0; - float attenuation = 1.0 / (denom * denom); - return max((attenuation - l._attenuation.z)/(1.0 - l._attenuation.z), 0.0); - // return clamp(1.0/(l._attenuation.x + l._attenuation.y * r + l._attenuation.z * r * r), 0.0, 1.0); -} - float getLightSpotAngleCos(Light l) { return l._spot.x; } @@ -86,22 +78,33 @@ float evalLightSpotAttenuation(Light l, float cosA) { return pow(cosA, l._spot.w); } -float getLightSquareRadius(Light l) { - return l._attenuation.w * l._attenuation.w; -} - float getLightRadius(Light l) { - return l._attenuation.w; + return l._attenuation.x; } -float getLightAttenuationCutoff(Light l) { +float getLightSquareRadius(Light l) { + return getLightRadius(l) * getLightRadius(l); +} + +float getLightCutoffRadius(Light l) { return l._attenuation.z; } +float getLightCutoffSquareRadius(Light l) { + return getLightCutoffRadius(l) * getLightCutoffRadius(l); +} + float getLightShowContour(Light l) { return l._control.w; } +float evalLightAttenuation(Light l, float d) { + float radius = getLightRadius(l); + float denom = d / radius + 1.0; + float attenuation = min(1.0, 1.0 / (denom * denom)); + return attenuation; +} + SphericalHarmonics getLightAmbientSphere(Light l) { return l._ambientSphere; } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 4e6418279d..88fabf1a5a 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -41,7 +41,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ATMOSPHERE_REMOVED; + return VERSION_LIGHT_HAS_FALLOFF_RADIUS; case PacketType::AvatarData: case PacketType::BulkAvatarData: return static_cast(AvatarMixerPacketVersion::SoftAttachmentSupport); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index af3fd49710..0f586018db 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -166,6 +166,7 @@ const PacketVersion VERSION_MODEL_ENTITIES_JOINTS_ON_WIRE = 53; const PacketVersion VERSION_ENTITITES_HAVE_QUERY_BOX = 54; const PacketVersion VERSION_ENTITITES_HAVE_COLLISION_MASK = 55; const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56; +const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index a685c2580c..8959bd4aa7 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -937,7 +937,7 @@ int Octree::encodeTreeBitstream(OctreeElementPointer element, params.stats->traversed(element); } - ViewFrustum::location parentLocationThisView = ViewFrustum::INTERSECT; // assume parent is in view, but not fully + ViewFrustum::intersection parentLocationThisView = ViewFrustum::INTERSECT; // assume parent is in view, but not fully int childBytesWritten = encodeTreeBitstreamRecursion(element, packetData, bag, params, currentEncodeLevel, parentLocationThisView); @@ -974,7 +974,7 @@ int Octree::encodeTreeBitstream(OctreeElementPointer element, int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element, OctreePacketData* packetData, OctreeElementBag& bag, EncodeBitstreamParams& params, int& currentEncodeLevel, - const ViewFrustum::location& parentLocationThisView) const { + const ViewFrustum::intersection& parentLocationThisView) const { const bool wantDebug = false; @@ -1013,7 +1013,7 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element, } } - ViewFrustum::location nodeLocationThisView = ViewFrustum::INSIDE; // assume we're inside + ViewFrustum::intersection nodeLocationThisView = ViewFrustum::INSIDE; // assume we're inside // caller can pass NULL as viewFrustum if they want everything if (params.viewFrustum) { @@ -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->computeViewIntersection(*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::intersection location = element->computeViewIntersection(*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::intersection location = childElement->computeViewIntersection(*params.lastViewFrustum); // If we're a leaf, then either intersect or inside is considered "formerly in view" if (childElement->isLeaf()) { diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 0939ae37f6..81a721c54e 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -367,7 +367,7 @@ protected: int encodeTreeBitstreamRecursion(OctreeElementPointer element, OctreePacketData* packetData, OctreeElementBag& bag, EncodeBitstreamParams& params, int& currentEncodeLevel, - const ViewFrustum::location& parentLocationThisView) const; + const ViewFrustum::intersection& parentLocationThisView) const; static bool countOctreeElementsOperation(OctreeElementPointer element, void* extraData); diff --git a/libraries/octree/src/OctreeElement.cpp b/libraries/octree/src/OctreeElement.cpp index f16e1dc88d..db0948b8b6 100644 --- a/libraries/octree/src/OctreeElement.cpp +++ b/libraries/octree/src/OctreeElement.cpp @@ -458,8 +458,8 @@ float OctreeElement::getEnclosingRadius() const { return getScale() * sqrtf(3.0f) / 2.0f; } -ViewFrustum::location OctreeElement::inFrustum(const ViewFrustum& viewFrustum) const { - return viewFrustum.cubeInFrustum(_cube); +ViewFrustum::intersection OctreeElement::computeViewIntersection(const ViewFrustum& viewFrustum) const { + return viewFrustum.calculateCubeKeyholeIntersection(_cube); } // There are two types of nodes for which we want to "render" diff --git a/libraries/octree/src/OctreeElement.h b/libraries/octree/src/OctreeElement.h index 3c25ec0850..398f1827b7 100644 --- a/libraries/octree/src/OctreeElement.h +++ b/libraries/octree/src/OctreeElement.h @@ -49,20 +49,20 @@ protected: OctreeElement(); virtual OctreeElementPointer createNewElement(unsigned char * octalCode = NULL) = 0; - + public: virtual void init(unsigned char * octalCode); /// Your subclass must call init on construction. virtual ~OctreeElement(); // methods you can and should override to implement your tree functionality - + /// Adds a child to the current element. Override this if there is additional child initialization your class needs. virtual OctreeElementPointer addChildAtIndex(int childIndex); - /// Override this to implement LOD averaging on changes to the tree. + /// Override this to implement LOD averaging on changes to the tree. virtual void calculateAverageFromChildren() { } - /// Override this to implement LOD collapsing and identical child pruning on changes to the tree. + /// Override this to implement LOD collapsing and identical child pruning on changes to the tree. virtual bool collapseChildren() { return false; } /// Should this element be considered to have content in it. This will be used in collision and ray casting methods. @@ -72,12 +72,12 @@ public: /// Should this element be considered to have detailed content in it. Specifically should it be rendered. /// By default we assume that only leaves have detailed content, but some octrees may have different semantics. virtual bool hasDetailedContent() const { return isLeaf(); } - + /// Override this to break up large octree elements when an edit operation is performed on a smaller octree element. - /// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the + /// For example, if the octrees represent solid cubes and a delete of a smaller octree element is done then the /// meaningful split would be to break the larger cube into smaller cubes of the same color/texture. virtual void splitChildren() { } - + /// Override to indicate that this element requires a split before editing lower elements in the octree virtual bool requiresSplit() const { return false; } @@ -88,17 +88,17 @@ public: virtual void initializeExtraEncodeData(EncodeBitstreamParams& params) { } virtual bool shouldIncludeChildData(int childIndex, EncodeBitstreamParams& params) const { return true; } virtual bool shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { return true; } - + virtual void updateEncodedData(int childIndex, AppendState childAppendState, EncodeBitstreamParams& params) const { } virtual void elementEncodeComplete(EncodeBitstreamParams& params) const { } /// Override to serialize the state of this element. This is used for persistance and for transmission across the network. - virtual AppendState appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const + virtual AppendState appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const { return COMPLETED; } - + /// Override to deserialize the state of this element. This is used for loading from a persisted file or from reading /// from the network. - virtual int readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) + virtual int readElementDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) { return 0; } /// Override to indicate that the item is currently rendered in the rendering engine. By default we assume that if @@ -106,7 +106,7 @@ public: /// where an element is not actually rendering all should render elements. If the isRendered() state doesn't match the /// shouldRender() state, the tree will remark elements as changed even in cases there the elements have not changed. virtual bool isRendered() const { return getShouldRender(); } - + virtual bool deleteApproved() const { return true; } virtual bool canRayIntersect() const { return isLeaf(); } @@ -114,7 +114,7 @@ public: /// \param radius radius of sphere in meters /// \param[out] penetration pointing into cube from sphere /// \param penetratedObject unused - virtual bool findSpherePenetration(const glm::vec3& center, float radius, + virtual bool findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration, void** penetratedObject) const; // Base class methods you don't need to implement @@ -125,7 +125,7 @@ public: bool isParentOf(OctreeElementPointer possibleChild) const; /// handles deletion of all descendants, returns false if delete not approved - bool safeDeepDeleteChildAtIndex(int childIndex, int recursionCount = 0); + bool safeDeepDeleteChildAtIndex(int childIndex, int recursionCount = 0); const AACube& getAACube() const { return _cube; } @@ -134,8 +134,8 @@ public: int getLevel() const { return numberOfThreeBitSectionsInCode(getOctalCode()) + 1; } float getEnclosingRadius() const; - bool isInView(const ViewFrustum& viewFrustum) const { return inFrustum(viewFrustum) != ViewFrustum::OUTSIDE; } - ViewFrustum::location inFrustum(const ViewFrustum& viewFrustum) const; + bool isInView(const ViewFrustum& viewFrustum) const { return computeViewIntersection(viewFrustum) != ViewFrustum::OUTSIDE; } + ViewFrustum::intersection computeViewIntersection(const ViewFrustum& viewFrustum) const; float distanceToCamera(const ViewFrustum& viewFrustum) const; float furthestDistanceToCamera(const ViewFrustum& viewFrustum) const; @@ -257,7 +257,7 @@ protected: static std::map _mapSourceUUIDsToKeys; static std::map _mapKeysToSourceUUIDs; - unsigned char _childBitmask; // 1 byte + unsigned char _childBitmask; // 1 byte bool _falseColored : 1, /// Client only, is this voxel false colored, 1 bit _isDirty : 1, /// Client only, has this voxel changed since being rendered, 1 bit diff --git a/libraries/octree/src/OctreeHeadlessViewer.cpp b/libraries/octree/src/OctreeHeadlessViewer.cpp index 547b3ac32b..33c12b1fe5 100644 --- a/libraries/octree/src/OctreeHeadlessViewer.cpp +++ b/libraries/octree/src/OctreeHeadlessViewer.cpp @@ -27,9 +27,9 @@ void OctreeHeadlessViewer::init() { void OctreeHeadlessViewer::queryOctree() { char serverType = getMyNodeType(); PacketType packetType = getMyQueryMessageType(); - + NodeToJurisdictionMap& jurisdictions = *_jurisdictionListener->getJurisdictions(); - + bool wantExtraDebugging = false; if (wantExtraDebugging) { @@ -52,7 +52,7 @@ void OctreeHeadlessViewer::queryOctree() { _octreeQuery.setCameraNearClip(_viewFrustum.getNearClip()); _octreeQuery.setCameraFarClip(_viewFrustum.getFarClip()); _octreeQuery.setCameraEyeOffsetPosition(glm::vec3()); - _octreeQuery.setKeyholeRadius(_viewFrustum.getKeyholeRadius()); + _octreeQuery.setCameraCenterRadius(_viewFrustum.getCenterRadius()); _octreeQuery.setOctreeSizeScale(_voxelSizeScale); _octreeQuery.setBoundaryLevelAdjust(_boundaryLevelAdjust); @@ -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,9 +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); - - if (serverFrustumLocation != ViewFrustum::OUTSIDE) { + if ((bool)(_viewFrustum.calculateCubeKeyholeIntersection(serverBounds))) { inViewServers++; } } @@ -164,13 +162,7 @@ void OctreeHeadlessViewer::queryOctree() { if (foundRootDetails) { AACube serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s); - - ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds); - if (serverFrustumLocation != ViewFrustum::OUTSIDE) { - inView = true; - } else { - inView = false; - } + inView = (bool)(_viewFrustum.calculateCubeKeyholeIntersection(serverBounds)); } if (inView) { @@ -208,7 +200,7 @@ void OctreeHeadlessViewer::queryOctree() { // setup the query packet auto queryPacket = NLPacket::create(packetType); - + // read the data to our packet and set the payload size to fit the query int querySize = _octreeQuery.getBroadcastData(reinterpret_cast(queryPacket->getPayload())); queryPacket->setPayloadSize(querySize); diff --git a/libraries/octree/src/OctreeHeadlessViewer.h b/libraries/octree/src/OctreeHeadlessViewer.h index bdfebd82f6..780b68b848 100644 --- a/libraries/octree/src/OctreeHeadlessViewer.h +++ b/libraries/octree/src/OctreeHeadlessViewer.h @@ -46,7 +46,8 @@ public slots: // setters for camera attributes void setPosition(const glm::vec3& position) { _viewFrustum.setPosition(position); } void setOrientation(const glm::quat& orientation) { _viewFrustum.setOrientation(orientation); } - void setKeyholeRadius(float keyholdRadius) { _viewFrustum.setKeyholeRadius(keyholdRadius); } + void setCenterRadius(float radius) { _viewFrustum.setCenterRadius(radius); } + void setKeyholeRadius(float radius) { _viewFrustum.setCenterRadius(radius); } // TODO: remove this legacy support // setters for LOD and PPS void setVoxelSizeScale(float sizeScale) { _voxelSizeScale = sizeScale; } diff --git a/libraries/octree/src/OctreeQuery.cpp b/libraries/octree/src/OctreeQuery.cpp index 0e7f565c00..a69d88c693 100644 --- a/libraries/octree/src/OctreeQuery.cpp +++ b/libraries/octree/src/OctreeQuery.cpp @@ -64,8 +64,8 @@ int OctreeQuery::getBroadcastData(unsigned char* destinationBuffer) { memcpy(destinationBuffer, &_boundaryLevelAdjust, sizeof(_boundaryLevelAdjust)); destinationBuffer += sizeof(_boundaryLevelAdjust); - memcpy(destinationBuffer, &_keyholeRadius, sizeof(_keyholeRadius)); - destinationBuffer += sizeof(_keyholeRadius); + memcpy(destinationBuffer, &_cameraCenterRadius, sizeof(_cameraCenterRadius)); + destinationBuffer += sizeof(_cameraCenterRadius); return destinationBuffer - bufferStart; } @@ -109,9 +109,9 @@ int OctreeQuery::parseData(ReceivedMessage& message) { auto bytesRead = sourceBuffer - startPosition; auto bytesLeft = message.getSize() - bytesRead; - if (bytesLeft >= (int)sizeof(_keyholeRadius)) { - memcpy(&_keyholeRadius, sourceBuffer, sizeof(_keyholeRadius)); - sourceBuffer += sizeof(_keyholeRadius); + if (bytesLeft >= (int)sizeof(_cameraCenterRadius)) { + memcpy(&_cameraCenterRadius, sourceBuffer, sizeof(_cameraCenterRadius)); + sourceBuffer += sizeof(_cameraCenterRadius); } return sourceBuffer - startPosition; } diff --git a/libraries/octree/src/OctreeQuery.h b/libraries/octree/src/OctreeQuery.h index 7e157c4252..e446e1abc7 100644 --- a/libraries/octree/src/OctreeQuery.h +++ b/libraries/octree/src/OctreeQuery.h @@ -58,7 +58,7 @@ public: float getCameraNearClip() const { return _cameraNearClip; } float getCameraFarClip() const { return _cameraFarClip; } const glm::vec3& getCameraEyeOffsetPosition() const { return _cameraEyeOffsetPosition; } - float getKeyholeRadius() const { return _keyholeRadius; } + float getCameraCenterRadius() const { return _cameraCenterRadius; } glm::vec3 calculateCameraDirection() const; @@ -70,7 +70,7 @@ public: void setCameraNearClip(float nearClip) { _cameraNearClip = nearClip; } void setCameraFarClip(float farClip) { _cameraFarClip = farClip; } void setCameraEyeOffsetPosition(const glm::vec3& eyeOffsetPosition) { _cameraEyeOffsetPosition = eyeOffsetPosition; } - void setKeyholeRadius(float keyholeRadius) { _keyholeRadius = keyholeRadius; } + void setCameraCenterRadius(float radius) { _cameraCenterRadius = radius; } // related to Octree Sending strategies int getMaxQueryPacketsPerSecond() const { return _maxQueryPPS; } @@ -90,7 +90,7 @@ protected: float _cameraAspectRatio = 1.0f; float _cameraNearClip = 0.0f; float _cameraFarClip = 0.0f; - float _keyholeRadius { 0.0f }; + float _cameraCenterRadius { 0.0f }; glm::vec3 _cameraEyeOffsetPosition = glm::vec3(0.0f); // octree server sending items diff --git a/libraries/octree/src/ViewFrustum.cpp b/libraries/octree/src/ViewFrustum.cpp index 4c07a0c784..228e4c7d35 100644 --- a/libraries/octree/src/ViewFrustum.cpp +++ b/libraries/octree/src/ViewFrustum.cpp @@ -115,10 +115,6 @@ void ViewFrustum::calculate() { // Our ModelViewProjection : multiplication of our 3 matrices (note: model is identity, so we can drop it) _ourModelViewProjectionMatrix = _projection * view; // Remember, matrix multiplication is the other way around - - // Set up our keyhole bounding box... - glm::vec3 corner = _position - _keyholeRadius; - _keyholeBoundingCube = AACube(corner,(_keyholeRadius * 2.0f)); } //enum { TOP_PLANE = 0, BOTTOM_PLANE, LEFT_PLANE, RIGHT_PLANE, NEAR_PLANE, FAR_PLANE }; @@ -134,231 +130,129 @@ const char* ViewFrustum::debugPlaneName (int plane) const { return "Unknown"; } -ViewFrustum::location ViewFrustum::pointInKeyhole(const glm::vec3& point) const { - - ViewFrustum::location result = INTERSECT; - - float distance = glm::distance(point, _position); - if (distance > _keyholeRadius) { - result = OUTSIDE; - } else if (distance < _keyholeRadius) { - result = INSIDE; - } - - return result; -} - -// To determine if two spheres intersect, simply calculate the distance between the centers of the two spheres. -// If the distance is greater than the sum of the two sphere radii, they don’t intersect. Otherwise they intersect. -// If the distance plus the radius of sphere A is less than the radius of sphere B then, sphere A is inside of sphere B -ViewFrustum::location ViewFrustum::sphereInKeyhole(const glm::vec3& center, float radius) const { - ViewFrustum::location result = INTERSECT; - - float distance = glm::distance(center, _position); - if (distance > (radius + _keyholeRadius)) { - result = OUTSIDE; - } else if ((distance + radius) < _keyholeRadius) { - result = INSIDE; - } - - return result; -} - - -// A box is inside a sphere if all of its corners are inside the sphere -// A box intersects a sphere if any of its edges (as rays) interesect the sphere -// A box is outside a sphere if none of its edges (as rays) interesect the sphere -ViewFrustum::location ViewFrustum::cubeInKeyhole(const AACube& cube) const { - - // First check to see if the cube is in the bounding cube for the sphere, if it's not, then we can short circuit - // this and not check with sphere penetration which is more expensive - if (!_keyholeBoundingCube.contains(cube)) { - return OUTSIDE; - } - - glm::vec3 penetration; - bool intersects = cube.findSpherePenetration(_position, _keyholeRadius, penetration); - - ViewFrustum::location result = OUTSIDE; - - // if the cube intersects the sphere, then it may also be inside... calculate further - if (intersects) { - result = INTERSECT; - - // test all the corners, if they are all inside the sphere, the entire cube is in the sphere - bool allPointsInside = true; // assume the best - for (int v = BOTTOM_LEFT_NEAR; v < TOP_LEFT_FAR; v++) { - glm::vec3 vertex = cube.getVertex((BoxVertex)v); - if (!pointInKeyhole(vertex)) { - allPointsInside = false; - break; +ViewFrustum::intersection ViewFrustum::calculateCubeFrustumIntersection(const AACube& cube) const { + // only check against frustum + ViewFrustum::intersection result = INSIDE; + for(int i=0; i < 6; i++) { + const glm::vec3& normal = _planes[i].getNormal(); + // check distance to farthest cube point + if ( _planes[i].distance(cube.getFarthestVertex(normal)) < 0.0f) { + return OUTSIDE; + } else { + // check distance to nearest cube point + if (_planes[i].distance(cube.getNearestVertex(normal)) < 0.0f) { + // cube straddles the plane + result = INTERSECT; } } - - if (allPointsInside) { - result = INSIDE; - } } - return result; } -// A box is inside a sphere if all of its corners are inside the sphere -// A box intersects a sphere if any of its edges (as rays) interesect the sphere -// A box is outside a sphere if none of its edges (as rays) interesect the sphere -ViewFrustum::location ViewFrustum::boxInKeyhole(const AABox& box) const { +const float HALF_SQRT_THREE = 0.8660254f; - // First check to see if the box is in the bounding box for the sphere, if it's not, then we can short circuit - // this and not check with sphere penetration which is more expensive - if (!_keyholeBoundingCube.contains(box)) { - return OUTSIDE; +ViewFrustum::intersection ViewFrustum::calculateCubeKeyholeIntersection(const AACube& cube) const { + // check against centeral sphere + ViewFrustum::intersection sphereResult = INTERSECT; + glm::vec3 cubeOffset = cube.calcCenter() - _position; + float distance = glm::length(cubeOffset); + if (distance > EPSILON) { + glm::vec3 vertex = cube.getFarthestVertex(cubeOffset) - _position; + if (glm::dot(vertex, cubeOffset) < _centerSphereRadius * distance) { + // the most outward cube vertex is inside central sphere + return INSIDE; + } + if (!cube.touchesSphere(_position, _centerSphereRadius)) { + sphereResult = OUTSIDE; + } + } else if (_centerSphereRadius > HALF_SQRT_THREE * cube.getScale()) { + // the cube is in center of sphere and its bounding radius is inside + return INSIDE; } - glm::vec3 penetration; - bool intersects = box.findSpherePenetration(_position, _keyholeRadius, penetration); + // check against frustum + ViewFrustum::intersection frustumResult = calculateCubeFrustumIntersection(cube); - ViewFrustum::location result = OUTSIDE; - - // if the box intersects the sphere, then it may also be inside... calculate further - if (intersects) { - result = INTERSECT; - - // test all the corners, if they are all inside the sphere, the entire box is in the sphere - bool allPointsInside = true; // assume the best - for (int v = BOTTOM_LEFT_NEAR; v < TOP_LEFT_FAR; v++) { - glm::vec3 vertex = box.getVertex((BoxVertex)v); - if (!pointInKeyhole(vertex)) { - allPointsInside = false; - break; - } - } - - if (allPointsInside) { - result = INSIDE; - } - } - - return result; + return (frustumResult == OUTSIDE) ? sphereResult : frustumResult; } -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 +bool ViewFrustum::pointIntersectsFrustum(const glm::vec3& point) const { + // only check against frustum 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 false; } } - return regularResult; + return true; } -ViewFrustum::location ViewFrustum::sphereInFrustum(const glm::vec3& center, float radius) const { - ViewFrustum::location regularResult = INSIDE; - ViewFrustum::location keyholeResult = OUTSIDE; - - // If we have a keyholeRadius, check that first, since it's cheaper - if (_keyholeRadius >= 0.0f) { - keyholeResult = sphereInKeyhole(center, radius); - } - if (keyholeResult == INSIDE) { - return keyholeResult; - } - - float distance; +bool ViewFrustum::sphereIntersectsFrustum(const glm::vec3& center, float radius) const { + // only check against frustum for(int i=0; i < 6; i++) { - distance = _planes[i].distance(center); + float distance = _planes[i].distance(center); if (distance < -radius) { // This is outside the regular frustum, so just return the value from checking the keyhole - return keyholeResult; - } else if (distance < radius) { - regularResult = INTERSECT; + return false; } } - - return regularResult; + return true; } - -ViewFrustum::location ViewFrustum::cubeInFrustum(const AACube& cube) const { - - ViewFrustum::location regularResult = INSIDE; - ViewFrustum::location keyholeResult = OUTSIDE; - - // If we have a keyholeRadius, check that first, since it's cheaper - if (_keyholeRadius >= 0.0f) { - keyholeResult = cubeInKeyhole(cube); - } - if (keyholeResult == INSIDE) { - return keyholeResult; - } - - // TODO: These calculations are expensive, taking up 80% of our time in this function. - // This appears to be expensive because we have to test the distance to each plane. - // One suggested optimization is to first check against the approximated cone. We might - // also be able to test against the cone to the bounding sphere of the box. +bool ViewFrustum::boxIntersectsFrustum(const AABox& box) const { + // only check against frustum for(int i=0; i < 6; i++) { const glm::vec3& normal = _planes[i].getNormal(); - const glm::vec3& boxVertexP = cube.getVertexP(normal); - float planeToBoxVertexPDistance = _planes[i].distance(boxVertexP); - - const glm::vec3& boxVertexN = cube.getVertexN(normal); - float planeToBoxVertexNDistance = _planes[i].distance(boxVertexN); - - if (planeToBoxVertexPDistance < 0) { - // This is outside the regular frustum, so just return the value from checking the keyhole - return keyholeResult; - } else if (planeToBoxVertexNDistance < 0) { - regularResult = INTERSECT; + // check distance to farthest box point + if ( _planes[i].distance(box.getFarthestVertex(normal)) < 0.0f) { + return false; } } - return regularResult; + return true; } -ViewFrustum::location ViewFrustum::boxInFrustum(const AABox& box) const { - - ViewFrustum::location regularResult = INSIDE; - ViewFrustum::location keyholeResult = OUTSIDE; - - // If we have a keyholeRadius, check that first, since it's cheaper - if (_keyholeRadius >= 0.0f) { - keyholeResult = boxInKeyhole(box); +bool ViewFrustum::sphereIntersectsKeyhole(const glm::vec3& center, float radius) const { + // check positive touch against central sphere + if (glm::length(center - _position) <= (radius + _centerSphereRadius)) { + return true; } - if (keyholeResult == INSIDE) { - return keyholeResult; - } - - // TODO: These calculations are expensive, taking up 80% of our time in this function. - // This appears to be expensive because we have to test the distance to each plane. - // One suggested optimization is to first check against the approximated cone. We might - // also be able to test against the cone to the bounding sphere of the box. + // check negative touches against frustum planes for(int i=0; i < 6; i++) { - const glm::vec3& normal = _planes[i].getNormal(); - const glm::vec3& boxVertexP = box.getVertexP(normal); - float planeToBoxVertexPDistance = _planes[i].distance(boxVertexP); - - const glm::vec3& boxVertexN = box.getVertexN(normal); - float planeToBoxVertexNDistance = _planes[i].distance(boxVertexN); - - if (planeToBoxVertexPDistance < 0) { - // This is outside the regular frustum, so just return the value from checking the keyhole - return keyholeResult; - } else if (planeToBoxVertexNDistance < 0) { - regularResult = INTERSECT; + if ( _planes[i].distance(center) < -radius) { + return false; } } - return regularResult; + return true; +} + +bool ViewFrustum::cubeIntersectsKeyhole(const AACube& cube) const { + // check positive touch against central sphere + if (cube.touchesSphere(_position, _centerSphereRadius)) { + return true; + } + // check negative touches against frustum planes + for(int i=0; i < 6; i++) { + const glm::vec3& normal = _planes[i].getNormal(); + if ( _planes[i].distance(cube.getFarthestVertex(normal)) < 0.0f) { + return false; + } + } + return true; +} + +bool ViewFrustum::boxIntersectsKeyhole(const AABox& box) const { + // check positive touch against central sphere + if (box.touchesSphere(_position, _centerSphereRadius)) { + return true; + } + // check negative touches against frustum planes + for(int i=0; i < 6; i++) { + const glm::vec3& normal = _planes[i].getNormal(); + if ( _planes[i].distance(box.getFarthestVertex(normal)) < 0.0f) { + return false; + } + } + return true; } bool testMatches(glm::quat lhs, glm::quat rhs, float epsilon = EPSILON) { @@ -490,7 +384,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); } @@ -540,7 +434,7 @@ void ViewFrustum::printDebugDetails() const { qCDebug(octree, "_right=%f,%f,%f", (double)_right.x, (double)_right.y, (double)_right.z ); qCDebug(octree, "_fieldOfView=%f", (double)_fieldOfView); qCDebug(octree, "_aspectRatio=%f", (double)_aspectRatio); - qCDebug(octree, "_keyHoleRadius=%f", (double)_keyholeRadius); + qCDebug(octree, "_centerSphereRadius=%f", (double)_centerSphereRadius); qCDebug(octree, "_nearClip=%f", (double)_nearClip); qCDebug(octree, "_farClip=%f", (double)_farClip); qCDebug(octree, "_focalLength=%f", (double)_focalLength); @@ -804,7 +698,7 @@ float ViewFrustum::calculateRenderAccuracy(const AABox& bounds, float octreeSize // FIXME - for now, it's either visible or not visible. We want to adjust this to eventually return // a floating point for objects that have small angular size to indicate that they may be rendered // with lower preciscion - return (distanceToCamera <= visibleDistanceAtClosestScale) ? 1.0f : 0.0f; + return (distanceToCamera <= visibleDistanceAtClosestScale) ? 1.0f : 0.0f; } float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeScale) { diff --git a/libraries/octree/src/ViewFrustum.h b/libraries/octree/src/ViewFrustum.h index 89c632df8c..412d0f82f2 100644 --- a/libraries/octree/src/ViewFrustum.h +++ b/libraries/octree/src/ViewFrustum.h @@ -27,12 +27,15 @@ #include "OctreeConstants.h" #include "OctreeProjectedPolygon.h" -const float DEFAULT_KEYHOLE_RADIUS = 3.0f; +const float DEFAULT_CENTER_SPHERE_RADIUS = 3.0f; const float DEFAULT_FIELD_OF_VIEW_DEGREES = 45.0f; const float DEFAULT_ASPECT_RATIO = 16.0f/9.0f; const float DEFAULT_NEAR_CLIP = 0.08f; const float DEFAULT_FAR_CLIP = (float)HALF_TREE_SCALE; +// the "ViewFrustum" has a "keyhole" shape: a regular frustum for stuff that is "visible" with +// a central sphere for stuff that is nearby (for physics simulation). + class ViewFrustum { public: // setters for camera attributes @@ -83,18 +86,26 @@ public: const glm::vec3& getNearBottomLeft() const { return _cornersWorld[BOTTOM_LEFT_NEAR]; } const glm::vec3& getNearBottomRight() const { return _cornersWorld[BOTTOM_RIGHT_NEAR]; } - // get/set for keyhole attribute - void setKeyholeRadius(float keyholdRadius) { _keyholeRadius = keyholdRadius; } - float getKeyholeRadius() const { return _keyholeRadius; } + // get/set for central spherek attribute + void setCenterRadius(float radius) { _centerSphereRadius = radius; } + float getCenterRadius() const { return _centerSphereRadius; } void calculate(); - typedef enum {OUTSIDE, INTERSECT, INSIDE} location; + typedef enum { OUTSIDE = 0, INTERSECT, INSIDE } intersection; - 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; + /// @return INSIDE, INTERSECT, or OUTSIDE depending on how cube intersects the keyhole shape + ViewFrustum::intersection calculateCubeFrustumIntersection(const AACube& cube) const; + ViewFrustum::intersection calculateCubeKeyholeIntersection(const AACube& cube) const; + + bool pointIntersectsFrustum(const glm::vec3& point) const; + bool sphereIntersectsFrustum(const glm::vec3& center, float radius) const; + bool cubeIntersectsFrustum(const AACube& box) const; + bool boxIntersectsFrustum(const AABox& box) const; + + bool sphereIntersectsKeyhole(const glm::vec3& center, float radius) const; + bool cubeIntersectsKeyhole(const AACube& cube) const; + bool boxIntersectsKeyhole(const AABox& box) const; // some frustum comparisons bool matches(const ViewFrustum& compareTo, bool debug = false) const; @@ -114,15 +125,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; @@ -131,12 +142,6 @@ public: const ::Plane* getPlanes() const { return _planes; } private: - // Used for keyhole calculations - ViewFrustum::location pointInKeyhole(const glm::vec3& point) const; - ViewFrustum::location sphereInKeyhole(const glm::vec3& center, float radius) const; - ViewFrustum::location cubeInKeyhole(const AACube& cube) const; - ViewFrustum::location boxInKeyhole(const AABox& box) const; - // camera location/orientation attributes glm::vec3 _position; // the position in world-frame glm::quat _orientation; @@ -150,9 +155,7 @@ private: glm::vec3 _up = IDENTITY_UP; glm::vec3 _right = IDENTITY_RIGHT; - // keyhole attributes - float _keyholeRadius = DEFAULT_KEYHOLE_RADIUS; - AACube _keyholeBoundingCube; + float _centerSphereRadius = DEFAULT_CENTER_SPHERE_RADIUS; // Calculated values glm::mat4 _inverseProjection; @@ -165,7 +168,7 @@ private: float _fieldOfView = DEFAULT_FIELD_OF_VIEW_DEGREES; glm::vec4 _corners[8]; glm::vec3 _cornersWorld[8]; - ::Plane _planes[6]; // How will this be used? + ::Plane _planes[6]; // plane normals point inside frustum const char* debugPlaneName (int plane) const; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 36b065c2b8..e2b5721bd9 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -98,12 +98,12 @@ void DeferredLightingEffect::init() { } void DeferredLightingEffect::addPointLight(const glm::vec3& position, float radius, const glm::vec3& color, - float intensity) { - addSpotLight(position, radius, color, intensity); + float intensity, float falloffRadius) { + addSpotLight(position, radius, color, intensity, falloffRadius); } void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radius, const glm::vec3& color, - float intensity, const glm::quat& orientation, float exponent, float cutoff) { + float intensity, float falloffRadius, const glm::quat& orientation, float exponent, float cutoff) { unsigned int lightID = (unsigned int)(_pointLights.size() + _spotLights.size() + _globalLights.size()); if (lightID >= _allocatedLights.size()) { @@ -115,7 +115,7 @@ void DeferredLightingEffect::addSpotLight(const glm::vec3& position, float radiu lp->setMaximumRadius(radius); lp->setColor(color); lp->setIntensity(intensity); - //lp->setShowContour(quadraticAttenuation); + lp->setFalloffRadius(falloffRadius); if (exponent == 0.0f && cutoff == PI) { lp->setType(model::Light::POINT); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index d19ce9929c..7fc6d99a95 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -36,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); diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 8bba58eae2..bcddc72fa4 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -450,9 +450,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); @@ -463,7 +463,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); @@ -511,17 +511,14 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { #ifdef DEBUG_BOUNDING_PARTS { AABox partBounds = getPartBounds(_meshIndex, partIndex); - bool inView = args->_viewFrustum->boxInFrustum(partBounds) != ViewFrustum::OUTSIDE; - - glm::vec4 cubeColor; + + glm::vec4 cubeColor(1.0f, 1.0f, 0.0f, 1.0f); if (isSkinned) { cubeColor = glm::vec4(0.0f, 1.0f, 1.0f, 1.0f); - } else if (inView) { + } else if (args->_viewFrustum->boxIntersectsFrustum(partBounds)) { cubeColor = glm::vec4(1.0f, 0.0f, 1.0f, 1.0f); - } else { - cubeColor = glm::vec4(1.0f, 1.0f, 0.0f, 1.0f); } - + Transform transform; transform.setTranslation(partBounds.calcCenter()); transform.setScale(partBounds.getDimensions()); @@ -529,7 +526,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { DependencyManager::get()->renderWireCube(batch, 1.0f, cubeColor); } #endif //def DEBUG_BOUNDING_PARTS - + auto locations = args->_pipeline->locations; assert(locations); @@ -537,23 +534,23 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { bool canCauterize = args->_renderMode != RenderArgs::SHADOW_RENDER_MODE; _model->updateClusterMatrices(_transform.getTranslation(), _transform.getRotation()); bindTransform(batch, locations, canCauterize); - + //Bind the index buffer and vertex buffer and Blend shapes if needed bindMesh(batch); - + // apply material properties bindMaterial(batch, locations); - + if (args) { args->_details._materialSwitches++; } - + // Draw! { PerformanceTimer perfTimer("batch.drawIndexed()"); drawCall(batch); } - + if (args) { const int INDICES_PER_TRIANGLE = 3; args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE; diff --git a/libraries/render-utils/src/point_light.slf b/libraries/render-utils/src/point_light.slf index 298b3751b7..0cadf3a760 100644 --- a/libraries/render-utils/src/point_light.slf +++ b/libraries/render-utils/src/point_light.slf @@ -48,7 +48,7 @@ void main(void) { vec3 fragLightVec = getLightPosition(light) - fragPos.xyz; // Kill if too far from the light center - if (dot(fragLightVec, fragLightVec) > getLightSquareRadius(light)) { + if (dot(fragLightVec, fragLightVec) > getLightCutoffSquareRadius(light)) { discard; } diff --git a/libraries/render-utils/src/spot_light.slf b/libraries/render-utils/src/spot_light.slf index a4077c0edb..69f8e836aa 100644 --- a/libraries/render-utils/src/spot_light.slf +++ b/libraries/render-utils/src/spot_light.slf @@ -47,7 +47,7 @@ void main(void) { vec3 fragLightVec = getLightPosition(light) - fragPos.xyz; // Kill if too far from the light center - if (dot(fragLightVec, fragLightVec) > getLightSquareRadius(light)) { + if (dot(fragLightVec, fragLightVec) > getLightCutoffSquareRadius(light)) { discard; } diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp index 3fc6bffbd3..7824514649 100644 --- a/libraries/render/src/render/CullTask.cpp +++ b/libraries/render/src/render/CullTask.cpp @@ -29,7 +29,7 @@ void render::cullItems(const RenderContextPointer& renderContext, const CullFunc ViewFrustum* frustum = args->_viewFrustum; details._considered += (int)inItems.size(); - + // Culling / LOD for (auto item : inItems) { if (item.bound.isNull()) { @@ -39,12 +39,12 @@ void render::cullItems(const RenderContextPointer& renderContext, const CullFunc // TODO: some entity types (like lights) might want to be rendered even // when they are outside of the view frustum... - bool outOfView; + bool inView; { - PerformanceTimer perfTimer("boxInFrustum"); - outOfView = frustum->boxInFrustum(item.bound) == ViewFrustum::OUTSIDE; + PerformanceTimer perfTimer("boxIntersectsFrustum"); + inView = frustum->boxIntersectsFrustum(item.bound); } - if (!outOfView) { + if (inView) { bool bigEnoughToRender; { PerformanceTimer perfTimer("shouldRender"); @@ -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(); @@ -238,7 +238,7 @@ void CullSpatialSelection::run(const SceneContextPointer& sceneContext, const Re } bool frustumTest(const AABox& bound) { - if (_args->_viewFrustum->boxInFrustum(bound) == ViewFrustum::OUTSIDE) { + if (!_args->_viewFrustum->boxIntersectsFrustum(bound)) { _renderDetails._outOfView++; return false; } diff --git a/libraries/shared/src/AABox.cpp b/libraries/shared/src/AABox.cpp index d16bdafcec..0b453e6f36 100644 --- a/libraries/shared/src/AABox.cpp +++ b/libraries/shared/src/AABox.cpp @@ -75,32 +75,32 @@ void AABox::setBox(const glm::vec3& corner, const glm::vec3& scale) { _scale = scale; } -glm::vec3 AABox::getVertexP(const glm::vec3& normal) const { +glm::vec3 AABox::getFarthestVertex(const glm::vec3& normal) const { glm::vec3 result = _corner; - if (normal.x > 0) { + if (normal.x > 0.0f) { result.x += _scale.x; } - if (normal.y > 0) { + if (normal.y > 0.0f) { result.y += _scale.y; } - if (normal.z > 0) { + if (normal.z > 0.0f) { result.z += _scale.z; } return result; } -glm::vec3 AABox::getVertexN(const glm::vec3& normal) const { +glm::vec3 AABox::getNearestVertex(const glm::vec3& normal) const { glm::vec3 result = _corner; - if (normal.x < 0) { + if (normal.x < 0.0f) { result.x += _scale.x; } - if (normal.y < 0) { + if (normal.y < 0.0f) { result.y += _scale.y; } - if (normal.z < 0) { + if (normal.z < 0.0f) { result.z += _scale.z; } @@ -217,7 +217,7 @@ bool AABox::expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& e isWithin(start.x + axisDistance*direction.x, expandedCorner.x, expandedSize.x)); } -bool AABox::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, +bool AABox::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) const { // handle the trivial case where the box contains the origin if (contains(origin)) { @@ -281,6 +281,12 @@ bool AABox::findRayIntersection(const glm::vec3& origin, const glm::vec3& direct return false; } +bool AABox::touchesSphere(const glm::vec3& center, float radius) const { + // Avro's algorithm from this paper: http://www.mrtc.mdh.se/projects/3Dgraphics/paperF.pdf + glm::vec3 e = glm::max(_corner - center, Vectors::ZERO) + glm::max(center - _corner - _scale, Vectors::ZERO); + return glm::length2(e) <= radius * radius; +} + bool AABox::findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration) const { glm::vec4 center4 = glm::vec4(center, 1.0f); @@ -537,4 +543,4 @@ void AABox::transform(const Transform& transform) { scale(transform.getScale()); rotate(transform.getRotation()); translate(transform.getTranslation()); -} \ No newline at end of file +} diff --git a/libraries/shared/src/AABox.h b/libraries/shared/src/AABox.h index 24791445b3..ec06c60121 100644 --- a/libraries/shared/src/AABox.h +++ b/libraries/shared/src/AABox.h @@ -35,12 +35,12 @@ public: AABox(const glm::vec3& corner, const glm::vec3& dimensions); AABox(); ~AABox() {}; - + void setBox(const glm::vec3& corner, const glm::vec3& scale); void setBox(const glm::vec3& corner, float scale); - glm::vec3 getVertexP(const glm::vec3& normal) const; - glm::vec3 getVertexN(const glm::vec3& normal) const; + glm::vec3 getFarthestVertex(const glm::vec3& normal) const; // return vertex most parallel to normal + glm::vec3 getNearestVertex(const glm::vec3& normal) const; // return vertex most anti-parallel to normal const glm::vec3& getCorner() const { return _corner; } const glm::vec3& getScale() const { return _scale; } @@ -68,11 +68,12 @@ public: bool expandedContains(const glm::vec3& point, float expansion) const; bool expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& end, float expansion) const; - bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, + bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) const; + bool touchesSphere(const glm::vec3& center, float radius) const; // fast but may generate false positives bool findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration) const; bool findCapsulePenetration(const glm::vec3& start, const glm::vec3& end, float radius, glm::vec3& penetration) const; - + bool isNull() const { return _scale == glm::vec3(0.0f, 0.0f, 0.0f); } AABox clamp(const glm::vec3& min, const glm::vec3& max) const; @@ -113,7 +114,7 @@ inline bool operator==(const AABox& a, const AABox& b) { } inline QDebug operator<<(QDebug debug, const AABox& box) { - debug << "AABox[ (" + debug << "AABox[ (" << box.getCorner().x << "," << box.getCorner().y << "," << box.getCorner().z << " ) to (" << box.calcTopFarLeft().x << "," << box.calcTopFarLeft().y << "," << box.calcTopFarLeft().z << ") size: (" << box.getDimensions().x << "," << box.getDimensions().y << "," << box.getDimensions().z << ")" diff --git a/libraries/shared/src/AACube.cpp b/libraries/shared/src/AACube.cpp index bc97a6ff69..f8122ea4dc 100644 --- a/libraries/shared/src/AACube.cpp +++ b/libraries/shared/src/AACube.cpp @@ -79,32 +79,32 @@ void AACube::setBox(const glm::vec3& corner, float scale) { _scale = scale; } -glm::vec3 AACube::getVertexP(const glm::vec3& normal) const { +glm::vec3 AACube::getFarthestVertex(const glm::vec3& normal) const { glm::vec3 result = _corner; - if (normal.x > 0) { + if (normal.x > 0.0f) { result.x += _scale; } - if (normal.y > 0) { + if (normal.y > 0.0f) { result.y += _scale; } - if (normal.z > 0) { + if (normal.z > 0.0f) { result.z += _scale; } return result; } -glm::vec3 AACube::getVertexN(const glm::vec3& normal) const { +glm::vec3 AACube::getNearestVertex(const glm::vec3& normal) const { glm::vec3 result = _corner; - if (normal.x < 0) { + if (normal.x < 0.0f) { result.x += _scale; } - if (normal.y < 0) { + if (normal.y < 0.0f) { result.y += _scale; } - if (normal.z < 0) { + if (normal.z < 0.0f) { result.z += _scale; } @@ -284,6 +284,12 @@ bool AACube::findRayIntersection(const glm::vec3& origin, const glm::vec3& direc return false; } +bool AACube::touchesSphere(const glm::vec3& center, float radius) const { + // Avro's algorithm from this paper: http://www.mrtc.mdh.se/projects/3Dgraphics/paperF.pdf + glm::vec3 e = glm::max(_corner - center, Vectors::ZERO) + glm::max(center - _corner - glm::vec3(_scale), Vectors::ZERO); + return glm::length2(e) <= radius * radius; +} + bool AACube::findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration) const { glm::vec4 center4 = glm::vec4(center, 1.0f); diff --git a/libraries/shared/src/AACube.h b/libraries/shared/src/AACube.h index dab5dafdaa..87a38cb304 100644 --- a/libraries/shared/src/AACube.h +++ b/libraries/shared/src/AACube.h @@ -34,8 +34,8 @@ public: ~AACube() {}; void setBox(const glm::vec3& corner, float scale); - glm::vec3 getVertexP(const glm::vec3& normal) const; - glm::vec3 getVertexN(const glm::vec3& normal) const; + glm::vec3 getFarthestVertex(const glm::vec3& normal) const; // return vertex most parallel to normal + glm::vec3 getNearestVertex(const glm::vec3& normal) const; // return vertex most anti-parallel to normal void scale(float scale); const glm::vec3& getCorner() const { return _corner; } float getScale() const { return _scale; } @@ -56,8 +56,9 @@ public: bool touches(const AABox& otherBox) const; bool expandedContains(const glm::vec3& point, float expansion) const; bool expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& end, float expansion) const; - bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, + bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) const; + bool touchesSphere(const glm::vec3& center, float radius) const; bool findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration) const; bool findCapsulePenetration(const glm::vec3& start, const glm::vec3& end, float radius, glm::vec3& penetration) const; @@ -88,7 +89,7 @@ inline bool operator!=(const AACube& a, const AACube& b) { } inline QDebug operator<<(QDebug debug, const AACube& cube) { - debug << "AACube[ (" + debug << "AACube[ (" << cube.getCorner().x << "," << cube.getCorner().y << "," << cube.getCorner().z << " ) to (" << cube.calcTopFarLeft().x << "," << cube.calcTopFarLeft().y << "," << cube.calcTopFarLeft().z << ") size: (" << cube.getDimensions().x << "," << cube.getDimensions().y << "," << cube.getDimensions().z << ")" diff --git a/tests/octree/src/OctreeTests.cpp b/tests/octree/src/OctreeTests.cpp index d1f6fba891..ef08855d06 100644 --- a/tests/octree/src/OctreeTests.cpp +++ b/tests/octree/src/OctreeTests.cpp @@ -670,9 +670,8 @@ void OctreeTests::byteCountCodingTests() { } void OctreeTests::modelItemTests() { - bool verbose = true; - #if 0 // TODO - repair/replace these + bool verbose = true; //verbose = true; EntityTreeElementExtraEncodeData modelTreeElementExtraEncodeData; diff --git a/tests/octree/src/ViewFrustumTests.cpp b/tests/octree/src/ViewFrustumTests.cpp new file mode 100644 index 0000000000..2e9df54d83 --- /dev/null +++ b/tests/octree/src/ViewFrustumTests.cpp @@ -0,0 +1,1347 @@ +// +// ViewFrustumTests.cpp +// tests/octree/src +// +// Created by Andrew Meadows on 2016.02.19 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ViewFrustumTests.h" + +#include + +#include +#include +#include + +//#include +#include <../GLMTestUtils.h> +#include <../QTestExtensions.h> + + +const float ACCEPTABLE_FLOAT_ERROR = 1.0e-6f; +const float ACCEPTABLE_DOT_ERROR = 1.0e-5f; +const float ACCEPTABLE_CLIP_ERROR = 3e-4f; + +const glm::vec3 localRight(1.0f, 0.0f, 0.0f); +const glm::vec3 localUp(0.0f, 1.0f, 0.0f); +const glm::vec3 localForward(0.0f, 0.0f, -1.0f); + + +QTEST_MAIN(ViewFrustumTests) + +void ViewFrustumTests::testInit() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + // check frustum dimensions + QCOMPARE_WITH_ABS_ERROR(fovX, glm::radians(view.getFieldOfView()), ACCEPTABLE_FLOAT_ERROR); + QCOMPARE_WITH_ABS_ERROR(aspect, view.getAspectRatio(), ACCEPTABLE_FLOAT_ERROR); + QCOMPARE_WITH_ABS_ERROR(farClip, view.getFarClip(), ACCEPTABLE_CLIP_ERROR); + QCOMPARE_WITH_ABS_ERROR(nearClip, view.getNearClip(), ACCEPTABLE_CLIP_ERROR); + + // check transform + QCOMPARE_WITH_ABS_ERROR(view.getPosition(), center, ACCEPTABLE_FLOAT_ERROR); + float rotationDot = glm::abs(glm::dot(rotation, view.getOrientation())); + QCOMPARE_WITH_ABS_ERROR(rotationDot, 1.0f, ACCEPTABLE_DOT_ERROR); + + // check view directions + glm::vec3 expectedForward = rotation * localForward; + float forwardDot = glm::dot(expectedForward, view.getDirection()); + QCOMPARE_WITH_ABS_ERROR(forwardDot, 1.0f, ACCEPTABLE_DOT_ERROR); + + glm::vec3 expectedRight = rotation * localRight; + float rightDot = glm::dot(expectedRight, view.getRight()); + QCOMPARE_WITH_ABS_ERROR(rightDot, 1.0f, ACCEPTABLE_DOT_ERROR); + + glm::vec3 expectedUp = rotation * localUp; + float upDot = glm::dot(expectedUp, view.getUp()); + QCOMPARE_WITH_ABS_ERROR(upDot, 1.0f, ACCEPTABLE_DOT_ERROR); +} + +void ViewFrustumTests::testCubeKeyholeIntersection() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float fovY = 2.0f * asinf(sinf(0.5f * fovX) / aspect); + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = Vectors::UNIT_Y; + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + float delta = 0.1f; + float deltaAngle = 0.01f; + glm::quat elevation, swing; + glm::vec3 cubeCenter, localOffset; + + float cubeScale = 2.68f; // must be much smaller than cubeDistance for small angle approx below + glm::vec3 halfScaleOffset = 0.5f * glm::vec3(cubeScale); + float cubeDistance = farClip; + float cubeBoundingRadius = 0.5f * sqrtf(3.0f) * cubeScale; + float cubeAngle = cubeBoundingRadius / cubeDistance; // sine of small angles approximation + AACube cube(center, cubeScale); + + // farPlane + localOffset = (cubeDistance - cubeBoundingRadius - delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + localOffset = cubeDistance * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); + + localOffset = (cubeDistance + cubeBoundingRadius + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::OUTSIDE); + + // nearPlane + localOffset = (nearClip + 2.0f * cubeBoundingRadius + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + localOffset = (nearClip + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + // topPlane + angle = 0.5f * fovY; + elevation = glm::angleAxis(angle - cubeAngle - deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); + + elevation = glm::angleAxis(angle + cubeAngle + deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::OUTSIDE); + + // bottom plane + angle = -0.5f * fovY; + elevation = glm::angleAxis(angle + cubeAngle + deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); + + elevation = glm::angleAxis(angle - cubeAngle - deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::OUTSIDE); + + // right plane + angle = 0.5f * fovX; + swing = glm::angleAxis(angle - cubeAngle - deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); + + swing = glm::angleAxis(angle + cubeAngle + deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::OUTSIDE); + + // left plane + angle = -0.5f * fovX; + swing = glm::angleAxis(angle + cubeAngle + deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); + + swing = glm::angleAxis(angle - cubeAngle - deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::OUTSIDE); + + // central sphere right + localOffset = (holeRadius - cubeBoundingRadius - delta) * localRight; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + localOffset = holeRadius * localRight; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); + + localOffset = (holeRadius + cubeBoundingRadius + delta) * localRight; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::OUTSIDE); + + // central sphere up + localOffset = (holeRadius - cubeBoundingRadius - delta) * localUp; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + localOffset = holeRadius * localUp; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); + + localOffset = (holeRadius + cubeBoundingRadius + delta) * localUp; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::OUTSIDE); + + // central sphere back + localOffset = (-holeRadius + cubeBoundingRadius + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); + + localOffset = - holeRadius * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); + + localOffset = (-holeRadius - cubeBoundingRadius - delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::OUTSIDE); + + // central sphere center + float bigCubeScale = 2.0f * holeRadius / sqrtf(3.0f) - delta; + cube.setBox(center - glm::vec3(0.5f * bigCubeScale), bigCubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INSIDE); // smaller than sphere + + bigCubeScale = 2.0f * holeRadius / sqrtf(3.0f) + delta; + cube.setBox(center - glm::vec3(0.5f * bigCubeScale), bigCubeScale); + QCOMPARE(view.calculateCubeKeyholeIntersection(cube), ViewFrustum::INTERSECT); // larger than sphere +} + +void ViewFrustumTests::testCubeFrustumIntersection() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float fovY = 2.0f * asinf(sinf(0.5f * fovX) / aspect); + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = Vectors::UNIT_Y; + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + float delta = 0.1f; + float deltaAngle = 0.01f; + glm::quat elevation, swing; + glm::vec3 cubeCenter, localOffset; + + float cubeScale = 2.68f; // must be much smaller than cubeDistance for small angle approx below + glm::vec3 halfScaleOffset = 0.5f * glm::vec3(cubeScale); + float cubeDistance = farClip; + float cubeBoundingRadius = 0.5f * sqrtf(3.0f) * cubeScale; + float cubeAngle = cubeBoundingRadius / cubeDistance; // sine of small angles approximation + AACube cube(center, cubeScale); + + // farPlane + localOffset = (cubeDistance - cubeBoundingRadius - delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INSIDE); + + localOffset = cubeDistance * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INTERSECT); + + localOffset = (cubeDistance + cubeBoundingRadius + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::OUTSIDE); + + // nearPlane + localOffset = (nearClip + 2.0f * cubeBoundingRadius + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INSIDE); + + localOffset = (nearClip + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INTERSECT); + + localOffset = (nearClip - cubeBoundingRadius - delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::OUTSIDE); + + // topPlane + angle = 0.5f * fovY; + elevation = glm::angleAxis(angle - cubeAngle - deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INSIDE); + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INTERSECT); + + elevation = glm::angleAxis(angle + cubeAngle + deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::OUTSIDE); + + // bottom plane + angle = -0.5f * fovY; + elevation = glm::angleAxis(angle + cubeAngle + deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INSIDE); + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INTERSECT); + + elevation = glm::angleAxis(angle - cubeAngle - deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::OUTSIDE); + + // right plane + angle = 0.5f * fovX; + swing = glm::angleAxis(angle - cubeAngle - deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INSIDE); + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INTERSECT); + + swing = glm::angleAxis(angle + cubeAngle + deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::OUTSIDE); + + // left plane + angle = -0.5f * fovX; + swing = glm::angleAxis(angle + cubeAngle + deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INSIDE); + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::INTERSECT); + + swing = glm::angleAxis(angle - cubeAngle - deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.calculateCubeFrustumIntersection(cube), ViewFrustum::OUTSIDE); +} + +void ViewFrustumTests::testPointIntersectsFrustum() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float fovY = 2.0f * asinf(sinf(0.5f * fovX) / aspect); + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = Vectors::UNIT_Y; + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + float delta = 0.1f; + float deltaAngle = 0.01f; + glm::quat elevation, swing; + glm::vec3 point, localOffset; + float pointDistance = farClip; + + // farPlane + localOffset = (pointDistance - delta) * localForward; + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), true); // inside + + localOffset = (pointDistance + delta) * localForward; + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), false); // outside + + // nearPlane + localOffset = (nearClip + delta) * localForward; + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), true); // inside + + localOffset = (nearClip - delta) * localForward; + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), false); // outside + + // topPlane + angle = 0.5f * fovY; + elevation = glm::angleAxis(angle - deltaAngle, localRight); + localOffset = elevation * (pointDistance * localForward); + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), true); // inside + + elevation = glm::angleAxis(angle + deltaAngle, localRight); + localOffset = elevation * (pointDistance * localForward); + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), false); // outside + + // bottom plane + angle = -0.5f * fovY; + elevation = glm::angleAxis(angle + deltaAngle, localRight); + localOffset = elevation * (pointDistance * localForward); + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), true); // inside + + elevation = glm::angleAxis(angle - deltaAngle, localRight); + localOffset = elevation * (pointDistance * localForward); + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), false); // outside + + // right plane + angle = 0.5f * fovX; + swing = glm::angleAxis(angle - deltaAngle, localUp); + localOffset = swing * (pointDistance * localForward); + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), true); // inside + + swing = glm::angleAxis(angle + deltaAngle, localUp); + localOffset = swing * (pointDistance * localForward); + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), false); // outside + + // left plane + angle = -0.5f * fovX; + swing = glm::angleAxis(angle + deltaAngle, localUp); + localOffset = swing * (pointDistance * localForward); + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), true); // inside + + swing = glm::angleAxis(angle - deltaAngle, localUp); + localOffset = swing * (pointDistance * localForward); + point = center + rotation * localOffset; + QCOMPARE(view.pointIntersectsFrustum(point), false); // outside +} + +void ViewFrustumTests::testSphereIntersectsFrustum() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float fovY = 2.0f * asinf(sinf(0.5f * fovX) / aspect); + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = Vectors::UNIT_Y; + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + float delta = 0.1f; + float deltaAngle = 0.01f; + glm::quat elevation, swing; + glm::vec3 sphereCenter, localOffset; + + float sphereRadius = 2.68f; // must be much smaller than sphereDistance for small angle approx below + float sphereDistance = farClip; + float sphereAngle = sphereRadius / sphereDistance; // sine of small angles approximation + + // farPlane + localOffset = (sphereDistance - sphereRadius - delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // inside + + localOffset = (sphereDistance + sphereRadius - delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // straddle + + localOffset = (sphereDistance + sphereRadius + delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), false); // outside + + // nearPlane + localOffset = (nearClip + 2.0f * sphereRadius + delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // inside + + localOffset = (nearClip - sphereRadius + delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // straddle + + localOffset = (nearClip - sphereRadius - delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), false); // outside + + // topPlane + angle = 0.5f * fovY - sphereAngle; + elevation = glm::angleAxis(angle - deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // inside + + angle = 0.5f * fovY + sphereAngle; + elevation = glm::angleAxis(angle - deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // straddle + + elevation = glm::angleAxis(angle + deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), false); // outside + + // bottom plane + angle = -0.5f * fovY + sphereAngle; + elevation = glm::angleAxis(angle + deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // inside + + angle = -0.5f * fovY - sphereAngle; + elevation = glm::angleAxis(angle + deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // straddle + + elevation = glm::angleAxis(angle - deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), false); // outside + + // right plane + angle = 0.5f * fovX - sphereAngle; + swing = glm::angleAxis(angle - deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // inside + + angle = 0.5f * fovX + sphereAngle; + swing = glm::angleAxis(angle - deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // straddle + + swing = glm::angleAxis(angle + deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), false); // outside + + // left plane + angle = -0.5f * fovX + sphereAngle; + swing = glm::angleAxis(angle + deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // inside + + angle = -0.5f * fovX - sphereAngle; + swing = glm::angleAxis(angle + deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), true); // straddle + + swing = glm::angleAxis(angle - sphereAngle - deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsFrustum(sphereCenter, sphereRadius), false); // outside +} + +void ViewFrustumTests::testBoxIntersectsFrustum() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float fovY = 2.0f * asinf(sinf(0.5f * fovX) / aspect); + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = Vectors::UNIT_Y; + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + float delta = 0.1f; + float deltaAngle = 0.01f; + glm::quat elevation, swing; + glm::vec3 boxCenter, localOffset; + + glm::vec3 boxScale = glm::vec3(2.68f, 1.78f, 0.431f); + float boxDistance = farClip; + float boxBoundingRadius = 0.5f * glm::length(boxScale); + float boxAngle = boxBoundingRadius / boxDistance; // sine of small angles approximation + AABox box(center, boxScale); + + // farPlane + localOffset = (boxDistance - boxBoundingRadius - delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // inside + + localOffset = boxDistance * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // straddle + + localOffset = (boxDistance + boxBoundingRadius + delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), false); // outside + + // nearPlane + localOffset = (nearClip + 2.0f * boxBoundingRadius + delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // inside + + localOffset = nearClip * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // straddle + + localOffset = (nearClip - boxBoundingRadius - delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), false); // outside + + // topPlane + angle = 0.5f * fovY; + elevation = glm::angleAxis(angle - boxAngle - deltaAngle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // inside + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // straddle + + elevation = glm::angleAxis(angle + boxAngle + deltaAngle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), false); // outside + + // bottom plane + angle = -0.5f * fovY; + elevation = glm::angleAxis(angle + boxAngle + deltaAngle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // inside + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // straddle + + elevation = glm::angleAxis(angle - boxAngle - deltaAngle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), false); // outside + + // right plane + angle = 0.5f * fovX; + swing = glm::angleAxis(angle - boxAngle - deltaAngle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // inside + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // straddle + + swing = glm::angleAxis(angle + boxAngle + deltaAngle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), false); // outside + + // left plane + angle = -0.5f * fovX; + swing = glm::angleAxis(angle + boxAngle + deltaAngle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // inside + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), true); // straddle + + swing = glm::angleAxis(angle - boxAngle - deltaAngle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - 0.5f * boxScale, boxScale); + QCOMPARE(view.boxIntersectsFrustum(box), false); // outside +} + +void ViewFrustumTests::testSphereIntersectsKeyhole() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float fovY = 2.0f * asinf(sinf(0.5f * fovX) / aspect); + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = Vectors::UNIT_Y; + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + float delta = 0.1f; + float deltaAngle = 0.01f; + glm::quat elevation, swing; + glm::vec3 sphereCenter, localOffset; + + float sphereRadius = 2.68f; // must be much smaller than sphereDistance for small angle approx below + float sphereDistance = farClip; + float sphereAngle = sphereRadius / sphereDistance; // sine of small angles approximation + + // farPlane + localOffset = (sphereDistance - sphereRadius - delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside + + localOffset = sphereDistance * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle + + localOffset = (sphereDistance + sphereRadius + delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), false); // outside + + // nearPlane + localOffset = (nearClip + 2.0f * sphereRadius + delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside + + localOffset = (nearClip - sphereRadius + delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle + + localOffset = (nearClip - sphereRadius - delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // touches central sphere + + // topPlane + angle = 0.5f * fovY - sphereAngle; + elevation = glm::angleAxis(angle - deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside + + angle = 0.5f * fovY + sphereAngle; + elevation = glm::angleAxis(angle - deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle + + elevation = glm::angleAxis(angle + deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), false); // outside + + // bottom plane + angle = -0.5f * fovY + sphereAngle; + elevation = glm::angleAxis(angle + deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside + + angle = -0.5f * fovY - sphereAngle; + elevation = glm::angleAxis(angle + deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle + + elevation = glm::angleAxis(angle - deltaAngle, localRight); + localOffset = elevation * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), false); // outside + + // right plane + angle = 0.5f * fovX - sphereAngle; + swing = glm::angleAxis(angle - deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside + + angle = 0.5f * fovX + sphereAngle; + swing = glm::angleAxis(angle - deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle + + swing = glm::angleAxis(angle + deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), false); // outside + + // left plane + angle = -0.5f * fovX + sphereAngle; + swing = glm::angleAxis(angle + deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside + + angle = -0.5f * fovX - sphereAngle; + swing = glm::angleAxis(angle + deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle + + swing = glm::angleAxis(angle - sphereAngle - deltaAngle, localUp); + localOffset = swing * (sphereDistance * localForward); + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), false); // outside + + // central sphere right + localOffset = (holeRadius - sphereRadius - delta) * localRight; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside right + + localOffset = holeRadius * localRight; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle right + + localOffset = (holeRadius + sphereRadius + delta) * localRight; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), false); // outside right + + // central sphere up + localOffset = (holeRadius - sphereRadius - delta) * localUp; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside up + + localOffset = holeRadius * localUp; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle up + + localOffset = (holeRadius + sphereRadius + delta) * localUp; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), false); // outside up + + // central sphere back + localOffset = (-holeRadius + sphereRadius + delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // inside back + + localOffset = - holeRadius * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), true); // straddle back + + localOffset = (-holeRadius - sphereRadius - delta) * localForward; + sphereCenter = center + rotation * localOffset; + QCOMPARE(view.sphereIntersectsKeyhole(sphereCenter, sphereRadius), false); // outside back +} + +void ViewFrustumTests::testCubeIntersectsKeyhole() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float fovY = 2.0f * asinf(sinf(0.5f * fovX) / aspect); + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = Vectors::UNIT_Y; + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + float delta = 0.1f; + float deltaAngle = 0.01f; + glm::quat elevation, swing; + glm::vec3 cubeCenter, localOffset; + + float cubeScale = 2.68f; // must be much smaller than cubeDistance for small angle approx below + glm::vec3 halfScaleOffset = 0.5f * glm::vec3(cubeScale); + float cubeDistance = farClip; + float cubeBoundingRadius = 0.5f * sqrtf(3.0f) * cubeScale; + float cubeAngle = cubeBoundingRadius / cubeDistance; // sine of small angles approximation + AACube cube(center, cubeScale); + + // farPlane + localOffset = (cubeDistance - cubeBoundingRadius - delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); + + localOffset = cubeDistance * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); + + localOffset = (cubeDistance + cubeBoundingRadius + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), false); + + // nearPlane + localOffset = (nearClip + 2.0f * cubeBoundingRadius + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // inside + + localOffset = (nearClip + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // straddle + + localOffset = (nearClip - cubeBoundingRadius - delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // touches centeral sphere + + // topPlane + angle = 0.5f * fovY; + elevation = glm::angleAxis(angle - cubeAngle - deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // inside + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // straddle + + elevation = glm::angleAxis(angle + cubeAngle + deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), false); // outside + + // bottom plane + angle = -0.5f * fovY; + elevation = glm::angleAxis(angle + cubeAngle + deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // inside + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // straddle + + elevation = glm::angleAxis(angle - cubeAngle - deltaAngle, localRight); + localOffset = elevation * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), false); // outside + + // right plane + angle = 0.5f * fovX; + swing = glm::angleAxis(angle - cubeAngle - deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // inside + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // straddle + + swing = glm::angleAxis(angle + cubeAngle + deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), false); // outside + + // left plane + angle = -0.5f * fovX; + swing = glm::angleAxis(angle + cubeAngle + deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // inside + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // straddle + + swing = glm::angleAxis(angle - cubeAngle - deltaAngle, localUp); + localOffset = swing * (cubeDistance * localForward); + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), false); // outside + + // central sphere right + localOffset = (holeRadius - cubeBoundingRadius - delta) * localRight; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // inside right + + localOffset = holeRadius * localRight; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // straddle right + + localOffset = (holeRadius + cubeBoundingRadius + delta) * localRight; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), false); // outside right + + // central sphere up + localOffset = (holeRadius - cubeBoundingRadius - delta) * localUp; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // inside up + + localOffset = holeRadius * localUp; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // straddle up + + localOffset = (holeRadius + cubeBoundingRadius + delta) * localUp; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), false); // outside up + + // central sphere back + localOffset = (-holeRadius + cubeBoundingRadius + delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // inside back + + localOffset = - holeRadius * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), true); // straddle back + + localOffset = (-holeRadius - cubeBoundingRadius - delta) * localForward; + cubeCenter = center + rotation * localOffset; + cube.setBox(cubeCenter - halfScaleOffset, cubeScale); + QCOMPARE(view.cubeIntersectsKeyhole(cube), false); // outside back +} + +void ViewFrustumTests::testBoxIntersectsKeyhole() { + float aspect = 1.0f; + float fovX = PI / 2.0f; + float fovY = 2.0f * asinf(sinf(0.5f * fovX) / aspect); + float nearClip = 1.0f; + float farClip = 100.0f; + float holeRadius = 10.0f; + + glm::vec3 center = glm::vec3(12.3f, 4.56f, 89.7f); + + float angle = PI / 7.0f; + glm::vec3 axis = Vectors::UNIT_Y; + glm::quat rotation = glm::angleAxis(angle, axis); + + ViewFrustum view; + view.setProjection(glm::perspective(fovX, aspect, nearClip, farClip)); + view.setPosition(center); + view.setOrientation(rotation); + view.setCenterRadius(holeRadius); + view.calculate(); + + float delta = 0.1f; + float deltaAngle = 0.01f; + glm::quat elevation, swing; + glm::vec3 boxCenter, localOffset; + + glm::vec3 boxScale = glm::vec3(2.68f, 1.78f, 0.431f); // sides must be much smaller than boxDistance + glm::vec3 halfScaleOffset = 0.5f * boxScale; + float boxDistance = farClip; + float boxBoundingRadius = 0.5f * glm::length(boxScale); + float boxAngle = boxBoundingRadius / boxDistance; // sine of small angles approximation + AABox box(center, boxScale); + + // farPlane + localOffset = (boxDistance - boxBoundingRadius - delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); + + localOffset = boxDistance * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); + + localOffset = (boxDistance + boxBoundingRadius + delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), false); + + // nearPlane + localOffset = (nearClip + 2.0f * boxBoundingRadius + delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // inside + + localOffset = (nearClip + delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // straddle + + localOffset = (nearClip - boxBoundingRadius - delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // touches centeral sphere + + // topPlane + angle = 0.5f * fovY; + elevation = glm::angleAxis(angle - boxAngle - deltaAngle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // inside + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // straddle + + elevation = glm::angleAxis(angle + boxAngle + deltaAngle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), false); // outside + + // bottom plane + angle = -0.5f * fovY; + elevation = glm::angleAxis(angle + boxAngle + deltaAngle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // inside + + elevation = glm::angleAxis(angle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // straddle + + elevation = glm::angleAxis(angle - boxAngle - deltaAngle, localRight); + localOffset = elevation * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), false); // outside + + // right plane + angle = 0.5f * fovX; + swing = glm::angleAxis(angle - boxAngle - deltaAngle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // inside + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // straddle + + swing = glm::angleAxis(angle + boxAngle + deltaAngle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), false); // outside + + // left plane + angle = -0.5f * fovX; + swing = glm::angleAxis(angle + boxAngle + deltaAngle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // inside + + swing = glm::angleAxis(angle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // straddle + + swing = glm::angleAxis(angle - boxAngle - deltaAngle, localUp); + localOffset = swing * (boxDistance * localForward); + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), false); // outside + + // central sphere right + localOffset = (holeRadius - boxBoundingRadius - delta) * localRight; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // inside right + + localOffset = holeRadius * localRight; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // straddle right + + localOffset = (holeRadius + boxBoundingRadius + delta) * localRight; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), false); // outside right + + // central sphere up + localOffset = (holeRadius - boxBoundingRadius - delta) * localUp; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // inside up + + localOffset = holeRadius * localUp; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // straddle up + + localOffset = (holeRadius + boxBoundingRadius + delta) * localUp; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), false); // outside up + + // central sphere back + localOffset = (-holeRadius + boxBoundingRadius + delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // inside back + + localOffset = - holeRadius * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), true); // straddle back + + localOffset = (-holeRadius - boxBoundingRadius - delta) * localForward; + boxCenter = center + rotation * localOffset; + box.setBox(boxCenter - halfScaleOffset, boxScale); + QCOMPARE(view.boxIntersectsKeyhole(box), false); // outside back +} diff --git a/tests/octree/src/ViewFrustumTests.h b/tests/octree/src/ViewFrustumTests.h new file mode 100644 index 0000000000..a64a72e669 --- /dev/null +++ b/tests/octree/src/ViewFrustumTests.h @@ -0,0 +1,32 @@ +// +// ViewFrustumTests.h +// tests/octree/src +// +// Created by Andrew Meadows on 2016.02.19 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ViewFruxtumTests_h +#define hifi_ViewFruxtumTests_h + +#include + +class ViewFrustumTests : public QObject { + Q_OBJECT + +private slots: + void testInit(); + void testCubeFrustumIntersection(); + void testCubeKeyholeIntersection(); + void testPointIntersectsFrustum(); + void testSphereIntersectsFrustum(); + void testBoxIntersectsFrustum(); + void testSphereIntersectsKeyhole(); + void testCubeIntersectsKeyhole(); + void testBoxIntersectsKeyhole(); +}; + +#endif // hifi_ViewFruxtumTests_h diff --git a/tests/shared/src/AABoxTests.cpp b/tests/shared/src/AABoxTests.cpp new file mode 100644 index 0000000000..fd709a488c --- /dev/null +++ b/tests/shared/src/AABoxTests.cpp @@ -0,0 +1,153 @@ +// +// AABoxTests.cpp +// tests/shared/src +// +// Created by Andrew Meadows on 2016.02.19 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "AABoxTests.h" + +#include +#include +#include + +#include <../GLMTestUtils.h> +#include <../QTestExtensions.h> + + +QTEST_MAIN(AABoxTests) + +void AABoxTests::testCtorsAndSetters() { + const glm::vec3 corner(1.23f, 4.56f, 7.89f); + const glm::vec3 scale(2.34f, 7.53f, 9.14f); + + // test ctor + AABox box(corner, scale); + QCOMPARE_WITH_ABS_ERROR(box.getCorner(), corner, EPSILON); + QCOMPARE_WITH_ABS_ERROR(box.getScale(), scale, EPSILON); + + // test copy ctor + AABox copyBox(box); + QCOMPARE_WITH_ABS_ERROR(copyBox.getCorner(), corner, EPSILON); + QCOMPARE_WITH_ABS_ERROR(copyBox.getScale(), scale, EPSILON); + + // test setBox() + const glm::vec3 newCorner(9.87f, 6.54f, 3.21f); + const glm::vec3 newScale = glm::vec3(4.32f, 8.95f, 10.31f); + box.setBox(newCorner, newScale); + QCOMPARE_WITH_ABS_ERROR(box.getCorner(), newCorner, EPSILON); + QCOMPARE_WITH_ABS_ERROR(box.getScale(), newScale, EPSILON); + + // test misc + QCOMPARE_WITH_ABS_ERROR(newCorner, box.getMinimumPoint(), EPSILON); + + glm::vec3 expectedMaxCorner = newCorner + glm::vec3(newScale); + QCOMPARE_WITH_ABS_ERROR(expectedMaxCorner, box.getMaximumPoint(), EPSILON); + + glm::vec3 expectedCenter = newCorner + glm::vec3(0.5f * newScale); + QCOMPARE_WITH_ABS_ERROR(expectedCenter, box.calcCenter(), EPSILON); +} + +void AABoxTests::testContainsPoint() { + const glm::vec3 corner(4.56f, 7.89f, -1.35f); + const glm::vec3 scale(2.34f, 7.53f, 9.14f); + AABox box(corner, scale); + + float delta = 0.00001f; + glm::vec3 center = box.calcCenter(); + QCOMPARE(box.contains(center), true); + + for (int i = 0; i < 3; ++i) { + glm::vec3 halfScale = Vectors::ZERO; + halfScale[i] = 0.5f * scale[i]; + glm::vec3 deltaOffset = Vectors::ZERO; + deltaOffset[i] = delta; + + QCOMPARE(box.contains(center + halfScale + deltaOffset), false); // outside +face + QCOMPARE(box.contains(center + halfScale - deltaOffset), true); // inside +face + QCOMPARE(box.contains(center - halfScale + deltaOffset), true); // inside -face + QCOMPARE(box.contains(center - halfScale - deltaOffset), false); // outside -face + } +} + +void AABoxTests::testTouchesSphere() { + glm::vec3 corner(-4.56f, 7.89f, -1.35f); + float scale = 1.23f; + AABox box(corner, scale); + + float delta = 0.00001f; + glm::vec3 cubeCenter = box.calcCenter(); + float sphereRadius = 0.468f; + + for (int i = 0; i < 3; ++i) { + int j = (i + 1) % 3; + int k = (j + 1) % 3; + + { // faces + glm::vec3 scaleOffset = Vectors::ZERO; + scaleOffset[i] = 0.5f * scale + sphereRadius; + + glm::vec3 deltaOffset = Vectors::ZERO; + deltaOffset[i] = delta; + + // outside +face + glm::vec3 sphereCenter = cubeCenter + scaleOffset + deltaOffset; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), false); + + // inside +face + sphereCenter = cubeCenter + scaleOffset - deltaOffset; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), true); + + // inside -face + sphereCenter = cubeCenter - scaleOffset + deltaOffset; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), true); + + // outside -face + sphereCenter = cubeCenter - scaleOffset - deltaOffset; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), false); + } + + { // edges + glm::vec3 edgeOffset = Vectors::ZERO; + edgeOffset[i] = 0.5f * scale; + edgeOffset[j] = 0.5f * scale; + glm::vec3 edgeDirection = glm::normalize(edgeOffset); + glm::vec3 sphereCenter; + + // inside ij + sphereCenter = cubeCenter + edgeOffset + (sphereRadius - delta) * edgeDirection; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), true); + sphereCenter = cubeCenter - edgeOffset - (sphereRadius - delta) * edgeDirection; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), true); + + // outside ij + sphereCenter = cubeCenter + edgeOffset + (sphereRadius + delta) * edgeDirection; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), false); + sphereCenter = cubeCenter - edgeOffset - (sphereRadius + delta) * edgeDirection; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), false); + + edgeOffset[j] = 0.0f; + edgeOffset[k] = 0.5f * scale; + edgeDirection = glm::normalize(edgeOffset); + + // inside ik + sphereCenter = cubeCenter + edgeOffset + (sphereRadius - delta) * edgeDirection; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), true); + sphereCenter = cubeCenter - edgeOffset - (sphereRadius - delta) * edgeDirection; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), true); + + // outside ik + sphereCenter = cubeCenter + edgeOffset + (sphereRadius + delta) * edgeDirection; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), false); + sphereCenter = cubeCenter - edgeOffset - (sphereRadius + delta) * edgeDirection; + QCOMPARE(box.touchesSphere(sphereCenter, sphereRadius), false); + } + } +} + diff --git a/tests/shared/src/AABoxTests.h b/tests/shared/src/AABoxTests.h new file mode 100644 index 0000000000..fe7afede57 --- /dev/null +++ b/tests/shared/src/AABoxTests.h @@ -0,0 +1,28 @@ +// +// AABoxTests.h +// tests/shared/src +// +// Created by Andrew Meadows on 2016.02.19 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AABoxTests_h +#define hifi_AABoxTests_h + +#include +#include + +#include + +class AABoxTests : public QObject { + Q_OBJECT +private slots: + void testCtorsAndSetters(); + void testContainsPoint(); + void testTouchesSphere(); +}; + +#endif // hifi_AABoxTests_h diff --git a/tests/shared/src/AACubeTests.cpp b/tests/shared/src/AACubeTests.cpp new file mode 100644 index 0000000000..177daf89f1 --- /dev/null +++ b/tests/shared/src/AACubeTests.cpp @@ -0,0 +1,154 @@ +// +// AACubeTests.cpp +// tests/shared/src +// +// Created by Andrew Meadows on 2016.02.19 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "AACubeTests.h" + +#include +#include +#include + +#include <../GLMTestUtils.h> +#include <../QTestExtensions.h> + + +QTEST_MAIN(AACubeTests) + +void AACubeTests::ctorsAndSetters() { + const glm::vec3 corner(1.23f, 4.56f, 7.89f); + const float scale = 2.34f; + + // test ctor + AACube cube(corner, scale); + QCOMPARE_WITH_ABS_ERROR(cube.getCorner(), corner, EPSILON); + QCOMPARE_WITH_ABS_ERROR(cube.getScale(), scale, EPSILON); + + // test copy ctor + AACube copyCube(cube); + QCOMPARE_WITH_ABS_ERROR(copyCube.getCorner(), corner, EPSILON); + QCOMPARE_WITH_ABS_ERROR(copyCube.getScale(), scale, EPSILON); + + // test setBox() + const glm::vec3 newCorner(9.87f, 6.54f, 3.21f); + const float newScale = 4.32f; + cube.setBox(newCorner, newScale); + QCOMPARE_WITH_ABS_ERROR(cube.getCorner(), newCorner, EPSILON); + QCOMPARE_WITH_ABS_ERROR(cube.getScale(), newScale, EPSILON); + + // test misc + QCOMPARE_WITH_ABS_ERROR(cube.getMinimumPoint(), newCorner, EPSILON); + + glm::vec3 expectedMaxCorner = newCorner + glm::vec3(newScale); + QCOMPARE_WITH_ABS_ERROR(cube.getMaximumPoint(), expectedMaxCorner, EPSILON); + + glm::vec3 expectedCenter = newCorner + glm::vec3(0.5f * newScale); + QCOMPARE_WITH_ABS_ERROR(cube.calcCenter(), expectedCenter, EPSILON); +} + +void AACubeTests::containsPoint() { + const glm::vec3 corner(4.56f, 7.89f, -1.35f); + const float scale = 1.23f; + AACube cube(corner, scale); + + const float delta = scale / 1000.0f; + const glm::vec3 center = cube.calcCenter(); + QCOMPARE(cube.contains(center), true); + + for (int i = 0; i < 3; ++i) { + glm::vec3 scaleOffset = Vectors::ZERO; + scaleOffset[i] = 0.5f * scale; + + glm::vec3 deltaOffset = Vectors::ZERO; + deltaOffset[i] = delta; + + QCOMPARE(cube.contains(center + scaleOffset + deltaOffset), false); // outside +face + QCOMPARE(cube.contains(center + scaleOffset - deltaOffset), true); // inside +face + QCOMPARE(cube.contains(center - scaleOffset + deltaOffset), true); // inside -face + QCOMPARE(cube.contains(center - scaleOffset - deltaOffset), false); // outside -face + } +} + +void AACubeTests::touchesSphere() { + const glm::vec3 corner(-4.56f, 7.89f, -1.35f); + const float scale = 1.23f; + AACube cube(corner, scale); + + const float delta = scale / 1000.0f; + const glm::vec3 cubeCenter = cube.calcCenter(); + const float sphereRadius = 0.468f; + + for (int i = 0; i < 3; ++i) { + int j = (i + 1) % 3; + int k = (j + 1) % 3; + + { // faces + glm::vec3 scaleOffset = Vectors::ZERO; + scaleOffset[i] = 0.5f * scale + sphereRadius; + + glm::vec3 deltaOffset = Vectors::ZERO; + deltaOffset[i] = delta; + + // outside +face + glm::vec3 sphereCenter = cubeCenter + scaleOffset + deltaOffset; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), false); + + // inside +face + sphereCenter = cubeCenter + scaleOffset - deltaOffset; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), true); + + // inside -face + sphereCenter = cubeCenter - scaleOffset + deltaOffset; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), true); + + // outside -face + sphereCenter = cubeCenter - scaleOffset - deltaOffset; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), false); + } + + { // edges + glm::vec3 edgeOffset = Vectors::ZERO; + edgeOffset[i] = 0.5f * scale; + edgeOffset[j] = 0.5f * scale; + glm::vec3 edgeDirection = glm::normalize(edgeOffset); + glm::vec3 sphereCenter; + + // inside ij + sphereCenter = cubeCenter + edgeOffset + (sphereRadius - delta) * edgeDirection; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), true); + sphereCenter = cubeCenter - edgeOffset - (sphereRadius - delta) * edgeDirection; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), true); + + // outside ij + sphereCenter = cubeCenter + edgeOffset + (sphereRadius + delta) * edgeDirection; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), false); + sphereCenter = cubeCenter - edgeOffset - (sphereRadius + delta) * edgeDirection; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), false); + + edgeOffset[j] = 0.0f; + edgeOffset[k] = 0.5f * scale; + edgeDirection = glm::normalize(edgeOffset); + + // inside ik + sphereCenter = cubeCenter + edgeOffset + (sphereRadius - delta) * edgeDirection; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), true); + sphereCenter = cubeCenter - edgeOffset - (sphereRadius - delta) * edgeDirection; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), true); + + // outside ik + sphereCenter = cubeCenter + edgeOffset + (sphereRadius + delta) * edgeDirection; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), false); + sphereCenter = cubeCenter - edgeOffset - (sphereRadius + delta) * edgeDirection; + QCOMPARE(cube.touchesSphere(sphereCenter, sphereRadius), false); + } + } +} + diff --git a/tests/shared/src/AACubeTests.h b/tests/shared/src/AACubeTests.h new file mode 100644 index 0000000000..a2b2e08cc5 --- /dev/null +++ b/tests/shared/src/AACubeTests.h @@ -0,0 +1,28 @@ +// +// AACubeTests.h +// tests/shared/src +// +// Created by Andrew Meadows on 2016.02.19 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AACubeTests_h +#define hifi_AACubeTests_h + +#include +#include + +#include + +class AACubeTests : public QObject { + Q_OBJECT +private slots: + void ctorsAndSetters(); + void containsPoint(); + void touchesSphere(); +}; + +#endif // hifi_AACubeTests_h diff --git a/tests/shared/src/AngularConstraintTests.cpp b/tests/shared/src/AngularConstraintTests.cpp deleted file mode 100644 index a711bac709..0000000000 --- a/tests/shared/src/AngularConstraintTests.cpp +++ /dev/null @@ -1,316 +0,0 @@ -// -// AngularConstraintTests.cpp -// tests/physics/src -// -// Created by Andrew Meadows on 2014.05.30 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "AngularConstraintTests.h" - -#include - -#include -#include -#include - -#include "../QTestExtensions.h" - - -QTEST_MAIN(AngularConstraintTests) - -void AngularConstraintTests::testHingeConstraint() { - float minAngle = -PI; - float maxAngle = 0.0f; - glm::vec3 yAxis(0.0f, 1.0f, 0.0f); - glm::vec3 minAngles(0.0f, -PI, 0.0f); - glm::vec3 maxAngles(0.0f, 0.0f, 0.0f); - - AngularConstraint* c = AngularConstraint::newAngularConstraint(minAngles, maxAngles); - QVERIFY2(c != nullptr, "newAngularConstraint should make a constraint"); - { // test in middle of constraint - float angle = 0.5f * (minAngle + maxAngle); - glm::quat rotation = glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - QVERIFY2(constrained == false, "HingeConstraint should not clamp()"); - QVERIFY2(rotation == newRotation, "HingeConstraint should not change rotation"); - } - { // test just inside min edge of constraint - float angle = minAngle + 10.0f * EPSILON; - glm::quat rotation = glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - QVERIFY2(!constrained, "HingeConstraint should not clamp()"); - QVERIFY2(newRotation == rotation, "HingeConstraint should not change rotation"); - } - { // test just inside max edge of constraint - float angle = maxAngle - 10.0f * EPSILON; - glm::quat rotation = glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - QVERIFY2(!constrained, "HingeConstraint should not clamp()"); - QVERIFY2(newRotation == rotation, "HingeConstraint should not change rotation"); - } - { // test just outside min edge of constraint - float angle = minAngle - 0.001f; - glm::quat rotation = glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(minAngle, yAxis); - - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - { // test just outside max edge of constraint - float angle = maxAngle + 0.001f; - glm::quat rotation = glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, rotation, EPSILON); - } - { // test far outside min edge of constraint (wraps around to max) - float angle = minAngle - 0.75f * (TWO_PI - (maxAngle - minAngle)); - glm::quat rotation = glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - glm::quat expectedRotation = glm::angleAxis(maxAngle, yAxis); - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - { // test far outside max edge of constraint (wraps around to min) - float angle = maxAngle + 0.75f * (TWO_PI - (maxAngle - minAngle)); - glm::quat rotation = glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(minAngle, yAxis); - - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - - float ACCEPTABLE_ERROR = 1.0e-4f; - { // test nearby but off-axis rotation - float offAngle = 0.1f; - glm::quat offRotation(offAngle, glm::vec3(1.0f, 0.0f, 0.0f)); - float angle = 0.5f * (maxAngle + minAngle); - glm::quat rotation = offRotation * glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(angle, yAxis); - - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, ACCEPTABLE_ERROR); - } - { // test way off rotation > maxAngle - float offAngle = 0.5f; - glm::quat offRotation = glm::angleAxis(offAngle, glm::vec3(1.0f, 0.0f, 0.0f)); - float angle = maxAngle + 0.2f * (TWO_PI - (maxAngle - minAngle)); - glm::quat rotation = glm::angleAxis(angle, yAxis); - rotation = offRotation * glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(maxAngle, yAxis); - - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - { // test way off rotation < minAngle - float offAngle = 0.5f; - glm::quat offRotation = glm::angleAxis(offAngle, glm::vec3(1.0f, 0.0f, 0.0f)); - float angle = minAngle - 0.2f * (TWO_PI - (maxAngle - minAngle)); - glm::quat rotation = glm::angleAxis(angle, yAxis); - rotation = offRotation * glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(minAngle, yAxis); - - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - { // test way off rotation > maxAngle with wrap over to minAngle - float offAngle = -0.5f; - glm::quat offRotation = glm::angleAxis(offAngle, glm::vec3(1.0f, 0.0f, 0.0f)); - float angle = maxAngle + 0.6f * (TWO_PI - (maxAngle - minAngle)); - glm::quat rotation = glm::angleAxis(angle, yAxis); - rotation = offRotation * glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(minAngle, yAxis); - - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - { // test way off rotation < minAngle with wrap over to maxAngle - float offAngle = -0.6f; - glm::quat offRotation = glm::angleAxis(offAngle, glm::vec3(1.0f, 0.0f, 0.0f)); - float angle = minAngle - 0.7f * (TWO_PI - (maxAngle - minAngle)); - glm::quat rotation = glm::angleAxis(angle, yAxis); - rotation = offRotation * glm::angleAxis(angle, yAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(maxAngle, yAxis); - - QVERIFY2(constrained, "HingeConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "HingeConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - delete c; -} - -void AngularConstraintTests::testConeRollerConstraint() { - float minAngleX = -PI / 5.0f; - float minAngleY = -PI / 5.0f; - float minAngleZ = -PI / 8.0f; - - float maxAngleX = PI / 4.0f; - float maxAngleY = PI / 3.0f; - float maxAngleZ = PI / 4.0f; - - glm::vec3 minAngles(minAngleX, minAngleY, minAngleZ); - glm::vec3 maxAngles(maxAngleX, maxAngleY, maxAngleZ); - AngularConstraint* c = AngularConstraint::newAngularConstraint(minAngles, maxAngles); - - float expectedConeAngle = 0.25f * (maxAngleX - minAngleX + maxAngleY - minAngleY); - glm::vec3 middleAngles = 0.5f * (maxAngles + minAngles); - glm::quat yaw = glm::angleAxis(middleAngles[1], glm::vec3(0.0f, 1.0f, 0.0f)); - glm::quat pitch = glm::angleAxis(middleAngles[0], glm::vec3(1.0f, 0.0f, 0.0f)); - glm::vec3 expectedConeAxis = pitch * yaw * glm::vec3(0.0f, 0.0f, 1.0f); - - glm::vec3 xAxis(1.0f, 0.0f, 0.0f); - glm::vec3 perpAxis = glm::normalize(xAxis - glm::dot(xAxis, expectedConeAxis) * expectedConeAxis); - - QVERIFY2(c != nullptr, "newAngularConstraint() should make a constraint"); - { // test in middle of constraint - glm::vec3 angles(PI/20.0f, 0.0f, PI/10.0f); - glm::quat rotation(angles); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - QVERIFY2(!constrained, "ConeRollerConstraint should not clamp()"); - QVERIFY2(newRotation == rotation, "ConeRollerConstraint should not change rotation"); - } - float deltaAngle = 0.001f; - { // test just inside edge of cone - glm::quat rotation = glm::angleAxis(expectedConeAngle - deltaAngle, perpAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - QVERIFY2(!constrained, "ConeRollerConstraint should not clamp()"); - QVERIFY2(newRotation == rotation, "ConeRollerConstraint should not change rotation"); - } - { // test just outside edge of cone - glm::quat rotation = glm::angleAxis(expectedConeAngle + deltaAngle, perpAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - QVERIFY2(constrained, "ConeRollerConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "ConeRollerConstraint should change rotation"); - } - { // test just inside min edge of roll - glm::quat rotation = glm::angleAxis(minAngleZ + deltaAngle, expectedConeAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - QVERIFY2(!constrained, "ConeRollerConstraint should not clamp()"); - QVERIFY2(newRotation == rotation, "ConeRollerConstraint should not change rotation"); - } - { // test just inside max edge of roll - glm::quat rotation = glm::angleAxis(maxAngleZ - deltaAngle, expectedConeAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - QVERIFY2(!constrained, "ConeRollerConstraint should not clamp()"); - QVERIFY2(newRotation == rotation, "ConeRollerConstraint should not change rotation"); - } - { // test just outside min edge of roll - glm::quat rotation = glm::angleAxis(minAngleZ - deltaAngle, expectedConeAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(minAngleZ, expectedConeAxis); - - QVERIFY2(constrained, "ConeRollerConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "ConeRollerConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - { // test just outside max edge of roll - glm::quat rotation = glm::angleAxis(maxAngleZ + deltaAngle, expectedConeAxis); - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - glm::quat expectedRotation = glm::angleAxis(maxAngleZ, expectedConeAxis); - - QVERIFY2(constrained, "ConeRollerConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "ConeRollerConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - deltaAngle = 0.25f * expectedConeAngle; - { // test far outside cone and min roll - glm::quat roll = glm::angleAxis(minAngleZ - deltaAngle, expectedConeAxis); - glm::quat pitchYaw = glm::angleAxis(expectedConeAngle + deltaAngle, perpAxis); - glm::quat rotation = pitchYaw * roll; - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - glm::quat expectedRoll = glm::angleAxis(minAngleZ, expectedConeAxis); - glm::quat expectedPitchYaw = glm::angleAxis(expectedConeAngle, perpAxis); - glm::quat expectedRotation = expectedPitchYaw * expectedRoll; - - QVERIFY2(constrained, "ConeRollerConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "ConeRollerConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - { // test far outside cone and max roll - glm::quat roll = glm::angleAxis(maxAngleZ + deltaAngle, expectedConeAxis); - glm::quat pitchYaw = glm::angleAxis(- expectedConeAngle - deltaAngle, perpAxis); - glm::quat rotation = pitchYaw * roll; - - glm::quat newRotation = rotation; - bool constrained = c->clamp(newRotation); - - glm::quat expectedRoll = glm::angleAxis(maxAngleZ, expectedConeAxis); - glm::quat expectedPitchYaw = glm::angleAxis(- expectedConeAngle, perpAxis); - glm::quat expectedRotation = expectedPitchYaw * expectedRoll; - - QVERIFY2(constrained, "ConeRollerConstraint should clamp()"); - QVERIFY2(newRotation != rotation, "ConeRollerConstraint should change rotation"); - QCOMPARE_WITH_ABS_ERROR(newRotation, expectedRotation, EPSILON); - } - delete c; -} - diff --git a/tests/shared/src/AngularConstraintTests.h b/tests/shared/src/AngularConstraintTests.h deleted file mode 100644 index 705639b571..0000000000 --- a/tests/shared/src/AngularConstraintTests.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// AngularConstraintTests.h -// tests/physics/src -// -// Created by Andrew Meadows on 2014.05.30 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_AngularConstraintTests_h -#define hifi_AngularConstraintTests_h - -#include -#include - -class AngularConstraintTests : public QObject { - Q_OBJECT -private slots: - void testHingeConstraint(); - void testConeRollerConstraint(); -}; - -float getErrorDifference(const glm::quat& a, const glm::quat& b); - -#endif // hifi_AngularConstraintTests_h diff --git a/unpublishedScripts/DomainContent/Home/tiltMaze/createTiltMaze.js b/unpublishedScripts/DomainContent/Home/tiltMaze/createTiltMaze.js new file mode 100644 index 0000000000..7779bb7a07 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/tiltMaze/createTiltMaze.js @@ -0,0 +1,272 @@ +// +// createTiltMaze.js +// +// Created by James B. Pollack @imgntn on 2/15/2016 +// Copyright 2016 High Fidelity, Inc. +// +// This script creates a maze with a ball that you can tilt to try to get to the end. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var ball, ballSpawningAnchor, ballDetector, tiltMaze, lightAtTheEnd; + +var MAZE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/DomainContent/Home/tiltMaze/newmaze_tex-4.fbx"; +var MAZE_COLLISION_HULL = "http://hifi-content.s3.amazonaws.com/DomainContent/Home/tiltMaze/newmaze_tex-3.obj"; +var MAZE_SCRIPT = Script.resolvePath('maze.js?' + Math.random()); + +var SCALE = 0.5; + +var MAZE_DIMENSIONS = Vec3.multiply(SCALE, { + x: 1, + y: 0.15, + z: 1 +}); + +var BALL_DIMENSIONS = Vec3.multiply(SCALE, { + x: 0.035, + y: 0.035, + z: 0.035 +}); + +var BALL_SPAWNER_DIMENSIONS = Vec3.multiply(SCALE, { + x: 0.05, + y: 0.05, + z: 0.05 +}); + +var BALL_DETECTOR_DIMENSIONS = Vec3.multiply(SCALE, { + x: 0.1, + y: 0.1, + z: 0.1 +}); + +var BALL_COLOR = { + red: 255, + green: 0, + blue: 0 +}; + +var DEBUG_COLOR = { + red: 0, + green: 255, + blue: 0 +}; + +var center = Vec3.sum(Vec3.sum(MyAvatar.position, { + x: 0, + y: 0.5, + z: 0 +}), Vec3.multiply(1.5, Quat.getFront(Camera.getOrientation()))); + +var CLEANUP = true; + +var BALL_FORWARD_OFFSET = -0.2 * SCALE; +var BALL_RIGHT_OFFSET = -0.4 * SCALE; +var BALL_VERTICAL_OFFSET = 0.02 * SCALE; + +var BALL_FRICTION = 0.7; +var BALL_RESTITUTION = 0.1; +var BALL_DAMPING = 0.6; +var BALL_ANGULAR_DAMPING = 0.2; +var BALL_DENSITY = 1000; +var BALL_GRAVITY = { + x: 0, + y: -9.8, + z: 0 +}; + +var MAZE_DENSITY = 1000; +var MAZE_RESTITUTION = 0.1; +var MAZE_DAMPING = 0.6; +var MAZE_ANGULAR_DAMPING = 0.6; + +var DETECTOR_VERTICAL_OFFSET = 0.0 * SCALE; +var DETECTOR_FORWARD_OFFSET = 0.35 * SCALE; +var DETECTOR_RIGHT_OFFSET = 0.35 * SCALE; + +var END_LIGHT_COLOR = { + red: 255, + green: 0, + blue: 0 +}; + +var END_LIGHT_DIMENSIONS = { + x: 0.2, + y: 0.2, + z: 0.8 +}; + +var END_LIGHT_INTENSITY = 0.035; +var END_LIGHT_CUTOFF = 30; +var END_LIGHT_EXPONENT = 1; + +var getBallStartLocation = function() { + var mazeProps = Entities.getEntityProperties(tiltMaze); + var right = Quat.getRight(mazeProps.rotation); + var front = Quat.getFront(mazeProps.rotation); + var vertical = { + x: 0, + y: BALL_VERTICAL_OFFSET, + z: 0 + }; + + var finalOffset = Vec3.sum(vertical, Vec3.multiply(right, BALL_RIGHT_OFFSET)); + finalOffset = Vec3.sum(finalOffset, Vec3.multiply(front, BALL_FORWARD_OFFSET)); + var location = Vec3.sum(mazeProps.position, finalOffset); + return location; +}; + +var getBallFinishLocation = function() { + var mazeProps = Entities.getEntityProperties(tiltMaze); + var right = Quat.getRight(mazeProps.rotation); + var forward = Quat.getFront(mazeProps.rotation); + var up = Quat.getUp(mazeProps.rotation); + + var position = Vec3.sum(mazeProps.position, Vec3.multiply(up, DETECTOR_VERTICAL_OFFSET)); + position = Vec3.sum(position, Vec3.multiply(right, DETECTOR_RIGHT_OFFSET)); + position = Vec3.sum(position, Vec3.multiply(forward, DETECTOR_FORWARD_OFFSET)); + + return position; +}; + + +var createBall = function(position) { + var properties = { + name: 'Hifi Tilt Maze Ball', + type: 'Sphere', + position: getBallStartLocation(), + dynamic: true, + collisionless: false, + friction: BALL_FRICTION, + restitution: BALL_RESTITUTION, + angularDamping: BALL_ANGULAR_DAMPING, + damping: BALL_DAMPING, + gravity: BALL_GRAVITY, + density: BALL_DENSITY, + color: BALL_COLOR, + dimensions: BALL_DIMENSIONS, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }) + + }; + + ball = Entities.addEntity(properties); + +}; + +var createBallSpawningAnchor = function() { + var properties = { + name: 'Hifi Tilt Maze Ball Detector', + parentID: tiltMaze, + type: 'Box', + color: DEBUG_COLOR, + dimensions: BALL_SPAWNER_DIMENSIONS, + position: getBallStartLocation(), + collisionless: true, + visible: false, + }; + + ballSpawningAnchor = Entities.addEntity(properties); +}; + + +var createBallDetector = function() { + + var properties = { + name: 'Hifi Tilt Maze Ball Detector', + parentID: tiltMaze, + type: 'Box', + color: DEBUG_COLOR, + shapeType: 'none', + dimensions: BALL_DETECTOR_DIMENSIONS, + position: getBallFinishLocation(), + collisionless: true, + dynamic: false, + visible: false, + }; + + ballDetector = Entities.addEntity(properties); + +}; + +var createTiltMaze = function(position) { + var properties = { + name: 'Hifi Tilt Maze', + type: 'Model', + modelURL: MAZE_MODEL_URL, + compoundShapeURL: MAZE_COLLISION_HULL, + dimensions: MAZE_DIMENSIONS, + position: position, + restitution: MAZE_RESTITUTION, + damping: MAZE_DAMPING, + angularDamping: MAZE_ANGULAR_DAMPING, + dynamic: true, + density: MAZE_DENSITY, + script: MAZE_SCRIPT + } + + tiltMaze = Entities.addEntity(properties); + +}; + +var createLightAtTheEnd = function() { + + var mazeProps = Entities.getEntityProperties(tiltMaze, 'position'); + + var up = Quat.getUp(mazeProps.rotation); + var down = Vec3.multiply(-1, up); + + var emitOrientation = Quat.rotationBetween(Vec3.UNIT_NEG_Z, down); + + var position = getBallFinishLocation(); + var lightProperties = { + parentID: tiltMaze, + name: 'Hifi Tilt Maze End Light', + type: "Light", + isSpotlight: true, + dimensions: END_LIGHT_DIMENSIONS, + color: END_LIGHT_COLOR, + intensity: END_LIGHT_INTENSITY, + exponent: END_LIGHT_EXPONENT, + cutoff: END_LIGHT_CUTOFF, + lifetime: -1, + position: position, + rotation: emitOrientation + }; + + lightAtTheEnd = Entities.addEntity(lightProperties); +}; + +var createAll = function() { + createTiltMaze(center); + createBallSpawningAnchor(); + createBallDetector(center); + createBall(center); + createLightAtTheEnd(); + Entities.editEntity(tiltMaze, { + userData: JSON.stringify({ + tiltMaze: { + firstBall: ball, + ballSpawner: ballSpawningAnchor, + detector: ballDetector, + lightAtTheEnd: lightAtTheEnd + } + }) + }); +}; + +createAll(); + +if (CLEANUP === true) { + Script.scriptEnding.connect(function() { + Entities.deleteEntity(tiltMaze); + Entities.deleteEntity(ball); + Entities.deleteEntity(ballSpawningAnchor); + Entities.deleteEntity(lightAtTheEnd); + }); +}; \ No newline at end of file diff --git a/unpublishedScripts/DomainContent/Home/tiltMaze/maze.js b/unpublishedScripts/DomainContent/Home/tiltMaze/maze.js new file mode 100644 index 0000000000..557861ba30 --- /dev/null +++ b/unpublishedScripts/DomainContent/Home/tiltMaze/maze.js @@ -0,0 +1,212 @@ +// +// maze.js +// +// +// Created by James B. Pollack @imgntn on 2/15/2016 +// Copyright 2016 High Fidelity, Inc. +// +// This script resets a ball to its original position when the ball enters it. +// +// 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 SCALE = 0.5; + var VICTORY_SOUND; + var BALL_DISTANCE_THRESHOLD = 1 * SCALE; + + var BALL_DETECTOR_THRESHOLD = 0.075 * SCALE; + var BALL_FORWARD_OFFSET = -0.2 * SCALE; + var BALL_RIGHT_OFFSET = -0.4 * SCALE; + var BALL_VERTICAL_OFFSET = 0.02 * SCALE; + + + var BALL_FRICTION = 0.7; + var BALL_RESTITUTION = 0.1; + var BALL_DAMPING = 0.6; + var BALL_ANGULAR_DAMPING = 0.2; + var BALL_DENSITY = 1000; + var BALL_GRAVITY = { + x: 0, + y: -9.8, + z: 0 + }; + + var BALL_DIMENSIONS = Vec3.multiply(SCALE, { + x: 0.05, + y: 0.05, + z: 0.05 + }); + + var BALL_COLOR = { + red: 255, + green: 0, + blue: 0 + }; + + var _this; + + function Maze() { + _this = this; + return; + } + + Maze.prototype = { + ball: null, + ballLocked: false, + preload: function(entityID) { + this.entityID = entityID; + VICTORY_SOUND = SoundCache.getSound("http://hifi-content.s3.amazonaws.com/DomainContent/Home/tiltMaze/levelUp.wav"); + }, + startNearGrab: function() { + //check to make sure a ball is in range, otherwise create one + this.testBallDistance(); + }, + continueNearGrab: function() { + this.testWinDistance(); + this.testBallDistance(); + }, + continueDistantGrab: function() { + this.testBallDistance(); + this.testWinDistance(); + }, + releaseGrab: function() { + this.testBallDistance(); + }, + getBallStartLocation: function() { + var mazeProps = Entities.getEntityProperties(this.entityID); + var right = Quat.getRight(mazeProps.rotation); + var front = Quat.getFront(mazeProps.rotation); + var vertical = { + x: 0, + y: BALL_VERTICAL_OFFSET, + z: 0 + }; + + var finalOffset = Vec3.sum(vertical, Vec3.multiply(right, BALL_RIGHT_OFFSET)); + finalOffset = Vec3.sum(finalOffset, Vec3.multiply(front, BALL_FORWARD_OFFSET)); + var location = Vec3.sum(mazeProps.position, finalOffset); + return location; + }, + createBall: function() { + if (this.ballLocked === true) { + return; + } + + var properties = { + name: 'Hifi Tilt Maze Ball', + type: 'Sphere', + position: this.getBallStartLocation(), + dynamic: true, + collisionless: false, + friction: BALL_FRICTION, + restitution: BALL_RESTITUTION, + angularDamping: BALL_ANGULAR_DAMPING, + damping: BALL_DAMPING, + gravity: BALL_GRAVITY, + density: BALL_DENSITY, + color: BALL_COLOR, + dimensions: BALL_DIMENSIONS + }; + + this.ball = Entities.addEntity(properties); + }, + destroyBall: function() { + var results = Entities.findEntities(MyAvatar.position, 10); + results.forEach(function(result) { + var props = Entities.getEntityProperties(result, ['name']); + var isAMazeBall = props.name.indexOf('Maze Ball'); + if (isAMazeBall > -1 && result === _this.ball) { + Entities.deleteEntity(result); + } + }) + }, + testBallDistance: function() { + if (this.ballLocked === true) { + return; + } + + var userData = Entities.getEntityProperties(this.entityID, 'userData').userData; + var data = null; + try { + data = JSON.parse(userData); + } catch (e) { + // print('error parsing json in maze userdata') + } + if (data === null) { + // print('data is null in userData') + return; + } + + var ballPosition; + if (this.ball === null) { + this.ball = data.tiltMaze.firstBall; + ballPosition = Entities.getEntityProperties(data.tiltMaze.firstBall, 'position').position; + + } else { + ballPosition = Entities.getEntityProperties(this.ball, 'position').position; + } + + var ballSpawnerPosition = Entities.getEntityProperties(data.tiltMaze.ballSpawner, 'position').position; + + var separation = Vec3.distance(ballPosition, ballSpawnerPosition); + if (separation > BALL_DISTANCE_THRESHOLD) { + this.destroyBall(); + this.createBall(); + } + }, + testWinDistance: function() { + if (this.ballLocked === true) { + return; + } + // print('testing win distance') + var userData = Entities.getEntityProperties(this.entityID, 'userData').userData; + var data = null; + try { + data = JSON.parse(userData); + } catch (e) { + // print('error parsing json in maze userdata') + } + if (data === null) { + // print('data is null in userData') + return; + } + + var ballPosition; + if (this.ball === null) { + this.ball = data.tiltMaze.firstBall; + ballPosition = Entities.getEntityProperties(data.tiltMaze.firstBall, 'position').position; + } else { + ballPosition = Entities.getEntityProperties(this.ball, 'position').position; + } + + var ballDetectorPosition = Entities.getEntityProperties(data.tiltMaze.detector, 'position').position; + var separation = Vec3.distance(ballPosition, ballDetectorPosition); + if (separation < BALL_DETECTOR_THRESHOLD) { + this.ballLocked = true; + this.destroyBall(); + this.playVictorySound(); + Script.setTimeout(function() { + _this.ballLocked = false; + _this.createBall(); + }, 1500) + } + }, + playVictorySound: function() { + var position = Entities.getEntityProperties(this.entityID, "position").position; + + var audioProperties = { + volume: 0.25, + position: position + }; + Audio.playSound(VICTORY_SOUND, audioProperties); + + }, + }; + + return new Maze(); +}); \ No newline at end of file