diff options
Diffstat (limited to 'intern/ghost')
24 files changed, 1030 insertions, 202 deletions
diff --git a/intern/ghost/CMakeLists.txt b/intern/ghost/CMakeLists.txt index 76cac1049fb..84f156949aa 100644 --- a/intern/ghost/CMakeLists.txt +++ b/intern/ghost/CMakeLists.txt @@ -245,10 +245,10 @@ elseif(WITH_GHOST_X11 OR WITH_GHOST_WAYLAND) if(WITH_X11_XF86VMODE) add_definitions(-DWITH_X11_XF86VMODE) list(APPEND INC_SYS - ${X11_xf86vmode_INCLUDE_PATH} + ${X11_Xxf86vmode_INCLUDE_PATH} ) list(APPEND LIB - ${X11_Xf86vmode_LIB} + ${X11_Xxf86vmode_LIB} ) endif() @@ -473,6 +473,7 @@ if(WITH_XR_OPENXR) intern/GHOST_Xr.cpp intern/GHOST_XrAction.cpp intern/GHOST_XrContext.cpp + intern/GHOST_XrControllerModel.cpp intern/GHOST_XrEvent.cpp intern/GHOST_XrGraphicsBinding.cpp intern/GHOST_XrSession.cpp @@ -482,13 +483,22 @@ if(WITH_XR_OPENXR) intern/GHOST_IXrGraphicsBinding.h intern/GHOST_XrAction.h intern/GHOST_XrContext.h + intern/GHOST_XrControllerModel.h intern/GHOST_XrException.h intern/GHOST_XrSession.h intern/GHOST_XrSwapchain.h intern/GHOST_Xr_intern.h intern/GHOST_Xr_openxr_includes.h + + # Header only library. + ../../extern/tinygltf/tiny_gltf.h + ) + list(APPEND INC + ../../extern/json/include + ../../extern/tinygltf ) list(APPEND INC_SYS + ${EIGEN3_INCLUDE_DIRS} ${XR_OPENXR_SDK_INCLUDE_DIR} ) list(APPEND LIB diff --git a/intern/ghost/GHOST_C-api.h b/intern/ghost/GHOST_C-api.h index b78aac6f5eb..98094cc0669 100644 --- a/intern/ghost/GHOST_C-api.h +++ b/intern/ghost/GHOST_C-api.h @@ -729,13 +729,6 @@ extern GHOST_TSuccess GHOST_ReleaseOpenGLContext(GHOST_ContextHandle contexthand extern unsigned int GHOST_GetContextDefaultOpenGLFramebuffer(GHOST_ContextHandle contexthandle); /** - * Returns whether a context is rendered upside down compared to OpenGL. This only needs to be - * called if there's a non-OpenGL context, which is really the exception. - * So generally, this does not need to be called. - */ -extern int GHOST_isUpsideDownContext(GHOST_ContextHandle contexthandle); - -/** * Get the OpenGL frame-buffer handle that serves as a default frame-buffer. */ extern unsigned int GHOST_GetDefaultOpenGLFramebuffer(GHOST_WindowHandle windwHandle); @@ -1140,6 +1133,30 @@ void GHOST_XrGetActionCustomdataArray(GHOST_XrContextHandle xr_context, const char *action_set_name, void **r_customdata_array); +/* controller model */ +/** + * Load the OpenXR controller model. + */ +int GHOST_XrLoadControllerModel(GHOST_XrContextHandle xr_context, const char *subaction_path); + +/** + * Unload the OpenXR controller model. + */ +void GHOST_XrUnloadControllerModel(GHOST_XrContextHandle xr_context, const char *subaction_path); + +/** + * Update component transforms for the OpenXR controller model. + */ +int GHOST_XrUpdateControllerModelComponents(GHOST_XrContextHandle xr_context, + const char *subaction_path); + +/** + * Get vertex data for the OpenXR controller model. + */ +int GHOST_XrGetControllerModelData(GHOST_XrContextHandle xr_context, + const char *subaction_path, + GHOST_XrControllerModelData *r_data); + #endif /* WITH_XR_OPENXR */ #ifdef __cplusplus diff --git a/intern/ghost/GHOST_Types.h b/intern/ghost/GHOST_Types.h index 221fa140f70..7fe9300ec3f 100644 --- a/intern/ghost/GHOST_Types.h +++ b/intern/ghost/GHOST_Types.h @@ -496,8 +496,6 @@ typedef struct { int target_start; /** Represents the position of the end of the selection */ int target_end; - /** custom temporal data */ - GHOST_TUserDataPtr tmp; } GHOST_TEventImeData; typedef struct { @@ -569,6 +567,7 @@ typedef enum { GHOST_kUserSpecialDirMusic, GHOST_kUserSpecialDirPictures, GHOST_kUserSpecialDirVideos, + GHOST_kUserSpecialDirCaches, /* Can be extended as needed. */ } GHOST_TUserSpecialDirTypes; @@ -653,8 +652,8 @@ enum { GHOST_kXrContextDebug = (1 << 0), GHOST_kXrContextDebugTime = (1 << 1), # ifdef WIN32 - /* Needed to avoid issues with the SteamVR OpenGL graphics binding (use DirectX fallback - instead). */ + /* Needed to avoid issues with the SteamVR OpenGL graphics binding + * (use DirectX fallback instead). */ GHOST_kXrContextGpuNVIDIA = (1 << 2), # endif }; @@ -753,8 +752,31 @@ typedef struct GHOST_XrActionProfileInfo { const char *profile_path; uint32_t count_subaction_paths; const char **subaction_paths; - /* Bindings for each subaction path. */ + /** Bindings for each subaction path. */ const GHOST_XrActionBindingInfo *bindings; } GHOST_XrActionProfileInfo; +typedef struct GHOST_XrControllerModelVertex { + float position[3]; + float normal[3]; +} GHOST_XrControllerModelVertex; + +typedef struct GHOST_XrControllerModelComponent { + /** World space transform. */ + float transform[4][4]; + uint32_t vertex_offset; + uint32_t vertex_count; + uint32_t index_offset; + uint32_t index_count; +} GHOST_XrControllerModelComponent; + +typedef struct GHOST_XrControllerModelData { + uint32_t count_vertices; + const GHOST_XrControllerModelVertex *vertices; + uint32_t count_indices; + const uint32_t *indices; + uint32_t count_components; + const GHOST_XrControllerModelComponent *components; +} GHOST_XrControllerModelData; + #endif /* WITH_XR_OPENXR */ diff --git a/intern/ghost/intern/GHOST_C-api.cpp b/intern/ghost/intern/GHOST_C-api.cpp index a2871b46222..a21c3a90c06 100644 --- a/intern/ghost/intern/GHOST_C-api.cpp +++ b/intern/ghost/intern/GHOST_C-api.cpp @@ -1069,4 +1069,39 @@ void GHOST_XrGetActionCustomdataArray(GHOST_XrContextHandle xr_contexthandle, xr_context); } +int GHOST_XrLoadControllerModel(GHOST_XrContextHandle xr_contexthandle, const char *subaction_path) +{ + GHOST_IXrContext *xr_context = (GHOST_IXrContext *)xr_contexthandle; + GHOST_XrSession *xr_session = xr_context->getSession(); + GHOST_XR_CAPI_CALL_RET(xr_session->loadControllerModel(subaction_path), xr_context); + return 0; +} + +void GHOST_XrUnloadControllerModel(GHOST_XrContextHandle xr_contexthandle, + const char *subaction_path) +{ + GHOST_IXrContext *xr_context = (GHOST_IXrContext *)xr_contexthandle; + GHOST_XrSession *xr_session = xr_context->getSession(); + GHOST_XR_CAPI_CALL(xr_session->unloadControllerModel(subaction_path), xr_context); +} + +int GHOST_XrUpdateControllerModelComponents(GHOST_XrContextHandle xr_contexthandle, + const char *subaction_path) +{ + GHOST_IXrContext *xr_context = (GHOST_IXrContext *)xr_contexthandle; + GHOST_XrSession *xr_session = xr_context->getSession(); + GHOST_XR_CAPI_CALL_RET(xr_session->updateControllerModelComponents(subaction_path), xr_context); + return 0; +} + +int GHOST_XrGetControllerModelData(GHOST_XrContextHandle xr_contexthandle, + const char *subaction_path, + GHOST_XrControllerModelData *r_data) +{ + GHOST_IXrContext *xr_context = (GHOST_IXrContext *)xr_contexthandle; + GHOST_XrSession *xr_session = xr_context->getSession(); + GHOST_XR_CAPI_CALL_RET(xr_session->getControllerModelData(subaction_path, *r_data), xr_context); + return 0; +} + #endif /* WITH_XR_OPENXR */ diff --git a/intern/ghost/intern/GHOST_DisplayManagerSDL.cpp b/intern/ghost/intern/GHOST_DisplayManagerSDL.cpp index 5b026eb1632..09b2e4dfe2b 100644 --- a/intern/ghost/intern/GHOST_DisplayManagerSDL.cpp +++ b/intern/ghost/intern/GHOST_DisplayManagerSDL.cpp @@ -101,8 +101,7 @@ GHOST_TSuccess GHOST_DisplayManagerSDL::setCurrentDisplaySetting( uint8_t display, const GHOST_DisplaySetting &setting) { /* - * Mode switching code ported from Quake 2 version 3.21 and bzflag version - * 2.4.0: + * Mode switching code ported from Quake 2 version 3.21 and BZFLAG version 2.4.0: * ftp://ftp.idsoftware.com/idstuff/source/q2source-3.21.zip * See linux/gl_glx.c:GLimp_SetMode * http://wiki.bzflag.org/BZFlag_Source diff --git a/intern/ghost/intern/GHOST_ImeWin32.cpp b/intern/ghost/intern/GHOST_ImeWin32.cpp index 47b5f5688df..d1fc80adf56 100644 --- a/intern/ghost/intern/GHOST_ImeWin32.cpp +++ b/intern/ghost/intern/GHOST_ImeWin32.cpp @@ -106,7 +106,7 @@ bool GHOST_ImeWin32::IsImeKeyEvent(char ascii) if (IsLanguage(IMELANG_JAPANESE) && (ascii >= ' ' && ascii <= '~')) { return true; } - else if (IsLanguage(IMELANG_CHINESE) && ascii && strchr("!\"$'(),.:;<>?[\\]^_`", ascii)) { + else if (IsLanguage(IMELANG_CHINESE) && ascii && strchr("!\"$'(),.:;<>?[\\]^_`/", ascii)) { return true; } } diff --git a/intern/ghost/intern/GHOST_SystemCocoa.mm b/intern/ghost/intern/GHOST_SystemCocoa.mm index 189e663f91a..b92c3e73a88 100644 --- a/intern/ghost/intern/GHOST_SystemCocoa.mm +++ b/intern/ghost/intern/GHOST_SystemCocoa.mm @@ -116,8 +116,6 @@ static GHOST_TKey convertKey(int rawCode, unichar recvChar, UInt16 keyAction) case kVK_ANSI_Z: return GHOST_kKeyZ; #endif /* Numbers keys: mapped to handle some int'l keyboard (e.g. French). */ - case kVK_ISO_Section: - return GHOST_kKeyUnknown; case kVK_ANSI_1: return GHOST_kKey1; case kVK_ANSI_2: @@ -257,6 +255,7 @@ static GHOST_TKey convertKey(int rawCode, unichar recvChar, UInt16 keyAction) case kVK_ANSI_LeftBracket: return GHOST_kKeyLeftBracket; case kVK_ANSI_RightBracket: return GHOST_kKeyRightBracket; case kVK_ANSI_Grave: return GHOST_kKeyAccentGrave; + case kVK_ISO_Section: return GHOST_kKeyUnknown; #endif case kVK_VolumeUp: case kVK_VolumeDown: @@ -1246,7 +1245,7 @@ GHOST_TSuccess GHOST_SystemCocoa::handleDraggingEvent(GHOST_TEventType eventType /* Convert the image in a RGBA 32bit format */ /* As Core Graphics does not support contexts with non premutliplied alpha, - we need to get alpha key values in a separate batch */ + * we need to get alpha key values in a separate batch */ /* First get RGB values w/o Alpha to avoid pre-multiplication, * 32bit but last byte is unused */ @@ -1480,8 +1479,8 @@ GHOST_TSuccess GHOST_SystemCocoa::handleMouseEvent(void *eventPtr) CocoaWindow *cocoawindow; /* [event window] returns other windows if mouse-over, that's OSX input standard - however, if mouse exits window(s), the windows become inactive, until you click. - We then fall back to the active window from ghost */ + * however, if mouse exits window(s), the windows become inactive, until you click. + * We then fall back to the active window from ghost. */ window = (GHOST_WindowCocoa *)m_windowManager->getWindowAssociatedWithOSWindow( (void *)[event window]); if (!window) { diff --git a/intern/ghost/intern/GHOST_SystemPathsCocoa.mm b/intern/ghost/intern/GHOST_SystemPathsCocoa.mm index 3b29d5106f6..4032c145ab4 100644 --- a/intern/ghost/intern/GHOST_SystemPathsCocoa.mm +++ b/intern/ghost/intern/GHOST_SystemPathsCocoa.mm @@ -36,127 +36,101 @@ GHOST_SystemPathsCocoa::~GHOST_SystemPathsCocoa() #pragma mark Base directories retrieval -const char *GHOST_SystemPathsCocoa::getSystemDir(int, const char *versionstr) const +static const char *GetApplicationSupportDir(const char *versionstr, + const NSSearchPathDomainMask mask, + char *tempPath, + const std::size_t len_tempPath) { - static char tempPath[512] = ""; - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSString *basePath; - NSArray *paths; - - paths = NSSearchPathForDirectoriesInDomains( - NSApplicationSupportDirectory, NSLocalDomainMask, YES); - - if ([paths count] > 0) - basePath = [paths objectAtIndex:0]; - else { - [pool drain]; - return NULL; - } + @autoreleasepool { + const NSArray *const paths = NSSearchPathForDirectoriesInDomains( + NSApplicationSupportDirectory, mask, YES); - snprintf(tempPath, - sizeof(tempPath), - "%s/Blender/%s", - [basePath cStringUsingEncoding:NSASCIIStringEncoding], - versionstr); - - [pool drain]; + if ([paths count] == 0) { + return NULL; + } + const NSString *const basePath = [paths objectAtIndex:0]; + + snprintf(tempPath, + len_tempPath, + "%s/Blender/%s", + [basePath cStringUsingEncoding:NSASCIIStringEncoding], + versionstr); + } return tempPath; } -const char *GHOST_SystemPathsCocoa::getUserDir(int, const char *versionstr) const +const char *GHOST_SystemPathsCocoa::getSystemDir(int, const char *versionstr) const { static char tempPath[512] = ""; - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSString *basePath; - NSArray *paths; - - paths = NSSearchPathForDirectoriesInDomains( - NSApplicationSupportDirectory, NSUserDomainMask, YES); - - if ([paths count] > 0) - basePath = [paths objectAtIndex:0]; - else { - [pool drain]; - return NULL; - } - - snprintf(tempPath, - sizeof(tempPath), - "%s/Blender/%s", - [basePath cStringUsingEncoding:NSASCIIStringEncoding], - versionstr); + return GetApplicationSupportDir(versionstr, NSLocalDomainMask, tempPath, sizeof(tempPath)); +} - [pool drain]; - return tempPath; +const char *GHOST_SystemPathsCocoa::getUserDir(int, const char *versionstr) const +{ + static char tempPath[512] = ""; + return GetApplicationSupportDir(versionstr, NSUserDomainMask, tempPath, sizeof(tempPath)); } const char *GHOST_SystemPathsCocoa::getUserSpecialDir(GHOST_TUserSpecialDirTypes type) const { static char tempPath[512] = ""; - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSString *basePath; - NSArray *paths; - NSSearchPathDirectory ns_directory; - - switch (type) { - case GHOST_kUserSpecialDirDesktop: - ns_directory = NSDesktopDirectory; - break; - case GHOST_kUserSpecialDirDocuments: - ns_directory = NSDocumentDirectory; - break; - case GHOST_kUserSpecialDirDownloads: - ns_directory = NSDownloadsDirectory; - break; - case GHOST_kUserSpecialDirMusic: - ns_directory = NSMusicDirectory; - break; - case GHOST_kUserSpecialDirPictures: - ns_directory = NSPicturesDirectory; - break; - case GHOST_kUserSpecialDirVideos: - ns_directory = NSMoviesDirectory; - break; - default: - GHOST_ASSERT( - false, - "GHOST_SystemPathsCocoa::getUserSpecialDir(): Invalid enum value for type parameter"); - [pool drain]; + @autoreleasepool { + NSSearchPathDirectory ns_directory; + + switch (type) { + case GHOST_kUserSpecialDirDesktop: + ns_directory = NSDesktopDirectory; + break; + case GHOST_kUserSpecialDirDocuments: + ns_directory = NSDocumentDirectory; + break; + case GHOST_kUserSpecialDirDownloads: + ns_directory = NSDownloadsDirectory; + break; + case GHOST_kUserSpecialDirMusic: + ns_directory = NSMusicDirectory; + break; + case GHOST_kUserSpecialDirPictures: + ns_directory = NSPicturesDirectory; + break; + case GHOST_kUserSpecialDirVideos: + ns_directory = NSMoviesDirectory; + break; + case GHOST_kUserSpecialDirCaches: + ns_directory = NSCachesDirectory; + break; + default: + GHOST_ASSERT( + false, + "GHOST_SystemPathsCocoa::getUserSpecialDir(): Invalid enum value for type parameter"); + return NULL; + } + + const NSArray *const paths = NSSearchPathForDirectoriesInDomains( + ns_directory, NSUserDomainMask, YES); + if ([paths count] == 0) { return NULL; - } - - paths = NSSearchPathForDirectoriesInDomains(ns_directory, NSUserDomainMask, YES); + } + const NSString *const basePath = [paths objectAtIndex:0]; - if ([paths count] > 0) - basePath = [paths objectAtIndex:0]; - else { - [pool drain]; - return NULL; + strncpy(tempPath, [basePath cStringUsingEncoding:NSASCIIStringEncoding], sizeof(tempPath)); } - - strncpy( - (char *)tempPath, [basePath cStringUsingEncoding:NSASCIIStringEncoding], sizeof(tempPath)); - - [pool drain]; return tempPath; } const char *GHOST_SystemPathsCocoa::getBinaryDir() const { static char tempPath[512] = ""; - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSString *basePath; - - basePath = [[NSBundle mainBundle] bundlePath]; - if (basePath == nil) { - [pool drain]; - return NULL; - } + @autoreleasepool { + const NSString *const basePath = [[NSBundle mainBundle] bundlePath]; - strcpy((char *)tempPath, [basePath cStringUsingEncoding:NSASCIIStringEncoding]); + if (basePath == nil) { + return NULL; + } - [pool drain]; + strcpy(tempPath, [basePath cStringUsingEncoding:NSASCIIStringEncoding]); + } return tempPath; } diff --git a/intern/ghost/intern/GHOST_SystemPathsUnix.cpp b/intern/ghost/intern/GHOST_SystemPathsUnix.cpp index b58799e9c2a..72396782752 100644 --- a/intern/ghost/intern/GHOST_SystemPathsUnix.cpp +++ b/intern/ghost/intern/GHOST_SystemPathsUnix.cpp @@ -114,6 +114,7 @@ const char *GHOST_SystemPathsUnix::getUserDir(int version, const char *versionst const char *GHOST_SystemPathsUnix::getUserSpecialDir(GHOST_TUserSpecialDirTypes type) const { const char *type_str; + std::string add_path = ""; switch (type) { case GHOST_kUserSpecialDirDesktop: @@ -134,6 +135,18 @@ const char *GHOST_SystemPathsUnix::getUserSpecialDir(GHOST_TUserSpecialDirTypes case GHOST_kUserSpecialDirVideos: type_str = "VIDEOS"; break; + case GHOST_kUserSpecialDirCaches: { + const char *cache_dir = getenv("XDG_CACHE_HOME"); + if (cache_dir) { + return cache_dir; + } + /* Fallback to ~home/.cache/. + * When invoking `xdg-user-dir` without parameters the user folder + * will be read. `.cache` will be appended. */ + type_str = ""; + add_path = ".cache"; + break; + } default: GHOST_ASSERT( false, @@ -142,7 +155,7 @@ const char *GHOST_SystemPathsUnix::getUserSpecialDir(GHOST_TUserSpecialDirTypes } static string path = ""; - /* Pipe stderr to /dev/null to avoid error prints. We will fail gracefully still. */ + /* Pipe `stderr` to `/dev/null` to avoid error prints. We will fail gracefully still. */ string command = string("xdg-user-dir ") + type_str + " 2> /dev/null"; FILE *fstream = popen(command.c_str(), "r"); @@ -152,7 +165,7 @@ const char *GHOST_SystemPathsUnix::getUserSpecialDir(GHOST_TUserSpecialDirTypes std::stringstream path_stream; while (!feof(fstream)) { char c = fgetc(fstream); - /* xdg-user-dir ends the path with '\n'. */ + /* `xdg-user-dir` ends the path with '\n'. */ if (c == '\n') { break; } @@ -163,6 +176,10 @@ const char *GHOST_SystemPathsUnix::getUserSpecialDir(GHOST_TUserSpecialDirTypes return NULL; } + if (!add_path.empty()) { + path_stream << '/' << add_path; + } + path = path_stream.str(); return path[0] ? path.c_str() : NULL; } diff --git a/intern/ghost/intern/GHOST_SystemPathsWin32.cpp b/intern/ghost/intern/GHOST_SystemPathsWin32.cpp index 580cfcac7ba..bced552921f 100644 --- a/intern/ghost/intern/GHOST_SystemPathsWin32.cpp +++ b/intern/ghost/intern/GHOST_SystemPathsWin32.cpp @@ -100,6 +100,9 @@ const char *GHOST_SystemPathsWin32::getUserSpecialDir(GHOST_TUserSpecialDirTypes case GHOST_kUserSpecialDirVideos: folderid = FOLDERID_Videos; break; + case GHOST_kUserSpecialDirCaches: + folderid = FOLDERID_LocalAppData; + break; default: GHOST_ASSERT( false, diff --git a/intern/ghost/intern/GHOST_SystemWin32.cpp b/intern/ghost/intern/GHOST_SystemWin32.cpp index 482f20f5cd1..5251dd01b29 100644 --- a/intern/ghost/intern/GHOST_SystemWin32.cpp +++ b/intern/ghost/intern/GHOST_SystemWin32.cpp @@ -1002,10 +1002,10 @@ void GHOST_SystemWin32::processWintabEvent(GHOST_WindowWin32 *window) DWORD pos = GetMessagePos(); int x = GET_X_LPARAM(pos); int y = GET_Y_LPARAM(pos); + GHOST_TabletData td = wt->getLastTabletData(); - /* TODO supply tablet data */ system->pushEvent(new GHOST_EventCursor( - system->getMilliSeconds(), GHOST_kEventCursorMove, window, x, y, GHOST_TABLET_DATA_NONE)); + system->getMilliSeconds(), GHOST_kEventCursorMove, window, x, y, td)); } } @@ -1472,6 +1472,7 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, case WM_IME_SETCONTEXT: { GHOST_ImeWin32 *ime = window->getImeInput(); ime->UpdateInputLanguage(); + ime->UpdateConversionStatus(hwnd); ime->CreateImeWindow(hwnd); ime->CleanupComposition(hwnd); ime->CheckFirst(hwnd); @@ -1551,8 +1552,8 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, * button is press for menu. To prevent this we must return preventing DefWindowProc. * * Note that the four low-order bits of the wParam parameter are used internally by the - * OS. To obtain the correct result when testing the value of wParam, an application - * must combine the value 0xFFF0 with the wParam value by using the bitwise AND operator. + * OS. To obtain the correct result when testing the value of wParam, an application must + * combine the value 0xFFF0 with the wParam value by using the bit-wise AND operator. */ switch (wParam & 0xFFF0) { case SC_KEYMENU: diff --git a/intern/ghost/intern/GHOST_SystemX11.cpp b/intern/ghost/intern/GHOST_SystemX11.cpp index c87b745cf40..85504bd94fb 100644 --- a/intern/ghost/intern/GHOST_SystemX11.cpp +++ b/intern/ghost/intern/GHOST_SystemX11.cpp @@ -714,7 +714,7 @@ bool GHOST_SystemX11::processEvents(bool waitForEvent) anyProcessed = true; #ifdef USE_UNITY_WORKAROUND - /* note: processEvent() can't include this code because + /* NOTE: processEvent() can't include this code because * KeymapNotify event have no valid window information. */ /* the X server generates KeymapNotify event immediately after @@ -1044,7 +1044,7 @@ void GHOST_SystemX11::processEvent(XEvent *xe) #ifdef USE_NON_LATIN_KB_WORKAROUND /* XXX: Code below is kinda awfully convoluted... Issues are: - * - In keyboards like latin ones, numbers need a 'Shift' to be accessed but key_sym + * - In keyboards like Latin ones, numbers need a 'Shift' to be accessed but key_sym * is unmodified (or anyone swapping the keys with `xmodmap`). * - #XLookupKeysym seems to always use first defined key-map (see T47228), which generates * key-codes unusable by ghost_key_from_keysym for non-Latin-compatible key-maps. @@ -1131,7 +1131,7 @@ void GHOST_SystemX11::processEvent(XEvent *xe) } } #else - /* In keyboards like latin ones, + /* In keyboards like Latin ones, * numbers needs a 'Shift' to be accessed but key_sym * is unmodified (or anyone swapping the keys with xmodmap). * @@ -1514,7 +1514,7 @@ void GHOST_SystemX11::processEvent(XEvent *xe) * around tablet surface */ window->GetTabletData().Active = xtablet.mode; - /* Note: This event might be generated with incomplete data-set + /* NOTE: This event might be generated with incomplete data-set * (don't exactly know why, looks like in some cases, if the value does not change, * it is not included in subsequent #XDeviceMotionEvent events). * So we have to check which values this event actually contains! @@ -1528,13 +1528,13 @@ void GHOST_SystemX11::processEvent(XEvent *xe) window->GetTabletData().Pressure = axis_value / ((float)xtablet.PressureLevels); } - /* the (short) cast and the & 0xffff is bizarre and unexplained anywhere, - * but I got garbage data without it. Found it in the xidump.c source --matt + /* NOTE(@broken): the (short) cast and the & 0xffff is bizarre and unexplained anywhere, + * but I got garbage data without it. Found it in the `xidump.c` source. * - * The '& 0xffff' just truncates the value to its two lowest bytes, this probably means - * some drivers do not properly set the whole int value? Since we convert to float - * afterward, I don't think we need to cast to short here, but do not have a device to - * check this. --mont29 + * NOTE(@mont29): The '& 0xffff' just truncates the value to its two lowest bytes, + * this probably means some drivers do not properly set the whole int value? + * Since we convert to float afterward, + * I don't think we need to cast to short here, but do not have a device to check this. */ if (AXIS_VALUE_GET(3, axis_value)) { window->GetTabletData().Xtilt = (short)(axis_value & 0xffff) / @@ -2278,6 +2278,7 @@ void GHOST_SystemX11::putClipboard(const char *buffer, bool selection) const /* -------------------------------------------------------------------- */ /** \name Message Box * \{ */ + class DialogData { public: /* Width of the dialog. */ diff --git a/intern/ghost/intern/GHOST_Window.h b/intern/ghost/intern/GHOST_Window.h index f061e07b3c8..a3eb116780f 100644 --- a/intern/ghost/intern/GHOST_Window.h +++ b/intern/ghost/intern/GHOST_Window.h @@ -41,11 +41,10 @@ class GHOST_Window : public GHOST_IWindow { * Constructor. * Creates a new window and opens it. * To check if the window was created properly, use the getValid() method. - * \param width: The width the window. - * \param heigh: The height the window. + * \param width: The width of the window. + * \param height: The height of the window. * \param state: The state the window is initially opened with. - * \param type: The type of drawing context installed in this window. - * \param stereoVisual: Stereo visual for quad buffered stereo. + * \param wantStereoVisual: Stereo visual for quad buffered stereo. * \param exclusive: Use to show the window ontop and ignore others (used full-screen). */ GHOST_Window(uint32_t width, diff --git a/intern/ghost/intern/GHOST_WindowViewCocoa.h b/intern/ghost/intern/GHOST_WindowViewCocoa.h index fa629528809..1bda59c3505 100644 --- a/intern/ghost/intern/GHOST_WindowViewCocoa.h +++ b/intern/ghost/intern/GHOST_WindowViewCocoa.h @@ -510,6 +510,14 @@ - (void)checkKeyCodeIsControlChar:(NSEvent *)event { ime.state_flag &= ~GHOST_IME_KEY_CONTROL_CHAR; + + /* Don't use IME for command and ctrl key combinations, these are shortcuts. */ + if ([event modifierFlags] & (NSEventModifierFlagCommand | NSEventModifierFlagControl)) { + ime.state_flag |= GHOST_IME_KEY_CONTROL_CHAR; + return; + } + + /* Don't use IME for these control keys. */ switch ([event keyCode]) { case kVK_ANSI_KeypadEnter: case kVK_ANSI_KeypadClear: diff --git a/intern/ghost/intern/GHOST_WindowX11.cpp b/intern/ghost/intern/GHOST_WindowX11.cpp index de389951613..c77ef162d9a 100644 --- a/intern/ghost/intern/GHOST_WindowX11.cpp +++ b/intern/ghost/intern/GHOST_WindowX11.cpp @@ -544,7 +544,7 @@ void GHOST_WindowX11::refreshXInputDevices() std::vector<XEventClass> xevents; for (GHOST_SystemX11::GHOST_TabletX11 &xtablet : m_system->GetXTablets()) { - /* With modern XInput (xlib 1.6.2 at least and/or evdev 2.9.0) and some 'no-name' tablets + /* With modern XInput (XLIB 1.6.2 at least and/or EVDEV 2.9.0) and some 'no-name' tablets * like 'UC-LOGIC Tablet WP5540U', we also need to 'select' ButtonPress for motion event, * otherwise we do not get any tablet motion event once pen is pressed... See T43367. */ @@ -1092,9 +1092,9 @@ GHOST_TSuccess GHOST_WindowX11::setOrder(GHOST_TWindowOrder order) XWindowAttributes attr; Atom atom; - /* We use both XRaiseWindow and _NET_ACTIVE_WINDOW, since some - * window managers ignore the former (e.g. kwin from kde) and others - * don't implement the latter (e.g. fluxbox pre 0.9.9) */ + /* We use both #XRaiseWindow and #_NET_ACTIVE_WINDOW, since some + * window managers ignore the former (e.g. KWIN from KDE) and others + * don't implement the latter (e.g. FLUXBOX before 0.9.9). */ XRaiseWindow(m_display, m_window); diff --git a/intern/ghost/intern/GHOST_Wintab.cpp b/intern/ghost/intern/GHOST_Wintab.cpp index cf0309b1521..953fcb171e5 100644 --- a/intern/ghost/intern/GHOST_Wintab.cpp +++ b/intern/ghost/intern/GHOST_Wintab.cpp @@ -130,8 +130,7 @@ GHOST_Wintab *GHOST_Wintab::loadWintab(HWND hwnd) } } - return new GHOST_Wintab(hwnd, - std::move(handle), + return new GHOST_Wintab(std::move(handle), info, get, set, @@ -174,8 +173,7 @@ void GHOST_Wintab::extractCoordinates(LOGCONTEXT &lc, Coord &tablet, Coord &syst system.y.ext = -lc.lcSysExtY; } -GHOST_Wintab::GHOST_Wintab(HWND hwnd, - unique_hmodule handle, +GHOST_Wintab::GHOST_Wintab(unique_hmodule handle, GHOST_WIN32_WTInfo info, GHOST_WIN32_WTGet get, GHOST_WIN32_WTSet set, @@ -298,14 +296,12 @@ GHOST_TabletData GHOST_Wintab::getLastTabletData() void GHOST_Wintab::getInput(std::vector<GHOST_WintabInfoWin32> &outWintabInfo) { const int numPackets = m_fpPacketsGet(m_context.get(), m_pkts.size(), m_pkts.data()); - outWintabInfo.resize(numPackets); - size_t outExtent = 0; + outWintabInfo.reserve(numPackets); for (int i = 0; i < numPackets; i++) { PACKET pkt = m_pkts[i]; - GHOST_WintabInfoWin32 &out = outWintabInfo[i + outExtent]; + GHOST_WintabInfoWin32 out; - out.tabletData = GHOST_TABLET_DATA_NONE; /* % 3 for multiple devices ("DualTrack"). */ switch (pkt.pkCursor % 3) { case 0: @@ -328,12 +324,7 @@ void GHOST_Wintab::getInput(std::vector<GHOST_WintabInfoWin32> &outWintabInfo) } if ((m_maxAzimuth > 0) && (m_maxAltitude > 0)) { - ORIENTATION ort = pkt.pkOrientation; - float vecLen; - float altRad, azmRad; /* In radians. */ - - /* - * From the wintab spec: + /* From the wintab spec: * orAzimuth: Specifies the clockwise rotation of the cursor about the z axis through a * full circular range. * orAltitude: Specifies the angle with the x-y plane through a signed, semicircular range. @@ -346,12 +337,14 @@ void GHOST_Wintab::getInput(std::vector<GHOST_WintabInfoWin32> &outWintabInfo) * value. */ + ORIENTATION ort = pkt.pkOrientation; + /* Convert raw fixed point data to radians. */ - altRad = (float)((fabs((float)ort.orAltitude) / (float)m_maxAltitude) * M_PI / 2.0); - azmRad = (float)(((float)ort.orAzimuth / (float)m_maxAzimuth) * M_PI * 2.0); + float altRad = (float)((fabs((float)ort.orAltitude) / (float)m_maxAltitude) * M_PI / 2.0); + float azmRad = (float)(((float)ort.orAzimuth / (float)m_maxAzimuth) * M_PI * 2.0); /* Find length of the stylus' projected vector on the XY plane. */ - vecLen = cos(altRad); + float vecLen = cos(altRad); /* From there calculate X and Y components based on azimuth. */ out.tabletData.Xtilt = sin(azmRad) * vecLen; @@ -362,13 +355,8 @@ void GHOST_Wintab::getInput(std::vector<GHOST_WintabInfoWin32> &outWintabInfo) /* Some Wintab libraries don't handle relative button input, so we track button presses * manually. */ - out.button = GHOST_kButtonMaskNone; - out.type = GHOST_kEventCursorMove; - DWORD buttonsChanged = m_buttons ^ pkt.pkButtons; WORD buttonIndex = 0; - GHOST_WintabInfoWin32 buttonRef = out; - int buttons = 0; while (buttonsChanged) { if (buttonsChanged & 1) { @@ -376,23 +364,14 @@ void GHOST_Wintab::getInput(std::vector<GHOST_WintabInfoWin32> &outWintabInfo) GHOST_TButtonMask button = mapWintabToGhostButton(pkt.pkCursor, buttonIndex); if (button != GHOST_kButtonMaskNone) { - /* Extend output if multiple buttons are pressed. We don't extend input until we confirm - * a Wintab buttons maps to a system button. */ - if (buttons > 0) { - outWintabInfo.resize(outWintabInfo.size() + 1); - outExtent++; - GHOST_WintabInfoWin32 &out = outWintabInfo[i + outExtent]; - out = buttonRef; + /* If this is not the first button found, push info for the prior Wintab button. */ + if (out.button != GHOST_kButtonMaskNone) { + outWintabInfo.push_back(out); } - buttons++; out.button = button; - if (buttonsChanged & pkt.pkButtons) { - out.type = GHOST_kEventButtonDown; - } - else { - out.type = GHOST_kEventButtonUp; - } + out.type = buttonsChanged & pkt.pkButtons ? GHOST_kEventButtonDown : + GHOST_kEventButtonUp; } m_buttons ^= 1 << buttonIndex; @@ -401,6 +380,8 @@ void GHOST_Wintab::getInput(std::vector<GHOST_WintabInfoWin32> &outWintabInfo) buttonsChanged >>= 1; buttonIndex++; } + + outWintabInfo.push_back(out); } if (!outWintabInfo.empty()) { diff --git a/intern/ghost/intern/GHOST_Wintab.h b/intern/ghost/intern/GHOST_Wintab.h index 443c726d270..1994f057db9 100644 --- a/intern/ghost/intern/GHOST_Wintab.h +++ b/intern/ghost/intern/GHOST_Wintab.h @@ -56,11 +56,12 @@ typedef std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&::FreeLibrary) typedef std::unique_ptr<std::remove_pointer_t<HCTX>, GHOST_WIN32_WTClose> unique_hctx; struct GHOST_WintabInfoWin32 { - int32_t x, y; - GHOST_TEventType type; - GHOST_TButtonMask button; - uint64_t time; - GHOST_TabletData tabletData; + int32_t x = 0; + int32_t y = 0; + GHOST_TEventType type = GHOST_kEventCursorMove; + GHOST_TButtonMask button = GHOST_kButtonMaskNone; + uint64_t time = 0; + GHOST_TabletData tabletData = GHOST_TABLET_DATA_NONE; }; class GHOST_Wintab { @@ -148,7 +149,7 @@ class GHOST_Wintab { * \param wtY: Wintab cursor y position. * \return True if Win32 and Wintab cursor positions match within tolerance. * - * Note: Only test coordinates on button press, not release. This prevents issues when async + * NOTE: Only test coordinates on button press, not release. This prevents issues when async * mismatch causes mouse movement to replay and snap back, which is only an issue while drawing. */ bool testCoordinates(int sysX, int sysY, int wtX, int wtY); @@ -213,8 +214,7 @@ class GHOST_Wintab { /** Most recently received tablet data, or none if pen is not in range. */ GHOST_TabletData m_lastTabletData = GHOST_TABLET_DATA_NONE; - GHOST_Wintab(HWND hwnd, - unique_hmodule handle, + GHOST_Wintab(unique_hmodule handle, GHOST_WIN32_WTInfo info, GHOST_WIN32_WTGet get, GHOST_WIN32_WTSet set, diff --git a/intern/ghost/intern/GHOST_XrAction.cpp b/intern/ghost/intern/GHOST_XrAction.cpp index 704b1ce9fac..f51f98c9b3d 100644 --- a/intern/ghost/intern/GHOST_XrAction.cpp +++ b/intern/ghost/intern/GHOST_XrAction.cpp @@ -216,8 +216,9 @@ GHOST_XrAction::GHOST_XrAction(XrInstance instance, XrActionCreateInfo action_info{XR_TYPE_ACTION_CREATE_INFO}; strcpy(action_info.actionName, info.name); - strcpy(action_info.localizedActionName, info.name); /* Just use same name for localized. This can - be changed in the future if necessary. */ + + /* Just use same name for localized. This can be changed in the future if necessary. */ + strcpy(action_info.localizedActionName, info.name); switch (info.type) { case GHOST_kXrActionTypeBooleanInput: diff --git a/intern/ghost/intern/GHOST_XrContext.cpp b/intern/ghost/intern/GHOST_XrContext.cpp index fe8fec052fe..15b40690d83 100644 --- a/intern/ghost/intern/GHOST_XrContext.cpp +++ b/intern/ghost/intern/GHOST_XrContext.cpp @@ -412,11 +412,14 @@ void GHOST_XrContext::getExtensionsToEnable( try_ext.push_back(XR_EXT_DEBUG_UTILS_EXTENSION_NAME); } - /* Try enabling interaction profile extensions. */ + /* Interaction profile extensions. */ try_ext.push_back(XR_EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME); try_ext.push_back(XR_HTC_VIVE_COSMOS_CONTROLLER_INTERACTION_EXTENSION_NAME); try_ext.push_back(XR_HUAWEI_CONTROLLER_INTERACTION_EXTENSION_NAME); + /* Controller model extension. */ + try_ext.push_back(XR_MSFT_CONTROLLER_MODEL_EXTENSION_NAME); + /* Varjo quad view extension. */ try_ext.push_back(XR_VARJO_QUAD_VIEWS_EXTENSION_NAME); diff --git a/intern/ghost/intern/GHOST_XrControllerModel.cpp b/intern/ghost/intern/GHOST_XrControllerModel.cpp new file mode 100644 index 00000000000..aa46aaaf89a --- /dev/null +++ b/intern/ghost/intern/GHOST_XrControllerModel.cpp @@ -0,0 +1,617 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup GHOST + */ + +#include <cassert> + +#include <Eigen/Core> +#include <Eigen/Geometry> + +#include "GHOST_Types.h" +#include "GHOST_XrException.h" +#include "GHOST_Xr_intern.h" + +#include "GHOST_XrControllerModel.h" + +#define TINYGLTF_IMPLEMENTATION +#define TINYGLTF_NO_STB_IMAGE +#define TINYGLTF_NO_STB_IMAGE_WRITE +#define STBIWDEF static inline +#include "tiny_gltf.h" + +struct GHOST_XrControllerModelNode { + int32_t parent_idx = -1; + int32_t component_idx = -1; + float local_transform[4][4]; +}; + +/* -------------------------------------------------------------------- */ +/** \name glTF Utilities + * + * Adapted from Microsoft OpenXR-Mixed Reality Samples (MIT License): + * https://github.com/microsoft/OpenXR-MixedReality + * \{ */ + +struct GHOST_XrPrimitive { + std::vector<GHOST_XrControllerModelVertex> vertices; + std::vector<uint32_t> indices; +}; + +/** + * Validate that an accessor does not go out of bounds of the buffer view that it references and + * that the buffer view does not exceed the bounds of the buffer that it references + */ +static void validate_accessor(const tinygltf::Accessor &accessor, + const tinygltf::BufferView &buffer_view, + const tinygltf::Buffer &buffer, + size_t byte_stride, + size_t element_size) +{ + /* Make sure the accessor does not go out of range of the buffer view. */ + if (accessor.byteOffset + (accessor.count - 1) * byte_stride + element_size > + buffer_view.byteLength) { + throw GHOST_XrException("glTF: Accessor goes out of range of bufferview."); + } + + /* Make sure the buffer view does not go out of range of the buffer. */ + if (buffer_view.byteOffset + buffer_view.byteLength > buffer.data.size()) { + throw GHOST_XrException("glTF: BufferView goes out of range of buffer."); + } +} + +template<float (GHOST_XrControllerModelVertex::*field)[3]> +static void read_vertices(const tinygltf::Accessor &accessor, + const tinygltf::BufferView &buffer_view, + const tinygltf::Buffer &buffer, + GHOST_XrPrimitive &primitive) +{ + if (accessor.type != TINYGLTF_TYPE_VEC3) { + throw GHOST_XrException( + "glTF: Accessor for primitive attribute has incorrect type (VEC3 expected)."); + } + + if (accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) { + throw GHOST_XrException( + "glTF: Accessor for primitive attribute has incorrect component type (FLOAT expected)."); + } + + /* If stride is not specified, it is tightly packed. */ + constexpr size_t packed_size = sizeof(float) * 3; + const size_t stride = buffer_view.byteStride == 0 ? packed_size : buffer_view.byteStride; + validate_accessor(accessor, buffer_view, buffer, stride, packed_size); + + /* Resize the vertices vector, if necessary, to include room for the attribute data. + * If there are multiple attributes for a primitive, the first one will resize, and the + * subsequent will not need to. */ + primitive.vertices.resize(accessor.count); + + /* Copy the attribute value over from the glTF buffer into the appropriate vertex field. */ + const uint8_t *buffer_ptr = buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + for (size_t i = 0; i < accessor.count; i++, buffer_ptr += stride) { + memcpy(primitive.vertices[i].*field, buffer_ptr, stride); + } +} + +static void load_attribute_accessor(const tinygltf::Model &gltf_model, + const std::string &attribute_name, + int accessor_id, + GHOST_XrPrimitive &primitive) +{ + const auto &accessor = gltf_model.accessors.at(accessor_id); + + if (accessor.bufferView == -1) { + throw GHOST_XrException("glTF: Accessor for primitive attribute specifies no bufferview."); + } + + const tinygltf::BufferView &buffer_view = gltf_model.bufferViews.at(accessor.bufferView); + if (buffer_view.target != TINYGLTF_TARGET_ARRAY_BUFFER && buffer_view.target != 0) { + throw GHOST_XrException( + "glTF: Accessor for primitive attribute uses bufferview with invalid 'target' type."); + } + + const tinygltf::Buffer &buffer = gltf_model.buffers.at(buffer_view.buffer); + + if (attribute_name.compare("POSITION") == 0) { + read_vertices<&GHOST_XrControllerModelVertex::position>( + accessor, buffer_view, buffer, primitive); + } + else if (attribute_name.compare("NORMAL") == 0) { + read_vertices<&GHOST_XrControllerModelVertex::normal>( + accessor, buffer_view, buffer, primitive); + } +} + +/** + * Reads index data from a glTF primitive into a GHOST_XrPrimitive. glTF indices may be 8bit, 16bit + * or 32bit integers. This will coalesce indices from the source type(s) into a 32bit integer. + */ +template<typename TSrcIndex> +static void read_indices(const tinygltf::Accessor &accessor, + const tinygltf::BufferView &buffer_view, + const tinygltf::Buffer &buffer, + GHOST_XrPrimitive &primitive) +{ + + /* Allow 0 (not specified) even though spec doesn't seem to allow this (BoomBox GLB fails). */ + if (buffer_view.target != TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER && buffer_view.target != 0) { + throw GHOST_XrException( + "glTF: Accessor for indices uses bufferview with invalid 'target' type."); + } + + constexpr size_t component_size_bytes = sizeof(TSrcIndex); + if (buffer_view.byteStride != 0 && + buffer_view.byteStride != + component_size_bytes) { /* Index buffer must be packed per glTF spec. */ + throw GHOST_XrException( + "glTF: Accessor for indices uses bufferview with invalid 'byteStride'."); + } + + validate_accessor(accessor, buffer_view, buffer, component_size_bytes, component_size_bytes); + + /* Since only triangles are supported, enforce that the number of indices is divisible by 3. */ + if ((accessor.count % 3) != 0) { + throw GHOST_XrException("glTF: Unexpected number of indices for triangle primitive"); + } + + const TSrcIndex *index_buffer = reinterpret_cast<const TSrcIndex *>( + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset); + for (uint32_t i = 0; i < accessor.count; i++) { + primitive.indices.push_back(*(index_buffer + i)); + } +} + +/** + * Reads index data from a glTF primitive into a GHOST_XrPrimitive. + */ +static void load_index_accessor(const tinygltf::Model &gltf_model, + const tinygltf::Accessor &accessor, + GHOST_XrPrimitive &primitive) +{ + if (accessor.type != TINYGLTF_TYPE_SCALAR) { + throw GHOST_XrException("glTF: Accessor for indices specifies invalid 'type'."); + } + + if (accessor.bufferView == -1) { + throw GHOST_XrException("glTF: Index accessor without bufferView is currently not supported."); + } + + const tinygltf::BufferView &buffer_view = gltf_model.bufferViews.at(accessor.bufferView); + const tinygltf::Buffer &buffer = gltf_model.buffers.at(buffer_view.buffer); + + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { + read_indices<uint8_t>(accessor, buffer_view, buffer, primitive); + } + else if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + read_indices<uint16_t>(accessor, buffer_view, buffer, primitive); + } + else if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { + read_indices<uint32_t>(accessor, buffer_view, buffer, primitive); + } + else { + throw GHOST_XrException("glTF: Accessor for indices specifies invalid 'componentType'."); + } +} + +static GHOST_XrPrimitive read_primitive(const tinygltf::Model &gltf_model, + const tinygltf::Primitive &gltf_primitive) +{ + if (gltf_primitive.mode != TINYGLTF_MODE_TRIANGLES) { + throw GHOST_XrException( + "glTF: Unsupported primitive mode. Only TINYGLTF_MODE_TRIANGLES is supported."); + } + + GHOST_XrPrimitive primitive; + + /* glTF vertex data is stored in an attribute dictionary.Loop through each attribute and insert + * it into the GHOST_XrPrimitive. */ + for (const auto &[attr_name, accessor_idx] : gltf_primitive.attributes) { + load_attribute_accessor(gltf_model, attr_name, accessor_idx, primitive); + } + + if (gltf_primitive.indices != -1) { + /* If indices are specified for the glTF primitive, read them into the GHOST_XrPrimitive. */ + load_index_accessor(gltf_model, gltf_model.accessors.at(gltf_primitive.indices), primitive); + } + + return primitive; +} + +/** + * Calculate node local and world transforms. + */ +static void calc_node_transforms(const tinygltf::Node &gltf_node, + const float parent_transform[4][4], + float r_local_transform[4][4], + float r_world_transform[4][4]) +{ + /* A node may specify either a 4x4 matrix or TRS (Translation - Rotation - Scale) values, but not + * both. */ + if (gltf_node.matrix.size() == 16) { + const std::vector<double> &dm = gltf_node.matrix; + float m[4][4] = {{(float)dm[0], (float)dm[1], (float)dm[2], (float)dm[3]}, + {(float)dm[4], (float)dm[5], (float)dm[6], (float)dm[7]}, + {(float)dm[8], (float)dm[9], (float)dm[10], (float)dm[11]}, + {(float)dm[12], (float)dm[13], (float)dm[14], (float)dm[15]}}; + memcpy(r_local_transform, m, sizeof(float) * 16); + } + else { + /* No matrix is present, so construct a matrix from the TRS values (each one is optional). */ + std::vector<double> translation = gltf_node.translation; + std::vector<double> rotation = gltf_node.rotation; + std::vector<double> scale = gltf_node.scale; + Eigen::Matrix4f &m = *(Eigen::Matrix4f *)r_local_transform; + Eigen::Quaternionf q; + Eigen::Matrix3f scalemat; + + if (translation.size() != 3) { + translation.resize(3); + translation[0] = translation[1] = translation[2] = 0.0; + } + if (rotation.size() != 4) { + rotation.resize(4); + rotation[0] = rotation[1] = rotation[2] = 0.0; + rotation[3] = 1.0; + } + if (scale.size() != 3) { + scale.resize(3); + scale[0] = scale[1] = scale[2] = 1.0; + } + + q.w() = (float)rotation[3]; + q.x() = (float)rotation[0]; + q.y() = (float)rotation[1]; + q.z() = (float)rotation[2]; + q.normalize(); + + scalemat.setIdentity(); + scalemat(0, 0) = (float)scale[0]; + scalemat(1, 1) = (float)scale[1]; + scalemat(2, 2) = (float)scale[2]; + + m.setIdentity(); + m.block<3, 3>(0, 0) = q.toRotationMatrix() * scalemat; + m.block<3, 1>(0, 3) = Eigen::Vector3f( + (float)translation[0], (float)translation[1], (float)translation[2]); + } + + *(Eigen::Matrix4f *)r_world_transform = *(Eigen::Matrix4f *)parent_transform * + *(Eigen::Matrix4f *)r_local_transform; +} + +static void load_node(const tinygltf::Model &gltf_model, + int gltf_node_id, + int32_t parent_idx, + const float parent_transform[4][4], + const std::string &parent_name, + const std::vector<XrControllerModelNodePropertiesMSFT> &node_properties, + std::vector<GHOST_XrControllerModelVertex> &vertices, + std::vector<uint32_t> &indices, + std::vector<GHOST_XrControllerModelComponent> &components, + std::vector<GHOST_XrControllerModelNode> &nodes, + std::vector<int32_t> &node_state_indices) +{ + const tinygltf::Node &gltf_node = gltf_model.nodes.at(gltf_node_id); + float world_transform[4][4]; + + GHOST_XrControllerModelNode &node = nodes.emplace_back(); + const int32_t node_idx = (int32_t)(nodes.size() - 1); + node.parent_idx = parent_idx; + calc_node_transforms(gltf_node, parent_transform, node.local_transform, world_transform); + + for (size_t i = 0; i < node_properties.size(); ++i) { + if ((node_state_indices[i] < 0) && (parent_name == node_properties[i].parentNodeName) && + (gltf_node.name == node_properties[i].nodeName)) { + node_state_indices[i] = node_idx; + break; + } + } + + if (gltf_node.mesh != -1) { + const tinygltf::Mesh &gltf_mesh = gltf_model.meshes.at(gltf_node.mesh); + + GHOST_XrControllerModelComponent &component = components.emplace_back(); + node.component_idx = components.size() - 1; + memcpy(component.transform, world_transform, sizeof(component.transform)); + component.vertex_offset = vertices.size(); + component.index_offset = indices.size(); + + for (const tinygltf::Primitive &gltf_primitive : gltf_mesh.primitives) { + /* Read the primitive data from the glTF buffers. */ + const GHOST_XrPrimitive primitive = read_primitive(gltf_model, gltf_primitive); + + const size_t start_vertex = vertices.size(); + size_t offset = start_vertex; + size_t count = primitive.vertices.size(); + vertices.resize(offset + count); + memcpy(vertices.data() + offset, + primitive.vertices.data(), + count * sizeof(decltype(primitive.vertices)::value_type)); + + offset = indices.size(); + count = primitive.indices.size(); + indices.resize(offset + count); + for (size_t i = 0; i < count; i += 3) { + indices[offset + i + 0] = start_vertex + primitive.indices[i + 0]; + indices[offset + i + 1] = start_vertex + primitive.indices[i + 2]; + indices[offset + i + 2] = start_vertex + primitive.indices[i + 1]; + } + } + + component.vertex_count = vertices.size() - component.vertex_offset; + component.index_count = indices.size() - component.index_offset; + } + + /* Recursively load all children. */ + for (const int child_node_id : gltf_node.children) { + load_node(gltf_model, + child_node_id, + node_idx, + world_transform, + gltf_node.name, + node_properties, + vertices, + indices, + components, + nodes, + node_state_indices); + } +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name OpenXR Extension Functions + * + * \{ */ + +static PFN_xrGetControllerModelKeyMSFT g_xrGetControllerModelKeyMSFT = nullptr; +static PFN_xrLoadControllerModelMSFT g_xrLoadControllerModelMSFT = nullptr; +static PFN_xrGetControllerModelPropertiesMSFT g_xrGetControllerModelPropertiesMSFT = nullptr; +static PFN_xrGetControllerModelStateMSFT g_xrGetControllerModelStateMSFT = nullptr; +static XrInstance g_instance = XR_NULL_HANDLE; + +#define INIT_EXTENSION_FUNCTION(name) \ + CHECK_XR( \ + xrGetInstanceProcAddr(instance, #name, reinterpret_cast<PFN_xrVoidFunction *>(&g_##name)), \ + "Failed to get pointer to extension function: " #name); + +static void init_controller_model_extension_functions(XrInstance instance) +{ + if (instance != g_instance) { + g_instance = instance; + g_xrGetControllerModelKeyMSFT = nullptr; + g_xrLoadControllerModelMSFT = nullptr; + g_xrGetControllerModelPropertiesMSFT = nullptr; + g_xrGetControllerModelStateMSFT = nullptr; + } + + if (g_xrGetControllerModelKeyMSFT == nullptr) { + INIT_EXTENSION_FUNCTION(xrGetControllerModelKeyMSFT); + } + if (g_xrLoadControllerModelMSFT == nullptr) { + INIT_EXTENSION_FUNCTION(xrLoadControllerModelMSFT); + } + if (g_xrGetControllerModelPropertiesMSFT == nullptr) { + INIT_EXTENSION_FUNCTION(xrGetControllerModelPropertiesMSFT); + } + if (g_xrGetControllerModelStateMSFT == nullptr) { + INIT_EXTENSION_FUNCTION(xrGetControllerModelStateMSFT); + } +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name GHOST_XrControllerModel + * + * \{ */ + +GHOST_XrControllerModel::GHOST_XrControllerModel(XrInstance instance, + const char *subaction_path_str) +{ + init_controller_model_extension_functions(instance); + + CHECK_XR(xrStringToPath(instance, subaction_path_str, &m_subaction_path), + (std::string("Failed to get user path \"") + subaction_path_str + "\".").data()); +} + +GHOST_XrControllerModel::~GHOST_XrControllerModel() +{ + if (m_load_task.valid()) { + m_load_task.wait(); + } +} + +void GHOST_XrControllerModel::load(XrSession session) +{ + if (m_data_loaded || m_load_task.valid()) { + return; + } + + /* Get model key. */ + XrControllerModelKeyStateMSFT key_state{XR_TYPE_CONTROLLER_MODEL_KEY_STATE_MSFT}; + CHECK_XR(g_xrGetControllerModelKeyMSFT(session, m_subaction_path, &key_state), + "Failed to get controller model key state."); + + if (key_state.modelKey != XR_NULL_CONTROLLER_MODEL_KEY_MSFT) { + m_model_key = key_state.modelKey; + /* Load asynchronously. */ + m_load_task = std::async(std::launch::async, + [&, session = session]() { return loadControllerModel(session); }); + } +} + +void GHOST_XrControllerModel::loadControllerModel(XrSession session) +{ + /* Load binary buffers. */ + uint32_t buf_size = 0; + CHECK_XR(g_xrLoadControllerModelMSFT(session, m_model_key, 0, &buf_size, nullptr), + "Failed to get controller model buffer size."); + + std::vector<uint8_t> buf((size_t)buf_size); + CHECK_XR(g_xrLoadControllerModelMSFT(session, m_model_key, buf_size, &buf_size, buf.data()), + "Failed to load controller model binary buffers."); + + /* Convert to glTF model. */ + tinygltf::TinyGLTF gltf_loader; + tinygltf::Model gltf_model; + std::string err_msg; + { + /* Workaround for TINYGLTF_NO_STB_IMAGE define. Set custom image loader to prevent failure when + * parsing image data. */ + auto load_img_func = [](tinygltf::Image *img, + const int p0, + std::string *p1, + std::string *p2, + int p3, + int p4, + const unsigned char *p5, + int p6, + void *user_pointer) -> bool { + (void)img; + (void)p0; + (void)p1; + (void)p2; + (void)p3; + (void)p4; + (void)p5; + (void)p6; + (void)user_pointer; + return true; + }; + gltf_loader.SetImageLoader(load_img_func, nullptr); + } + + if (!gltf_loader.LoadBinaryFromMemory(&gltf_model, &err_msg, nullptr, buf.data(), buf_size)) { + throw GHOST_XrException(("Failed to load glTF controller model: " + err_msg).c_str()); + } + + /* Get node properties. */ + XrControllerModelPropertiesMSFT model_properties{XR_TYPE_CONTROLLER_MODEL_PROPERTIES_MSFT}; + model_properties.nodeCapacityInput = 0; + CHECK_XR(g_xrGetControllerModelPropertiesMSFT(session, m_model_key, &model_properties), + "Failed to get controller model node properties count."); + + std::vector<XrControllerModelNodePropertiesMSFT> node_properties( + model_properties.nodeCountOutput, {XR_TYPE_CONTROLLER_MODEL_NODE_PROPERTIES_MSFT}); + model_properties.nodeCapacityInput = (uint32_t)node_properties.size(); + model_properties.nodeProperties = node_properties.data(); + CHECK_XR(g_xrGetControllerModelPropertiesMSFT(session, m_model_key, &model_properties), + "Failed to get controller model node properties."); + + m_node_state_indices.resize(node_properties.size(), -1); + + /* Get mesh vertex data. */ + const tinygltf::Scene &default_scene = gltf_model.scenes.at( + (gltf_model.defaultScene == -1) ? 0 : gltf_model.defaultScene); + const int32_t root_idx = -1; + const std::string root_name = ""; + float root_transform[4][4] = {{0}}; + root_transform[0][0] = root_transform[1][1] = root_transform[2][2] = root_transform[3][3] = 1.0f; + + for (const int node_id : default_scene.nodes) { + load_node(gltf_model, + node_id, + root_idx, + root_transform, + root_name, + node_properties, + m_vertices, + m_indices, + m_components, + m_nodes, + m_node_state_indices); + } + + m_data_loaded = true; +} + +void GHOST_XrControllerModel::updateComponents(XrSession session) +{ + if (!m_data_loaded) { + return; + } + + /* Get node states. */ + XrControllerModelStateMSFT model_state{XR_TYPE_CONTROLLER_MODEL_STATE_MSFT}; + model_state.nodeCapacityInput = 0; + CHECK_XR(g_xrGetControllerModelStateMSFT(session, m_model_key, &model_state), + "Failed to get controller model node state count."); + + const uint32_t count = model_state.nodeCountOutput; + std::vector<XrControllerModelNodeStateMSFT> node_states( + count, {XR_TYPE_CONTROLLER_MODEL_NODE_STATE_MSFT}); + model_state.nodeCapacityInput = count; + model_state.nodeStates = node_states.data(); + CHECK_XR(g_xrGetControllerModelStateMSFT(session, m_model_key, &model_state), + "Failed to get controller model node states."); + + /* Update node local transforms. */ + assert(m_node_state_indices.size() == count); + + for (uint32_t state_idx = 0; state_idx < count; ++state_idx) { + const int32_t &node_idx = m_node_state_indices[state_idx]; + if (node_idx >= 0) { + const XrPosef &pose = node_states[state_idx].nodePose; + Eigen::Matrix4f &m = *(Eigen::Matrix4f *)m_nodes[node_idx].local_transform; + Eigen::Quaternionf q( + pose.orientation.w, pose.orientation.x, pose.orientation.y, pose.orientation.z); + m.setIdentity(); + m.block<3, 3>(0, 0) = q.toRotationMatrix(); + m.block<3, 1>(0, 3) = Eigen::Vector3f(pose.position.x, pose.position.y, pose.position.z); + } + } + + /* Calculate component transforms (in world space). */ + std::vector<Eigen::Matrix4f> world_transforms(m_nodes.size()); + uint32_t i = 0; + for (const GHOST_XrControllerModelNode &node : m_nodes) { + world_transforms[i] = (node.parent_idx >= 0) ? world_transforms[node.parent_idx] * + *(Eigen::Matrix4f *)node.local_transform : + *(Eigen::Matrix4f *)node.local_transform; + if (node.component_idx >= 0) { + memcpy(m_components[node.component_idx].transform, + world_transforms[i].data(), + sizeof(m_components[node.component_idx].transform)); + } + ++i; + } +} + +void GHOST_XrControllerModel::getData(GHOST_XrControllerModelData &r_data) +{ + if (m_data_loaded) { + r_data.count_vertices = (uint32_t)m_vertices.size(); + r_data.vertices = m_vertices.data(); + r_data.count_indices = (uint32_t)m_indices.size(); + r_data.indices = m_indices.data(); + r_data.count_components = (uint32_t)m_components.size(); + r_data.components = m_components.data(); + } + else { + r_data.count_vertices = 0; + r_data.vertices = nullptr; + r_data.count_indices = 0; + r_data.indices = nullptr; + r_data.count_components = 0; + r_data.components = nullptr; + } +} + +/** \} */ diff --git a/intern/ghost/intern/GHOST_XrControllerModel.h b/intern/ghost/intern/GHOST_XrControllerModel.h new file mode 100644 index 00000000000..a9aed961713 --- /dev/null +++ b/intern/ghost/intern/GHOST_XrControllerModel.h @@ -0,0 +1,59 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup GHOST + */ + +/* NOTE: Requires OpenXR headers to be included before this one for OpenXR types (XrInstance, + * XrSession, etc.). */ + +#pragma once + +#include <atomic> +#include <future> +#include <vector> + +struct GHOST_XrControllerModelNode; + +/** + * OpenXR glTF controller model. + */ +class GHOST_XrControllerModel { + public: + GHOST_XrControllerModel(XrInstance instance, const char *subaction_path); + ~GHOST_XrControllerModel(); + + void load(XrSession session); + void updateComponents(XrSession session); + void getData(GHOST_XrControllerModelData &r_data); + + private: + XrPath m_subaction_path = XR_NULL_PATH; + XrControllerModelKeyMSFT m_model_key = XR_NULL_CONTROLLER_MODEL_KEY_MSFT; + + std::future<void> m_load_task; + std::atomic<bool> m_data_loaded = false; + + std::vector<GHOST_XrControllerModelVertex> m_vertices; + std::vector<uint32_t> m_indices; + std::vector<GHOST_XrControllerModelComponent> m_components; + std::vector<GHOST_XrControllerModelNode> m_nodes; + /** Maps node states to nodes. */ + std::vector<int32_t> m_node_state_indices; + + void loadControllerModel(XrSession session); +}; diff --git a/intern/ghost/intern/GHOST_XrSession.cpp b/intern/ghost/intern/GHOST_XrSession.cpp index cd930c8328b..c68cb5992e3 100644 --- a/intern/ghost/intern/GHOST_XrSession.cpp +++ b/intern/ghost/intern/GHOST_XrSession.cpp @@ -30,6 +30,7 @@ #include "GHOST_IXrGraphicsBinding.h" #include "GHOST_XrAction.h" #include "GHOST_XrContext.h" +#include "GHOST_XrControllerModel.h" #include "GHOST_XrException.h" #include "GHOST_XrSwapchain.h" #include "GHOST_Xr_intern.h" @@ -52,6 +53,8 @@ struct OpenXRSessionData { std::vector<GHOST_XrSwapchain> swapchains; std::map<std::string, GHOST_XrActionSet> action_sets; + /* Controller models identified by subaction path. */ + std::map<std::string, GHOST_XrControllerModel> controller_models; }; struct GHOST_XrDrawInfo { @@ -124,7 +127,9 @@ void GHOST_XrSession::initSystem() /** \name State Management * \{ */ -static void create_reference_spaces(OpenXRSessionData &oxr, const GHOST_XrPose &base_pose) +static void create_reference_spaces(OpenXRSessionData &oxr, + const GHOST_XrPose &base_pose, + bool isDebugMode) { XrReferenceSpaceCreateInfo create_info = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; create_info.poseInReferenceSpace.orientation.w = 1.0f; @@ -160,10 +165,11 @@ static void create_reference_spaces(OpenXRSessionData &oxr, const GHOST_XrPose & * since runtimes are not required to support the stage reference space. If the runtime * doesn't support it then just fall back to the local space. */ if (result == XR_ERROR_REFERENCE_SPACE_UNSUPPORTED) { - printf( - "Warning: XR runtime does not support stage reference space, falling back to local " - "reference space.\n"); - + if (isDebugMode) { + printf( + "Warning: XR runtime does not support stage reference space, falling back to local " + "reference space.\n"); + } create_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; CHECK_XR(xrCreateReferenceSpace(oxr.session, &create_info, &oxr.reference_space), "Failed to create local reference space."); @@ -179,11 +185,12 @@ static void create_reference_spaces(OpenXRSessionData &oxr, const GHOST_XrPose & CHECK_XR(xrGetReferenceSpaceBoundsRect(oxr.session, XR_REFERENCE_SPACE_TYPE_STAGE, &extents), "Failed to get stage reference space bounds."); if (extents.width == 0.0f || extents.height == 0.0f) { - printf( - "Warning: Invalid stage reference space bounds, falling back to local reference space. " - "To use the stage reference space, please define a tracking space via the XR " - "runtime.\n"); - + if (isDebugMode) { + printf( + "Warning: Invalid stage reference space bounds, falling back to local reference " + "space. To use the stage reference space, please define a tracking space via the XR " + "runtime.\n"); + } /* Fallback to local space. */ if (oxr.reference_space != XR_NULL_HANDLE) { CHECK_XR(xrDestroySpace(oxr.reference_space), "Failed to destroy stage reference space."); @@ -252,7 +259,7 @@ void GHOST_XrSession::start(const GHOST_XrSessionBeginInfo *begin_info) "detailed error information to the command line."); prepareDrawing(); - create_reference_spaces(*m_oxr, begin_info->base_pose); + create_reference_spaces(*m_oxr, begin_info->base_pose, m_context->isDebugMode()); /* Create and bind actions here. */ m_context->getCustomFuncs().session_create_fn(); @@ -300,6 +307,7 @@ GHOST_XrSession::LifeExpectancy GHOST_XrSession::handleStateChangeEvent( return SESSION_KEEP_ALIVE; } + /** \} */ /* State Management */ /* -------------------------------------------------------------------- */ @@ -916,3 +924,71 @@ void GHOST_XrSession::getActionCustomdataArray(const char *action_set_name, } /** \} */ /* Actions */ + +/* -------------------------------------------------------------------- */ +/** \name Controller Model + * + * \{ */ + +bool GHOST_XrSession::loadControllerModel(const char *subaction_path) +{ + if (!m_context->isExtensionEnabled(XR_MSFT_CONTROLLER_MODEL_EXTENSION_NAME)) { + return false; + } + + XrSession session = m_oxr->session; + std::map<std::string, GHOST_XrControllerModel> &controller_models = m_oxr->controller_models; + std::map<std::string, GHOST_XrControllerModel>::iterator it = controller_models.find( + subaction_path); + + if (it == controller_models.end()) { + XrInstance instance = m_context->getInstance(); + it = controller_models + .emplace(std::piecewise_construct, + std::make_tuple(subaction_path), + std::make_tuple(instance, subaction_path)) + .first; + } + + it->second.load(session); + + return true; +} + +void GHOST_XrSession::unloadControllerModel(const char *subaction_path) +{ + std::map<std::string, GHOST_XrControllerModel> &controller_models = m_oxr->controller_models; + if (controller_models.find(subaction_path) != controller_models.end()) { + controller_models.erase(subaction_path); + } +} + +bool GHOST_XrSession::updateControllerModelComponents(const char *subaction_path) +{ + XrSession session = m_oxr->session; + std::map<std::string, GHOST_XrControllerModel>::iterator it = m_oxr->controller_models.find( + subaction_path); + if (it == m_oxr->controller_models.end()) { + return false; + } + + it->second.updateComponents(session); + + return true; +} + +bool GHOST_XrSession::getControllerModelData(const char *subaction_path, + GHOST_XrControllerModelData &r_data) +{ + std::map<std::string, GHOST_XrControllerModel>::iterator it = m_oxr->controller_models.find( + subaction_path); + if (it == m_oxr->controller_models.end()) { + return false; + } + + it->second.getData(r_data); + + return true; +} + +/** \} */ /* Controller Model */ diff --git a/intern/ghost/intern/GHOST_XrSession.h b/intern/ghost/intern/GHOST_XrSession.h index a76e11aede1..f5c0c04e65d 100644 --- a/intern/ghost/intern/GHOST_XrSession.h +++ b/intern/ghost/intern/GHOST_XrSession.h @@ -53,7 +53,7 @@ class GHOST_XrSession { void draw(void *draw_customdata); /** Action functions to be called pre-session start. - * Note: The "destroy" functions can also be called post-session start. */ + * NOTE: The "destroy" functions can also be called post-session start. */ bool createActionSet(const GHOST_XrActionSetInfo &info); void destroyActionSet(const char *action_set_name); bool createActions(const char *action_set_name, uint32_t count, const GHOST_XrActionInfo *infos); @@ -90,6 +90,12 @@ class GHOST_XrSession { uint32_t getActionCount(const char *action_set_name); void getActionCustomdataArray(const char *action_set_name, void **r_customdata_array); + /** Controller model functions. */ + bool loadControllerModel(const char *subaction_path); + void unloadControllerModel(const char *subaction_path); + bool updateControllerModelComponents(const char *subaction_path); + bool getControllerModelData(const char *subaction_path, GHOST_XrControllerModelData &r_data); + private: /** Pointer back to context managing this session. Would be nice to avoid, but needed to access * custom callbacks set before session start. */ diff --git a/intern/ghost/test/CMakeLists.txt b/intern/ghost/test/CMakeLists.txt index 37bb00332dd..c564085c774 100644 --- a/intern/ghost/test/CMakeLists.txt +++ b/intern/ghost/test/CMakeLists.txt @@ -292,7 +292,7 @@ target_link_libraries(multitest_c guardedalloc_lib wcwidth_lib ${OPENGL_gl_LIBRARY} - ${FREETYPE_LIBRARY} + ${FREETYPE_LIBRARIES} ${BROTLI_LIBRARIES} ${ZLIB_LIBRARIES} ${CMAKE_DL_LIBS} ${PLATFORM_LINKLIBS} |