diff --git a/Projects/Android/jni/JKXR/JKXR_SurfaceView.cpp b/Projects/Android/jni/JKXR/JKXR_SurfaceView.cpp index 8a29e1e..c075a70 100644 --- a/Projects/Android/jni/JKXR/JKXR_SurfaceView.cpp +++ b/Projects/Android/jni/JKXR/JKXR_SurfaceView.cpp @@ -445,7 +445,7 @@ void VR_FrameSetup() vr.immersive_cinematics = (vr_immersive_cinematics->value != 0.0f); } -bool VR_GetVRProjection(int eye, float zNear, float zFar, float* projection) +bool VR_GetVRProjection(int eye, float zNear, float zFar, float zZoomX, float zZoomY, float* projection) { //Don't use our projection if playing a cinematic and we are not immersive if (vr.cin_camera && !vr.immersive_cinematics) @@ -453,35 +453,23 @@ bool VR_GetVRProjection(int eye, float zNear, float zFar, float* projection) return false; } - if (!vr.cgzoommode) + //Just use game-calculated FOV when showing the quad screen + if (vr.using_screen_layer) { - - if (strstr(gAppState.OpenXRHMD, "meta") != NULL) - { - XrFovf fov = {}; - for (int eye = 0; eye < ovrMaxNumEyes; eye++) { - fov.angleLeft += gAppState.Projections[eye].fov.angleLeft / 2.0f; - fov.angleRight += gAppState.Projections[eye].fov.angleRight / 2.0f; - fov.angleUp += gAppState.Projections[eye].fov.angleUp / 2.0f; - fov.angleDown += gAppState.Projections[eye].fov.angleDown / 2.0f; - } - XrMatrix4x4f_CreateProjectionFov( - &(gAppState.ProjectionMatrices[eye]), GRAPHICS_OPENGL_ES, - fov, zNear, zFar); - } - - if (strstr(gAppState.OpenXRHMD, "pico") != NULL) - { - XrMatrix4x4f_CreateProjectionFov( - &(gAppState.ProjectionMatrices[eye]), GRAPHICS_OPENGL_ES, - gAppState.Projections[eye].fov, zNear, zFar); - } - - memcpy(projection, gAppState.ProjectionMatrices[eye].m, 16 * sizeof(float)); - return true; + return false; } - return false; + XrFovf fov = gAppState.Views[eye].fov; + fov.angleLeft /= zZoomX; + fov.angleRight /= zZoomX; + fov.angleUp /= zZoomY; + fov.angleDown /= zZoomY; + + XrMatrix4x4f_CreateProjectionFov( + (XrMatrix4x4f*)projection, GRAPHICS_OPENGL, + fov, zNear, zFar); + + return true; } diff --git a/Projects/Android/jni/JKXR/OpenXrInput.cpp b/Projects/Android/jni/JKXR/OpenXrInput.cpp index 7005f30..4db013b 100644 --- a/Projects/Android/jni/JKXR/OpenXrInput.cpp +++ b/Projects/Android/jni/JKXR/OpenXrInput.cpp @@ -439,7 +439,7 @@ void TBXR_UpdateControllers( ) XrSpaceLocation loc = {}; loc.type = XR_TYPE_SPACE_LOCATION; loc.next = &vel; - XrResult res = xrLocateSpace(aimSpace[i], gAppState.CurrentSpace, gAppState.FrameState.predictedDisplayTime, &loc); + XrResult res = xrLocateSpace(aimSpace[i], gAppState.StageSpace, gAppState.FrameState.predictedDisplayTime, &loc); if (res != XR_SUCCESS) { Com_Printf("xrLocateSpace error: %d", (int)res); } diff --git a/Projects/Android/jni/JKXR/TBXR_Common.cpp b/Projects/Android/jni/JKXR/TBXR_Common.cpp index 96fae45..6fc0b0d 100644 --- a/Projects/Android/jni/JKXR/TBXR_Common.cpp +++ b/Projects/Android/jni/JKXR/TBXR_Common.cpp @@ -874,10 +874,11 @@ void ovrApp_Clear(ovrApp* app) { memset(&app->ViewportConfig, 0, sizeof(XrViewConfigurationProperties)); memset(&app->ViewConfigurationView, 0, ovrMaxNumEyes * sizeof(XrViewConfigurationView)); app->SystemId = XR_NULL_SYSTEM_ID; + + app->LocalSpace = XR_NULL_HANDLE; app->HeadSpace = XR_NULL_HANDLE; app->StageSpace = XR_NULL_HANDLE; - app->FakeStageSpace = XR_NULL_HANDLE; - app->CurrentSpace = XR_NULL_HANDLE; + app->SessionActive = false; app->SupportedDisplayRefreshRates = NULL; app->RequestedDisplayRefreshRateIndex = 0; @@ -1335,6 +1336,7 @@ void TBXR_InitialiseResolution() XrViewConfigurationProperties viewportConfig; viewportConfig.type = XR_TYPE_VIEW_CONFIGURATION_PROPERTIES; + viewportConfig.next = NULL; OXR(xrGetViewConfigurationProperties( gAppState.Instance, gAppState.SystemId, viewportConfigType, &viewportConfig)); ALOGV( @@ -1424,12 +1426,8 @@ void TBXR_EnterVR( ) { void TBXR_LeaveVR( ) { if (gAppState.Session) { OXR(xrDestroySpace(gAppState.HeadSpace)); - // StageSpace is optional. - if (gAppState.StageSpace != XR_NULL_HANDLE) { - OXR(xrDestroySpace(gAppState.StageSpace)); - } - OXR(xrDestroySpace(gAppState.FakeStageSpace)); - gAppState.CurrentSpace = XR_NULL_HANDLE; + OXR(xrDestroySpace(gAppState.LocalSpace)); + OXR(xrDestroySpace(gAppState.StageSpace)); OXR(xrDestroySession(gAppState.Session)); gAppState.Session = NULL; } @@ -1554,14 +1552,12 @@ void TBXR_InitRenderer( ) { free(referenceSpaces); - if (gAppState.CurrentSpace == XR_NULL_HANDLE) { - TBXR_Recenter(); - } + TBXR_Recenter(); - gAppState.Projections = (XrView*)(malloc(ovrMaxNumEyes * sizeof(XrView))); + gAppState.Views = (XrView*)(malloc(ovrMaxNumEyes * sizeof(XrView))); for (int eye = 0; eye < ovrMaxNumEyes; eye++) { - memset(&gAppState.Projections[eye], 0, sizeof(XrView)); - gAppState.Projections[eye].type = XR_TYPE_VIEW; + memset(&gAppState.Views[eye], 0, sizeof(XrView)); + gAppState.Views[eye].type = XR_TYPE_VIEW; } if (strstr(gAppState.OpenXRHMD, "pico") != NULL) @@ -1585,7 +1581,7 @@ void TBXR_InitRenderer( ) { void VR_DestroyRenderer( ) { ovrRenderer_Destroy(&gAppState.Renderer); - free(gAppState.Projections); + free(gAppState.Views); } void TBXR_InitialiseOpenXR() @@ -1710,57 +1706,29 @@ void TBXR_Recenter() { XrReferenceSpaceCreateInfo spaceCreateInfo = {}; spaceCreateInfo.type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO; spaceCreateInfo.poseInReferenceSpace.orientation.w = 1.0f; - if (gAppState.CurrentSpace != XR_NULL_HANDLE) { + if (gAppState.StageSpace != XR_NULL_HANDLE) { vec3_t rotation = {0, 0, 0}; XrSpaceLocation loc = {}; loc.type = XR_TYPE_SPACE_LOCATION; - OXR(xrLocateSpace(gAppState.HeadSpace, gAppState.CurrentSpace, gAppState.FrameState.predictedDisplayTime, &loc)); + OXR(xrLocateSpace(gAppState.HeadSpace, gAppState.StageSpace, gAppState.FrameState.predictedDisplayTime, &loc)); QuatToYawPitchRoll(loc.pose.orientation, rotation, vr.hmdorientation); - - spaceCreateInfo.poseInReferenceSpace.orientation.x = 0; - spaceCreateInfo.poseInReferenceSpace.orientation.y = 0; - spaceCreateInfo.poseInReferenceSpace.orientation.z = 0; - spaceCreateInfo.poseInReferenceSpace.orientation.w = 1; } // Delete previous space instances if (gAppState.StageSpace != XR_NULL_HANDLE) { OXR(xrDestroySpace(gAppState.StageSpace)); } - if (gAppState.FakeStageSpace != XR_NULL_HANDLE) { - OXR(xrDestroySpace(gAppState.FakeStageSpace)); + if (gAppState.LocalSpace != XR_NULL_HANDLE) { + OXR(xrDestroySpace(gAppState.LocalSpace)); } - // Create a default stage space to use if SPACE_TYPE_STAGE is not - // supported, or calls to xrGetReferenceSpaceBoundsRect fail. + spaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + spaceCreateInfo.poseInReferenceSpace.position.y = 0.0f; + OXR(xrCreateReferenceSpace(gAppState.Session, &spaceCreateInfo, &gAppState.StageSpace)); + ALOGV("Created stage space"); + spaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; - spaceCreateInfo.poseInReferenceSpace.position.y = -1.6750f; - OXR(xrCreateReferenceSpace(gAppState.Session, &spaceCreateInfo, &gAppState.FakeStageSpace)); - ALOGV("Created fake stage space from local space with offset"); - gAppState.CurrentSpace = gAppState.FakeStageSpace; - - if (stageSupported) { - spaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; - spaceCreateInfo.poseInReferenceSpace.position.y = 0.0f; - OXR(xrCreateReferenceSpace(gAppState.Session, &spaceCreateInfo, &gAppState.StageSpace)); - ALOGV("Created stage space"); - gAppState.CurrentSpace = gAppState.StageSpace; - } -} - -void TBXR_UpdateStageBounds() { - XrExtent2Df stageBounds = {}; - - XrResult result; - OXR(result = xrGetReferenceSpaceBoundsRect( - gAppState.Session, XR_REFERENCE_SPACE_TYPE_STAGE, &stageBounds)); - if (result != XR_SUCCESS) { - ALOGE("Stage bounds query failed: using small defaults"); - stageBounds.width = 1.0f; - stageBounds.height = 1.0f; - - gAppState.CurrentSpace = gAppState.FakeStageSpace; - } + OXR(xrCreateReferenceSpace(gAppState.Session, &spaceCreateInfo, &gAppState.LocalSpace)); } void TBXR_WaitForSessionActive() @@ -1786,7 +1754,7 @@ static void TBXR_GetHMDOrientation() { // The better the prediction, the less black will be pulled in at the edges. XrSpaceLocation loc = {}; loc.type = XR_TYPE_SPACE_LOCATION; - OXR(xrLocateSpace(gAppState.HeadSpace, gAppState.CurrentSpace, gAppState.FrameState.predictedDisplayTime, &loc)); + OXR(xrLocateSpace(gAppState.HeadSpace, gAppState.StageSpace, gAppState.FrameState.predictedDisplayTime, &loc)); gAppState.xfStageFromHead = loc.pose; const XrQuaternionf quatHmd = gAppState.xfStageFromHead.orientation; @@ -1812,7 +1780,6 @@ void TBXR_FrameSetup() { TBXR_ProcessMessageQueue(); - GLboolean stageBoundsDirty = GL_TRUE; if (ovrApp_HandleXrEvents(&gAppState)) { TBXR_Recenter(); @@ -1823,12 +1790,6 @@ void TBXR_FrameSetup() continue; } - if (stageBoundsDirty) - { - TBXR_UpdateStageBounds(); - stageBoundsDirty = GL_FALSE; - } - break; } @@ -1901,10 +1862,16 @@ void TBXR_ClearFrameBuffer(int width, int height) void TBXR_prepareEyeBuffer(int eye ) { + vr.eye = eye; ovrFramebuffer* frameBuffer = &(gAppState.Renderer.FrameBuffer[eye]); ovrFramebuffer_Acquire(frameBuffer); ovrFramebuffer_SetCurrent(frameBuffer); TBXR_ClearFrameBuffer(frameBuffer->ColorSwapChain.Width, frameBuffer->ColorSwapChain.Height); + + //Seems odd, but used to move the HUD elements to be central on the player's view + //HMDs with a symmetric fov (like the PICO) will have 0 in this value, but the Meta Quest + //will have an asymmetric fov and the HUD would be very misaligned as a result + vr.off_center_fov = -(gAppState.Views[eye].fov.angleLeft + gAppState.Views[eye].fov.angleRight) / 2.0f; } void TBXR_finishEyeBuffer(int eye ) @@ -1931,7 +1898,7 @@ void TBXR_updateProjections() projectionInfo.type = XR_TYPE_VIEW_LOCATE_INFO; projectionInfo.viewConfigurationType = gAppState.ViewportConfig.viewConfigurationType; projectionInfo.displayTime = gAppState.FrameState.predictedDisplayTime; - projectionInfo.space = gAppState.HeadSpace; + projectionInfo.space = gAppState.LocalSpace; XrViewState viewState = {XR_TYPE_VIEW_STATE, NULL}; @@ -1944,7 +1911,7 @@ void TBXR_updateProjections() &viewState, projectionCapacityInput, &projectionCountOutput, - gAppState.Projections)); + gAppState.Views)); } void TBXR_submitFrame() @@ -1955,28 +1922,9 @@ void TBXR_submitFrame() TBXR_updateProjections(); - XrFovf fov = {}; - XrPosef viewTransform[2]; - - for (int eye = 0; eye < ovrMaxNumEyes; eye++) { - XrPosef xfHeadFromEye = gAppState.Projections[eye].pose; - XrPosef xfStageFromEye = XrPosef_Multiply(gAppState.xfStageFromHead, xfHeadFromEye); - viewTransform[eye] = XrPosef_Inverse(xfStageFromEye); - fov.angleLeft += gAppState.Projections[eye].fov.angleLeft / 2.0f; - fov.angleRight += gAppState.Projections[eye].fov.angleRight / 2.0f; - fov.angleUp += gAppState.Projections[eye].fov.angleUp / 2.0f; - fov.angleDown += gAppState.Projections[eye].fov.angleDown / 2.0f; - } - vr.fov_x = (fabs(fov.angleLeft) + fabs(fov.angleRight)) * 180.0f / M_PI; - vr.fov_y = (fabs(fov.angleUp) + fabs(fov.angleDown)) * 180.0f / M_PI; - - if (vr.cgzoommode) - { - fov.angleLeft /= 1.3f; - fov.angleRight /= 1.3f; - fov.angleUp /= 1.3f; - fov.angleDown /= 1.3f; - } + //Calculate the maximum extent fov for use in culling in the engine (we won't want to cull inside this fov) + vr.fov_x = (fabs(gAppState.Views[0].fov.angleLeft) + fabs(gAppState.Views[1].fov.angleLeft)) * 180.0f / M_PI; + vr.fov_y = (fabs(gAppState.Views[0].fov.angleUp) + fabs(gAppState.Views[0].fov.angleUp)) * 180.0f / M_PI; gAppState.LayerCount = 0; memset(gAppState.Layers, 0, sizeof(xrCompositorLayer_Union) * ovrMaxLayerCount); @@ -1987,16 +1935,26 @@ void TBXR_submitFrame() projection_layer.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION; projection_layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; projection_layer.layerFlags |= XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT; - projection_layer.space = gAppState.CurrentSpace; + projection_layer.space = gAppState.LocalSpace; projection_layer.viewCount = ovrMaxNumEyes; projection_layer.views = projection_layer_elements; for (int eye = 0; eye < ovrMaxNumEyes; eye++) { + + XrFovf fov = gAppState.Views[eye].fov; + if (vr.cgzoommode) + { + fov.angleLeft /= 1.3f; + fov.angleRight /= 1.3f; + fov.angleUp /= 1.3f; + fov.angleDown /= 1.3f; + } + ovrFramebuffer* frameBuffer = &gAppState.Renderer.FrameBuffer[eye]; memset(&projection_layer_elements[eye], 0, sizeof(XrCompositionLayerProjectionView)); projection_layer_elements[eye].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; - projection_layer_elements[eye].pose = gAppState.xfStageFromHead; + projection_layer_elements[eye].pose = gAppState.Views[eye].pose; projection_layer_elements[eye].fov = fov; memset(&projection_layer_elements[eye].subImage, 0, sizeof(XrSwapchainSubImage)); projection_layer_elements[eye].subImage.swapchain = @@ -2020,7 +1978,7 @@ void TBXR_submitFrame() quad_layer.type = XR_TYPE_COMPOSITION_LAYER_QUAD; quad_layer.next = NULL; quad_layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; - quad_layer.space = gAppState.CurrentSpace; + quad_layer.space = gAppState.StageSpace; quad_layer.eyeVisibility = XR_EYE_VISIBILITY_BOTH; memset(&quad_layer.subImage, 0, sizeof(XrSwapchainSubImage)); quad_layer.subImage.swapchain = gAppState.Renderer.FrameBuffer[0].ColorSwapChain.Handle; diff --git a/Projects/Android/jni/JKXR/TBXR_Common.h b/Projects/Android/jni/JKXR/TBXR_Common.h index 799f654..60342f7 100644 --- a/Projects/Android/jni/JKXR/TBXR_Common.h +++ b/Projects/Android/jni/JKXR/TBXR_Common.h @@ -235,14 +235,14 @@ typedef struct XrViewConfigurationProperties ViewportConfig; XrViewConfigurationView ViewConfigurationView[ovrMaxNumEyes]; XrSystemId SystemId; + + XrSpace LocalSpace; XrSpace HeadSpace; XrSpace StageSpace; - XrSpace FakeStageSpace; - XrSpace CurrentSpace; + GLboolean SessionActive; XrPosef xfStageFromHead; - XrView* Projections; - XrMatrix4x4f ProjectionMatrices[2]; + XrView* Views; float currentDisplayRefreshRate; @@ -303,7 +303,7 @@ void surfaceMessageQueue_PostMessage(surfaceMessageQueue * messageQueue, const s void VR_FrameSetup(); bool VR_UseScreenLayer(); float VR_GetScreenLayerDistance(); -bool VR_GetVRProjection(int eye, float zNear, float zFar, float* projection); +bool VR_GetVRProjection(int eye, float zNear, float zFar, float zZoomX, float zZoomY, float* projection); void VR_HandleControllerInput(); void VR_SetHMDOrientation(float pitch, float yaw, float roll ); void VR_SetHMDPosition(float x, float y, float z ); diff --git a/Projects/Android/jni/JKXR/VrClientInfo.h b/Projects/Android/jni/JKXR/VrClientInfo.h index df31ba3..42be67e 100644 --- a/Projects/Android/jni/JKXR/VrClientInfo.h +++ b/Projects/Android/jni/JKXR/VrClientInfo.h @@ -33,10 +33,12 @@ typedef struct { float remote_snapTurn; // how much turn has been applied to the yaw by joystick for a remote controlled entity int remote_cooldown; + int eye; bool using_screen_layer; bool third_person; float fov_x; float fov_y; + float off_center_fov; float tempWeaponVelocity; diff --git a/Projects/Android/jni/OpenJK/code/cgame/cg_drawtools.cpp b/Projects/Android/jni/OpenJK/code/cgame/cg_drawtools.cpp index dd8af8f..848a599 100644 --- a/Projects/Android/jni/OpenJK/code/cgame/cg_drawtools.cpp +++ b/Projects/Android/jni/OpenJK/code/cgame/cg_drawtools.cpp @@ -38,6 +38,9 @@ void CG_AdjustFrom640( float *x, float *y, float *w, float *h ) { xoffset *= -1; } + //We need to add an offset due to the effect of the compositor projection for asymmetric FOVs + xoffset += vr->off_center_fov * 640; + *x *= screenXScale; *y *= screenYScale; if (w != NULL) { diff --git a/Projects/Android/jni/OpenJK/code/rd-common/tr_public.h b/Projects/Android/jni/OpenJK/code/rd-common/tr_public.h index f7c91e7..ebc9f3d 100644 --- a/Projects/Android/jni/OpenJK/code/rd-common/tr_public.h +++ b/Projects/Android/jni/OpenJK/code/rd-common/tr_public.h @@ -129,7 +129,7 @@ typedef struct { //JKXR Functions bool (*TBXR_useScreenLayer) ( void ); - bool (*TBXR_GetVRProjection) (int eye, float zNear, float zFar, float* projection); + bool (*TBXR_GetVRProjection) (int eye, float zNear, float zFar, float zZoomX, float zZoomY, float* projection); } refimport_t; diff --git a/Projects/Android/jni/OpenJK/code/rd-gles/tr_backend.cpp b/Projects/Android/jni/OpenJK/code/rd-gles/tr_backend.cpp index 6cda4cc..ecbabdb 100644 --- a/Projects/Android/jni/OpenJK/code/rd-gles/tr_backend.cpp +++ b/Projects/Android/jni/OpenJK/code/rd-gles/tr_backend.cpp @@ -1431,8 +1431,6 @@ const void *RB_DrawBuffer( const void *data ) { cmd = (const drawBufferCommand_t *)data; - tr.stereoFrame = (stereoFrame_t )cmd->buffer; - // clear screen for debugging if ( r_clear->integer ) { qglClearColor( 0, 0, 0, 1 ); diff --git a/Projects/Android/jni/OpenJK/code/rd-gles/tr_local.h b/Projects/Android/jni/OpenJK/code/rd-gles/tr_local.h index 179aa84..14b8ebe 100644 --- a/Projects/Android/jni/OpenJK/code/rd-gles/tr_local.h +++ b/Projects/Android/jni/OpenJK/code/rd-gles/tr_local.h @@ -1018,7 +1018,6 @@ typedef struct { model_t *currentModel; viewParms_t viewParms; - stereoFrame_t stereoFrame; float identityLight; // 1.0 / ( 1 << overbrightBits ) int identityLightByte; // identityLight * 255 diff --git a/Projects/Android/jni/OpenJK/code/rd-gles/tr_main.cpp b/Projects/Android/jni/OpenJK/code/rd-gles/tr_main.cpp index b740e59..9d27f52 100644 --- a/Projects/Android/jni/OpenJK/code/rd-gles/tr_main.cpp +++ b/Projects/Android/jni/OpenJK/code/rd-gles/tr_main.cpp @@ -528,7 +528,7 @@ R_SetupProjection void R_SetupProjection( void ) { float xmin, xmax, ymin, ymax; float width, height, depth; - float zNear, zFar; + float zNear, zFar, zZoomX, zZoomY; // dynamically compute far clip plane distance SetFarClip(); @@ -538,14 +538,20 @@ void R_SetupProjection( void ) { // zNear = r_znear->value; zFar = tr.viewParms.zFar; + zZoomX = 1.0f; + zZoomY = 1.0f; - if (!tr.refdef.override_fov && - ri.TBXR_GetVRProjection((int)tr.stereoFrame, zNear, zFar, tr.viewParms.projectionMatrix)) + if (tr.refdef.override_fov || vr->cgzoommode) + { + zZoomX = vr->fov_x / tr.refdef.fov_x; + zZoomY = vr->fov_y / tr.refdef.fov_y; + } + + if (ri.TBXR_GetVRProjection(vr->eye, zNear, zFar, zZoomX, zZoomY, tr.viewParms.projectionMatrix)) { return; } - ymax = zNear * tan( tr.refdef.fov_y * M_PI / 360.0f ); ymin = -ymax; diff --git a/Projects/Android/jni/OpenJK/codeJK2/cgame/cg_drawtools.cpp b/Projects/Android/jni/OpenJK/codeJK2/cgame/cg_drawtools.cpp index 865d057..dc46b38 100644 --- a/Projects/Android/jni/OpenJK/codeJK2/cgame/cg_drawtools.cpp +++ b/Projects/Android/jni/OpenJK/codeJK2/cgame/cg_drawtools.cpp @@ -37,6 +37,9 @@ void CG_AdjustFrom640( float *x, float *y, float *w, float *h ) { xoffset *= -1; } + //We need to add an offset due to the effect of the compositor projection for asymmetric FOVs + xoffset += vr->off_center_fov * 640; + *x *= screenXScale; *y *= screenYScale; if (w != NULL) {