From eba2eb3a02959e9c1262b1b238b95f25e64f7a00 Mon Sep 17 00:00:00 2001 From: Adrian Iain Lam Date: Sun, 18 Oct 2020 09:33:33 +0100 Subject: [PATCH] Add command-line interface to control model This can now: - Start motions - Set expressions - Manually set Live2D parameters Some config file params renamed. --- CMakeLists.txt | 3 +- config.txt | 8 +- example/demo.patch | 263 +++++++++++++++--------- example/generate_patch.sh | 2 +- include/mouse_cursor_tracker.h | 54 +++-- src/mouse_cursor_tracker.cpp | 452 ++++++++++++++++++++++++++++++++++++++--- 6 files changed, 634 insertions(+), 148 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8780521..3a2ccf1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(MouseTrackerForCubism_project) find_library(xdo_LIBS NAMES xdo libxdo PATHS /usr/lib REQUIRED) find_library(pulse_LIBS NAMES pulse PATHS /usr/lib REQUIRED) +find_library(readline_LIBS NAMES readline libreadline /usr/lib REQUIRED) include_directories(include) @@ -17,4 +18,4 @@ set_target_properties( include/mouse_cursor_tracker.h ) -target_link_libraries(MouseTrackerForCubism ${xdo_LIBS} ${pulse_LIBS} pulse-simple) +target_link_libraries(MouseTrackerForCubism ${xdo_LIBS} ${pulse_LIBS} pulse-simple ${readline_LIBS}) diff --git a/config.txt b/config.txt index 1591db1..174f9e3 100644 --- a/config.txt +++ b/config.txt @@ -7,7 +7,7 @@ sleep_ms 5 # Automatic functionality in Live2D autoBlink 1 autoBreath 1 -randomMotion 0 +randomIdleMotion 1 useLipSync 1 # Lip sync configurations @@ -39,12 +39,12 @@ mouthForm 0 # If you have multiple screens, select the ID of the one you want to track. screen 0 -# The "middle" position, i.e. the coordinates of the cursor where +# The "origin" position, i.e. the coordinates of the cursor where # the Live2D model will be looking straight ahead. # For a 1920x1080 screen, {1600, 870} will be somewhere near the # bottom right corner. -middle_x 1600 -middle_y 870 +origin_x 1600 +origin_y 870 # The bounding box. These are the limits of the coordinates where the # Live2D model will be looking 30 degrees to each side. diff --git a/example/demo.patch b/example/demo.patch index 945a1ce..304b687 100644 --- a/example/demo.patch +++ b/example/demo.patch @@ -1,5 +1,5 @@ diff -pruN --exclude build ./demo_clean/CMakeLists.txt ./demo_dev/CMakeLists.txt ---- ./demo_clean/CMakeLists.txt 2020-10-02 02:01:04.825787688 +0100 +--- ./demo_clean/CMakeLists.txt 2020-10-01 22:47:25.846828066 +0100 +++ ./demo_dev/CMakeLists.txt 2020-10-01 23:29:15.530233484 +0100 @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16) # Set app name. @@ -43,8 +43,8 @@ diff -pruN --exclude build ./demo_clean/CMakeLists.txt ./demo_dev/CMakeLists.txt # Copy resource directory to build directory. add_custom_command( diff -pruN --exclude build ./demo_clean/scripts/make_gcc ./demo_dev/scripts/make_gcc ---- ./demo_clean/scripts/make_gcc 2020-10-02 02:01:04.825787688 +0100 -+++ ./demo_dev/scripts/make_gcc 2020-10-01 23:43:42.213875065 +0100 +--- ./demo_clean/scripts/make_gcc 2020-10-01 22:47:25.854827921 +0100 ++++ ./demo_dev/scripts/make_gcc 2020-10-12 03:42:07.847955578 +0100 @@ -10,4 +10,4 @@ BUILD_PATH=$SCRIPT_PATH/../build/make_gc cmake -S "$CMAKE_PATH" \ -B "$BUILD_PATH" \ @@ -52,7 +52,7 @@ diff -pruN --exclude build ./demo_clean/scripts/make_gcc ./demo_dev/scripts/make -cd "$BUILD_PATH" && make +cd "$BUILD_PATH" && make -j4 diff -pruN --exclude build ./demo_clean/src/CMakeLists.txt ./demo_dev/src/CMakeLists.txt ---- ./demo_clean/src/CMakeLists.txt 2020-10-02 02:01:04.829787750 +0100 +--- ./demo_clean/src/CMakeLists.txt 2020-10-01 22:47:25.850827994 +0100 +++ ./demo_dev/src/CMakeLists.txt 2020-10-01 22:47:24.842846271 +0100 @@ -19,6 +19,4 @@ target_sources(${APP_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/LAppView.cpp @@ -61,8 +61,25 @@ diff -pruN --exclude build ./demo_clean/src/CMakeLists.txt ./demo_dev/src/CMakeL - ${CMAKE_CURRENT_SOURCE_DIR}/TouchManager.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/TouchManager.hpp ) +diff -pruN --exclude build ./demo_clean/src/LAppDefine.cpp ./demo_dev/src/LAppDefine.cpp +--- ./demo_clean/src/LAppDefine.cpp 2020-10-01 22:47:25.850827994 +0100 ++++ ./demo_dev/src/LAppDefine.cpp 2020-10-18 04:59:13.238452938 +0100 +@@ -61,11 +61,11 @@ namespace LAppDefine { + const csmInt32 PriorityForce = 3; + + // デバッグ用ログの表示オプション +- const csmBool DebugLogEnable = true; ++ const csmBool DebugLogEnable = false; + const csmBool DebugTouchLogEnable = false; + + // Frameworkから出力するログのレベル設定 +- const CubismFramework::Option::LogLevel CubismLoggingLevel = CubismFramework::Option::LogLevel_Verbose; ++ const CubismFramework::Option::LogLevel CubismLoggingLevel = CubismFramework::Option::LogLevel_Warning; + + // デフォルトのレンダーターゲットサイズ + const csmInt32 RenderTargetWidth = 1900; diff -pruN --exclude build ./demo_clean/src/LAppDelegate.cpp ./demo_dev/src/LAppDelegate.cpp ---- ./demo_clean/src/LAppDelegate.cpp 2020-10-02 02:01:04.829787750 +0100 +--- ./demo_clean/src/LAppDelegate.cpp 2020-10-01 22:47:25.850827994 +0100 +++ ./demo_dev/src/LAppDelegate.cpp 2020-10-01 22:47:24.698848890 +0100 @@ -45,7 +45,8 @@ void LAppDelegate::ReleaseInstance() s_instance = NULL; @@ -192,7 +209,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppDelegate.cpp ./demo_dev/src/LApp Csm::csmVector LAppDelegate::Split(const std::string& baseString, char delimiter) diff -pruN --exclude build ./demo_clean/src/LAppDelegate.hpp ./demo_dev/src/LAppDelegate.hpp ---- ./demo_clean/src/LAppDelegate.hpp 2020-10-02 02:01:04.829787750 +0100 +--- ./demo_clean/src/LAppDelegate.hpp 2020-10-01 22:47:25.850827994 +0100 +++ ./demo_dev/src/LAppDelegate.hpp 2020-10-01 22:47:24.842846271 +0100 @@ -40,7 +40,8 @@ public: /** @@ -268,7 +285,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppDelegate.hpp ./demo_dev/src/LApp - -}; diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.cpp ./demo_dev/src/LAppLive2DManager.cpp ---- ./demo_clean/src/LAppLive2DManager.cpp 2020-10-02 02:01:04.829787750 +0100 +--- ./demo_clean/src/LAppLive2DManager.cpp 2020-10-01 22:47:25.850827994 +0100 +++ ./demo_dev/src/LAppLive2DManager.cpp 2020-10-02 02:00:49.961556700 +0100 @@ -52,9 +52,10 @@ void LAppLive2DManager::ReleaseInstance( @@ -373,7 +390,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.cpp ./demo_dev/src + _translateY = translateY; +} diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.hpp ./demo_dev/src/LAppLive2DManager.hpp ---- ./demo_clean/src/LAppLive2DManager.hpp 2020-10-02 02:01:04.825787688 +0100 +--- ./demo_clean/src/LAppLive2DManager.hpp 2020-10-01 22:47:25.846828066 +0100 +++ ./demo_dev/src/LAppLive2DManager.hpp 2020-10-01 23:36:24.583055381 +0100 @@ -6,12 +6,15 @@ */ @@ -450,18 +467,20 @@ diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.hpp ./demo_dev/src + float _translateY; }; diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppModel.cpp ---- ./demo_clean/src/LAppModel.cpp 2020-10-02 02:01:04.825787688 +0100 -+++ ./demo_dev/src/LAppModel.cpp 2020-10-01 23:34:43.482626010 +0100 -@@ -21,6 +21,8 @@ +--- ./demo_clean/src/LAppModel.cpp 2020-10-01 22:47:25.850827994 +0100 ++++ ./demo_dev/src/LAppModel.cpp 2020-10-18 09:26:08.998822685 +0100 +@@ -21,6 +21,10 @@ #include "LAppTextureManager.hpp" #include "LAppDelegate.hpp" +#include "mouse_cursor_tracker.h" + ++#include ++ using namespace Live2D::Cubism::Framework; using namespace Live2D::Cubism::Framework::DefaultParameterId; using namespace LAppDefine; -@@ -49,6 +51,7 @@ LAppModel::LAppModel() +@@ -49,6 +53,7 @@ LAppModel::LAppModel() : CubismUserModel() , _modelSetting(NULL) , _userTimeSeconds(0.0f) @@ -469,38 +488,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod { if (DebugLogEnable) { -@@ -128,30 +131,6 @@ void LAppModel::SetupModel(ICubismModelS - DeleteBuffer(buffer, path.GetRawString()); - } - -- //Expression -- if (_modelSetting->GetExpressionCount() > 0) -- { -- const csmInt32 count = _modelSetting->GetExpressionCount(); -- for (csmInt32 i = 0; i < count; i++) -- { -- csmString name = _modelSetting->GetExpressionName(i); -- csmString path = _modelSetting->GetExpressionFileName(i); -- path = _modelHomeDir + path; -- -- buffer = CreateBuffer(path.GetRawString(), &size); -- ACubismMotion* motion = LoadExpression(buffer, size, name.GetRawString()); -- -- if (_expressions[name] != NULL) -- { -- ACubismMotion::Delete(_expressions[name]); -- _expressions[name] = NULL; -- } -- _expressions[name] = motion; -- -- DeleteBuffer(buffer, path.GetRawString()); -- } -- } -- - //Physics - if (strcmp(_modelSetting->GetPhysicsFileName(), "") != 0) - { -@@ -335,59 +314,72 @@ void LAppModel::Update() +@@ -335,59 +340,110 @@ void LAppModel::Update() const csmFloat32 deltaTimeSeconds = LAppPal::GetDeltaTime(); _userTimeSeconds += deltaTimeSeconds; @@ -514,13 +502,13 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod - //----------------------------------------------------------------- - _model->LoadParameters(); // 前回セーブされた状態をロード - if (_motionManager->IsFinished()) -+ if (_tracker) - { +- { - // モーションの再生がない場合、待機モーションの中からランダムで再生する - StartRandomMotion(MotionGroupIdle, PriorityIdle); - } - else -- { ++ if (_tracker) + { - motionUpdated = _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新 - } - _model->SaveParameters(); // 状態を保存 @@ -532,61 +520,97 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod - if (!motionUpdated) - { - if (_eyeBlink != NULL) -+ // NOTE: Apparently, this LoadParameters/SaveParameters pair -+ // is needed for auto breath to work. + _model->LoadParameters(); // 前回セーブされた状態をロード -+ if (_motionManager->IsFinished() && params.randomMotion) ++ ++ int paramsMotionPriority = static_cast(params.motionPriority); ++ ++ if (paramsMotionPriority != PriorityNone) { - // メインモーションの更新がないとき - _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); // 目パチ ++ StartMotion(params.motionGroup.c_str(), params.motionNumber, ++ paramsMotionPriority); ++ } ++ else if (params.randomIdleMotion && _motionManager->IsFinished()) ++ { + // モーションの再生がない場合、待機モーションの中からランダムで再生する + StartRandomMotion(MotionGroupIdle, PriorityIdle); } - } -+ else -+ { -+ _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新 -+ } -+ _model->SaveParameters(); // 状態を保存 - if (_expressionManager != NULL) - { - _expressionManager->UpdateMotion(_model, deltaTimeSeconds); // 表情でパラメータ更新(相対変化) - } ++ // FIXME pose does not return to normal after motion ++ // if we don't have randomIdleMotion set ++ else ++ { ++ _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新 ++ } ++ _model->SaveParameters(); // 状態を保存 - //ドラッグによる変化 - //ドラッグによる顔の向きの調整 - _model->AddParameterValue(_idParamAngleX, _dragX * 30); // -30から30の値を加える - _model->AddParameterValue(_idParamAngleY, _dragY * 30); - _model->AddParameterValue(_idParamAngleZ, _dragX * _dragY * -30); -+ if (params.autoBlink && _eyeBlink) ++ if (params.expression != "") + { -+ _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); ++ SetExpression(params.expression.c_str()); + } -+ else ++ if (_expressionManager != NULL) + { -+ _model->SetParameterValue(idMan->GetId("ParamEyeLOpen"), -+ params.leftEyeOpenness); -+ _model->SetParameterValue(idMan->GetId("ParamEyeROpen"), -+ params.rightEyeOpenness); ++ _expressionManager->UpdateMotion(_model, deltaTimeSeconds); // 表情でパラメータ更新(相対変化) + } - //ドラッグによる体の向きの調整 - _model->AddParameterValue(_idParamBodyAngleX, _dragX * 10); // -10から10の値を加える -+ _model->SetParameterValue(idMan->GetId("ParamMouthForm"), -+ params.mouthForm); ++ bool autoBlink = params.autoBlink && _eyeBlink; ++ auto eyeLOpenIt = params.live2d.find("ParamEyeLOpen"); ++ auto eyeROpenIt = params.live2d.find("ParamEyeROpen"); - //ドラッグによる目の向きの調整 - _model->AddParameterValue(_idParamEyeBallX, _dragX); // -1から1の値を加える - _model->AddParameterValue(_idParamEyeBallY, _dragY); -+ if (params.useLipSync && _lipSync) ++ if (autoBlink) + { -+ csmFloat32 value = params.lipSyncParam; // 0 to 1 ++ // Handle blink first ++ _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); ++ } - // 呼吸など - if (_breath != NULL) - { - _breath->UpdateParameters(_model, deltaTimeSeconds); ++ if (eyeLOpenIt != params.live2d.end()) ++ { ++ // If value specified, override blinking ++ _model->SetParameterValue(idMan->GetId("ParamEyeLOpen"), ++ eyeLOpenIt->second); ++ } ++ else if (!autoBlink) ++ { ++ // If no value specified and no auto blink, set to 1 ++ _model->SetParameterValue(idMan->GetId("ParamEyeLOpen"), 1); ++ ++ } ++ ++ if (eyeROpenIt != params.live2d.end()) ++ { ++ _model->SetParameterValue(idMan->GetId("ParamEyeROpen"), ++ eyeROpenIt->second); ++ } ++ else if (!autoBlink) ++ { ++ _model->SetParameterValue(idMan->GetId("ParamEyeROpen"), 1); ++ } ++ ++ ++ if (params.useLipSync && _lipSync) ++ { ++ csmFloat32 value = params.lipSyncParam; // 0 to 1 ++ + for (csmUint32 i = 0; i < _lipSyncIds.GetSize(); ++i) + { + _model->AddParameterValue(_lipSyncIds[i], value, 0.8f); @@ -595,19 +619,21 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod + else + { + _model->SetParameterValue(idMan->GetId("ParamMouthOpenY"), -+ params.mouthOpenness); ++ params.live2d["ParamMouthOpenY"]); ++ } ++ ++ for (auto const &entry : params.live2d) ++ { ++ std::string key = entry.first; ++ double val = entry.second; ++ ++ if (key != "ParamEyeLOpen" && key != "ParamEyeROpen" && ++ key != "ParamMouthOpenY") ++ { ++ _model->SetParameterValue(idMan->GetId(key.c_str()), val); ++ } + } + -+ _model->SetParameterValue(idMan->GetId("ParamEyeLSmile"), -+ params.leftEyeSmile); -+ _model->SetParameterValue(idMan->GetId("ParamEyeRSmile"), -+ params.rightEyeSmile); -+ _model->SetParameterValue(idMan->GetId("ParamAngleX"), -+ params.faceXAngle); -+ _model->SetParameterValue(idMan->GetId("ParamAngleY"), -+ params.faceYAngle); -+ _model->SetParameterValue(idMan->GetId("ParamAngleZ"), -+ params.faceZAngle); + if (params.autoBreath && _breath) + { + // Note: _model->LoadParameters and SaveParameters is needed @@ -617,7 +643,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod } // 物理演算の設定 -@@ -396,17 +388,6 @@ void LAppModel::Update() +@@ -396,17 +452,6 @@ void LAppModel::Update() _physics->Evaluate(_model, deltaTimeSeconds); } @@ -635,7 +661,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod // ポーズの設定 if (_pose != NULL) { -@@ -626,3 +607,9 @@ Csm::Rendering::CubismOffscreenFrame_Ope +@@ -626,3 +671,14 @@ Csm::Rendering::CubismOffscreenFrame_Ope { return _renderBuffer; } @@ -645,9 +671,14 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod + _tracker = tracker; +} + ++Csm::ICubismModelSetting* LAppModel::GetModelSetting(void) const ++{ ++ return _modelSetting; ++} ++ diff -pruN --exclude build ./demo_clean/src/LAppModel.hpp ./demo_dev/src/LAppModel.hpp ---- ./demo_clean/src/LAppModel.hpp 2020-10-02 02:01:04.829787750 +0100 -+++ ./demo_dev/src/LAppModel.hpp 2020-10-01 23:35:39.254849094 +0100 +--- ./demo_clean/src/LAppModel.hpp 2020-10-01 22:47:25.850827994 +0100 ++++ ./demo_dev/src/LAppModel.hpp 2020-10-18 03:04:52.142045751 +0100 @@ -13,6 +13,7 @@ #include #include @@ -656,7 +687,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.hpp ./demo_dev/src/LAppMod /** * @brief ユーザーが実際に使用するモデルの実装クラス
-@@ -113,6 +114,13 @@ public: +@@ -113,6 +114,15 @@ public: */ Csm::Rendering::CubismOffscreenFrame_OpenGLES2& GetRenderBuffer(); @@ -667,10 +698,12 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.hpp ./demo_dev/src/LAppMod + */ + void SetTracker(MouseCursorTracker *tracker); + ++ Csm::ICubismModelSetting* GetModelSetting(void) const; ++ protected: /** * @brief モデルを描画する処理。モデルを描画する空間のView-Projection行列を渡す。 -@@ -183,6 +191,8 @@ private: +@@ -183,6 +193,8 @@ private: const Csm::CubismId* _idParamEyeBallY; ///< パラメータID: ParamEyeBallXY Csm::Rendering::CubismOffscreenFrame_OpenGLES2 _renderBuffer; ///< フレームバッファ以外の描画先 @@ -680,8 +713,8 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.hpp ./demo_dev/src/LAppMod diff -pruN --exclude build ./demo_clean/src/LAppPal.cpp ./demo_dev/src/LAppPal.cpp ---- ./demo_clean/src/LAppPal.cpp 2020-10-02 02:01:04.829787750 +0100 -+++ ./demo_dev/src/LAppPal.cpp 2020-10-01 22:47:24.722848453 +0100 +--- ./demo_clean/src/LAppPal.cpp 2020-10-01 22:47:25.850827994 +0100 ++++ ./demo_dev/src/LAppPal.cpp 2020-10-18 04:57:43.289600308 +0100 @@ -6,6 +6,7 @@ */ @@ -690,7 +723,15 @@ diff -pruN --exclude build ./demo_clean/src/LAppPal.cpp ./demo_dev/src/LAppPal.c #include #include #include -@@ -45,10 +46,7 @@ csmByte* LAppPal::LoadFileAsBytes(const +@@ -36,7 +37,6 @@ csmByte* LAppPal::LoadFileAsBytes(const + if (stat(path, &statBuf) == 0) + { + size = statBuf.st_size; +- PrintLog(path); + } + + std::fstream file; +@@ -45,10 +45,7 @@ csmByte* LAppPal::LoadFileAsBytes(const file.open(path, std::ios::in | std::ios::binary); if (!file.is_open()) { @@ -703,7 +744,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppPal.cpp ./demo_dev/src/LAppPal.c } file.read(buf, size); diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.cpp ./demo_dev/src/LAppTextureManager.cpp ---- ./demo_clean/src/LAppTextureManager.cpp 2020-10-02 02:01:04.833787812 +0100 +--- ./demo_clean/src/LAppTextureManager.cpp 2020-10-01 22:47:25.850827994 +0100 +++ ./demo_dev/src/LAppTextureManager.cpp 2020-10-01 22:47:24.654849690 +0100 @@ -96,6 +96,46 @@ LAppTextureManager::TextureInfo* LAppTex @@ -753,7 +794,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.cpp ./demo_dev/sr { for (Csm::csmUint32 i = 0; i < _textures.GetSize(); i++) diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.hpp ./demo_dev/src/LAppTextureManager.hpp ---- ./demo_clean/src/LAppTextureManager.hpp 2020-10-02 02:01:04.825787688 +0100 +--- ./demo_clean/src/LAppTextureManager.hpp 2020-10-01 22:47:25.846828066 +0100 +++ ./demo_dev/src/LAppTextureManager.hpp 2020-10-01 22:47:24.786847290 +0100 @@ -72,6 +72,8 @@ public: */ @@ -765,7 +806,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.hpp ./demo_dev/sr * @brief 画像の解放 * diff -pruN --exclude build ./demo_clean/src/LAppView.cpp ./demo_dev/src/LAppView.cpp ---- ./demo_clean/src/LAppView.cpp 2020-10-02 02:01:04.833787812 +0100 +--- ./demo_clean/src/LAppView.cpp 2020-10-01 22:47:25.850827994 +0100 +++ ./demo_dev/src/LAppView.cpp 2020-10-01 22:47:24.602850636 +0100 @@ -13,7 +13,6 @@ #include "LAppLive2DManager.hpp" @@ -942,7 +983,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppView.cpp ./demo_dev/src/LAppView - } } diff -pruN --exclude build ./demo_clean/src/LAppView.hpp ./demo_dev/src/LAppView.hpp ---- ./demo_clean/src/LAppView.hpp 2020-10-02 02:01:04.825787688 +0100 +--- ./demo_clean/src/LAppView.hpp 2020-10-01 22:47:25.846828066 +0100 +++ ./demo_dev/src/LAppView.hpp 2020-10-01 22:47:24.802846999 +0100 @@ -14,7 +14,6 @@ #include "CubismFramework.hpp" @@ -998,15 +1039,18 @@ diff -pruN --exclude build ./demo_clean/src/LAppView.hpp ./demo_dev/src/LAppView // レンダリング先を別ターゲットにする方式の場合に使用 LAppSprite* _renderSprite; ///< モードによっては_renderBufferのテクスチャを描画 diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp ---- ./demo_clean/src/main.cpp 2020-10-02 02:01:04.825787688 +0100 -+++ ./demo_dev/src/main.cpp 2020-10-01 23:42:12.845205308 +0100 -@@ -5,18 +5,154 @@ +--- ./demo_clean/src/main.cpp 2020-10-01 22:47:25.846828066 +0100 ++++ ./demo_dev/src/main.cpp 2020-10-18 07:03:46.194220443 +0100 +@@ -5,18 +5,182 @@ * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. */ +#include +#include +#include ++#include ++#include ++#include + +#ifdef __cpp_lib_filesystem +#include @@ -1016,8 +1060,10 @@ diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp +namespace fs = std::experimental::filesystem; +#endif + ++#include "ICubismModelSetting.hpp" #include "LAppDelegate.hpp" +#include "LAppLive2DManager.hpp" ++#include "LAppModel.hpp" +#include "mouse_cursor_tracker.h" + +struct CmdArgs @@ -1140,16 +1186,39 @@ diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp - LAppDelegate::GetInstance()->Run(); + delegate->SetRootDirectory(cmdArgs.rootDir); + -+ MouseCursorTracker tracker(cmdArgs.cfgPath); -+ -+ std::thread trackerThread(&MouseCursorTracker::mainLoop, &tracker); -+ + LAppLive2DManager *manager = LAppLive2DManager::GetInstance(); + manager->SetModel(cmdArgs.modelName); + + manager->SetProjectionScaleTranslate(cmdArgs.scaleFactor, + cmdArgs.translateX, + cmdArgs.translateY); ++ ++ LAppModel *model = manager->GetModel(0); ++ if (!model) throw std::runtime_error("model is null"); ++ ++ Live2D::Cubism::Framework::ICubismModelSetting *modelSetting = model->GetModelSetting(); ++ if (!modelSetting) throw std::runtime_error("modelSetting is null"); ++ ++ std::vector > motions; ++ int motionGroupCount = modelSetting->GetMotionGroupCount(); ++ for (int i = 0; i < motionGroupCount; i++) ++ { ++ const char *motionGroup = modelSetting->GetMotionGroupName(i); ++ int motionCount = modelSetting->GetMotionCount(motionGroup); ++ motions.push_back(std::make_pair(std::string(motionGroup), motionCount)); ++ } ++ ++ std::vector expressions; ++ int expCount = modelSetting->GetExpressionCount(); ++ for (int i = 0; i < expCount; i++) ++ { ++ const char *expName = modelSetting->GetExpressionName(i); ++ expressions.push_back(std::string(expName)); ++ } ++ ++ MouseCursorTracker tracker(cmdArgs.cfgPath, motions, expressions); ++ ++ std::thread trackerThread(&MouseCursorTracker::mainLoop, &tracker); + manager->SetTracker(&tracker); + + delegate->Run(); diff --git a/example/generate_patch.sh b/example/generate_patch.sh index 068f4b0..738ff0a 100755 --- a/example/generate_patch.sh +++ b/example/generate_patch.sh @@ -1,5 +1,5 @@ #!/bin/sh mkdir -p demo_clean -cp -r CubismSdkForNative-4-r.1/Samples/OpenGL/Demo/proj.linux.cmake/* ./demo_clean/ +cp -p -r CubismSdkForNative-4-r.1/Samples/OpenGL/Demo/proj.linux.cmake/* ./demo_clean/ diff -pruN --exclude build ./demo_clean ./demo_dev > ./demo.patch diff --git a/include/mouse_cursor_tracker.h b/include/mouse_cursor_tracker.h index a53713f..6cd91c4 100644 --- a/include/mouse_cursor_tracker.h +++ b/include/mouse_cursor_tracker.h @@ -26,8 +26,11 @@ SOFTWARE. ****/ #include -#include #include +#include +#include +#include +#include extern "C" { #include @@ -37,28 +40,38 @@ extern "C" class MouseCursorTracker { public: - MouseCursorTracker(std::string cfgPath); + MouseCursorTracker(std::string cfgPath, + std::vector > motions = {}, + std::vector expressions = {}); ~MouseCursorTracker(); + enum class MotionPriority + { + // See LAppDefine.cpp in Demo + none, + idle, + normal, + force + }; + struct Params { - double leftEyeOpenness; - double rightEyeOpenness; - double leftEyeSmile; - double rightEyeSmile; - double mouthOpenness; - double mouthForm; - double faceXAngle; - double faceYAngle; - double faceZAngle; + std::map live2d; + bool autoBlink; bool autoBreath; - bool randomMotion; + bool randomIdleMotion; bool useLipSync; double lipSyncParam; + + MotionPriority motionPriority; + std::string motionGroup; + int motionNumber; + + std::string expression; }; - Params getParams(void) const; + Params getParams(void); void stop(void); @@ -76,7 +89,7 @@ private: int sleepMs; bool autoBlink; bool autoBreath; - bool randomMotion; + bool randomIdleMotion; bool useLipSync; double lipSyncGain; double lipSyncCutOff; @@ -87,9 +100,11 @@ private: int left; int right; int screen; - Coord middle; + Coord origin; } m_cfg; + std::map m_overrideMap; + bool m_stop; Coord m_curPos; @@ -97,10 +112,19 @@ private: xdo_t *m_xdo; std::thread m_getVolumeThread; + std::thread m_parseCommandThread; void audioLoop(void); + void cliLoop(void); + void processCommand(std::string); double m_currentVol; pa_simple *m_pulse; + MotionPriority m_motionPriority; + std::string m_motionGroup; + int m_motionNumber; + std::mutex m_motionMutex; + std::string m_expression; + void populateDefaultConfig(void); void parseConfig(std::string cfgPath); }; diff --git a/src/mouse_cursor_tracker.cpp b/src/mouse_cursor_tracker.cpp index 8ccdb35..9892482 100644 --- a/src/mouse_cursor_tracker.cpp +++ b/src/mouse_cursor_tracker.cpp @@ -27,15 +27,22 @@ SOFTWARE. #include #include #include +#include +#include #include #include +#include #include +#include +#include extern "C" { #include #include +#include +#include } #include "mouse_cursor_tracker.h" @@ -49,9 +56,209 @@ static double rms(float *buf, std::size_t count) return std::sqrt(sum / count); } -MouseCursorTracker::MouseCursorTracker(std::string cfgPath) +static std::vector split(std::string s) +{ + std::vector v; + std::string tmp; + + for (std::size_t i = 0; i < s.length(); i++) + { + char c = s[i]; + if (std::isspace(c)) + { + if (tmp != "") + { + v.push_back(tmp); + tmp = ""; + } + } + else + { + tmp += c; + } + } + if (tmp != "") + { + v.push_back(tmp); + } + return v; +} + + +/* Using readline callback functions means that we need to pass + * information from our class to the callback, and the only way + * to do so is using globals. + */ + +// Taken from https://github.com/eliben/code-for-blog/blob/master/2016/readline-samples/utils.cpp +static std::string longest_common_prefix(std::string s, + const std::vector& candidates) { + assert(candidates.size() > 0); + if (candidates.size() == 1) { + return candidates[0]; + } + + std::string prefix(s); + while (true) { + // Each iteration of this loop advances to the next location in all the + // candidates and sees if they match up to it. + size_t nextloc = prefix.size(); + auto i = candidates.begin(); + if (i->size() <= nextloc) { + return prefix; + } + char nextchar = (*(i++))[nextloc]; + for (; i != candidates.end(); ++i) { + if (i->size() <= nextloc || (*i)[nextloc] != nextchar) { + // Bail out if there's a mismatch for this candidate. + return prefix; + } + } + // All candidates have contents[nextloc] == nextchar, so we can safely + // extend the prefix. + prefix.append(1, nextchar); + } + + assert(0 && "unreachable"); +} + +std::vector commands = +{ + "help", "motion", "expression", "set", "clear" +}; + +std::vector live2dParams = +{ // https://docs.live2d.com/cubism-editor-manual/standard-parametor-list/?locale=ja + "ParamAngleX", "ParamAngleY", "ParamAngleZ", + "ParamEyeLOpen", "ParamEyeLSmile", "ParamEyeROpen", "ParamEyeRSmile", + "ParamEyeBallX", "ParamEyeBallY", "ParamEyeBallForm", + "ParamBrowLY", "ParamBrowRY", "ParamBrowLX", "ParamBrowRX", + "ParamBrowLAngle", "ParamBrowRAngle", "ParamBrowLForm", "ParamBrowRForm", + "ParamMouthForm", "ParamMouthOpenY", "ParamTere", + "ParamBodyAngleX", "ParamBodyAngleY", "ParamBodyAngleZ", "ParamBreath", + "ParamArmLA", "ParamArmRA", "ParamArmLB", "ParamArmRB", + "ParamHandL", "ParamHandR", + "ParamHairFront", "ParamHairSide", "ParamHairBack", "ParamHairFluffy", + "ParamShoulderY", "ParamBustX", "ParamBustY", + "ParamBaseX", "ParamBaseY" +}; +std::vector > MCT_motions; +std::vector MCT_expressions; +std::map *MCT_overrideMap; + +static char **cliCompletionFunction(const char *textCStr, int start, int end) +{ + // Reference: https://github.com/eliben/code-for-blog/blob/master/2016/readline-samples/readline-complete-subcommand.cpp + rl_attempted_completion_over = 1; + + std::string line(rl_line_buffer); + + std::vector cmdline = split(line); + + std::vector constructed; + std::vector *vocab = nullptr; + + if (cmdline.size() == 0 || + (cmdline.size() == 1 && line.back() != ' ') || + cmdline[0] == "help") + { + vocab = &commands; + } + else if (cmdline[0] == "motion") + { + for (auto it = MCT_motions.begin(); it != MCT_motions.end(); ++it) + { + if ((cmdline.size() == 1 && line.back() == ' ') || + (cmdline.size() == 2 && line.back() != ' ')) + { // motionGroup + { + constructed.push_back(it->first); + } + } + else if ((cmdline.size() == 2 && line.back() == ' ') || + (cmdline.size() == 3 && line.back() != ' ')) + { // motionNumber + if (it->first == cmdline[1]) + { + for (int i = 0; i < it->second; i++) + { + constructed.push_back(std::to_string(i)); + } + break; + } + } + else if (cmdline.size() <= 4) + { // priority + for (int i = 0; i < 4; i++) + { + constructed.push_back(std::to_string(i)); + } + } + } + + vocab = &constructed; + } + else if (cmdline[0] == "expression") + { + vocab = &MCT_expressions; + } + else if (cmdline[0] == "set") + { + if ((cmdline.size() % 2 == 0 && line.back() != ' ') || + (cmdline.size() % 2 == 1 && line.back() == ' ')) + { + vocab = &live2dParams; + } + } + else if (cmdline[0] == "clear") + { + constructed.push_back("all"); + for (auto const &entry : *MCT_overrideMap) + { + constructed.push_back(entry.first); + } + vocab = &constructed; + } + + if (!vocab) + { + return nullptr; + } + + std::string text(textCStr); + std::vector matches; + std::copy_if(vocab->begin(), vocab->end(), std::back_inserter(matches), + [&text](const std::string &s) + { + return (s.size() >= text.size() && + s.compare(0, text.size(), text) == 0); + }); + + if (matches.empty()) + { + return nullptr; + } + + char** array = + static_cast(malloc((2 + matches.size()) * sizeof(*array))); + array[0] = strdup(longest_common_prefix(text, matches).c_str()); + size_t ptr = 1; + for (const auto& m : matches) { + array[ptr++] = strdup(m.c_str()); + } + array[ptr] = nullptr; + return array; +} + +MouseCursorTracker::MouseCursorTracker(std::string cfgPath, + std::vector > motions, + std::vector expressions) : m_stop(false) { + m_motionPriority = MotionPriority::none; + m_motionNumber = 0; + + parseConfig(cfgPath); m_xdo = xdo_new(nullptr); @@ -69,6 +276,11 @@ MouseCursorTracker::MouseCursorTracker(std::string cfgPath) } m_getVolumeThread = std::thread(&MouseCursorTracker::audioLoop, this); + m_parseCommandThread = std::thread(&MouseCursorTracker::cliLoop, this); + + MCT_motions = motions; + MCT_expressions = expressions; + MCT_overrideMap = &m_overrideMap; } void MouseCursorTracker::audioLoop(void) @@ -89,10 +301,167 @@ void MouseCursorTracker::audioLoop(void) delete[] buf; } +void MouseCursorTracker::cliLoop(void) +{ + rl_catch_signals = 0; + rl_attempted_completion_function = cliCompletionFunction; + while (!m_stop) + { + char *buf = readline(">> "); + + if (buf) + { + std::string cmdline(buf); + free(buf); + processCommand(cmdline); + } + else + { + std::cout << "Exiting CLI loop. Use Ctrl+C to exit the whole process." << std::endl; + stop(); + } + } +} + + +void MouseCursorTracker::processCommand(std::string cmdline) +{ + auto cmdSplit = split(cmdline); + + if (cmdSplit.size() > 0) + { + add_history(cmdline.c_str()); + + if (cmdSplit[0] == "help") + { + if (cmdSplit.size() == 1) + { + std::cout << "Available commands: motion set clear\n" + << "Type \"help \" for more help" << std::endl; + } + else if (cmdSplit[1] == "motion") + { + std::cout << "motion []\n" + << "motionGroup: The motion name in the .model3.json file\n" + << "motionNumber: The index of this motion in the .model3.json file, 0-indexed\n" + << "priority: 0 = none, 1 = idle, 2 = normal, 3 = force (default normal)" << std::endl; + } + else if (cmdSplit[1] == "expression") + { + std::cout << "expression \n" + << "expressionName: Name of expression in the .model3.json file" << std::endl; + } + else if (cmdSplit[1] == "set") + { + std::cout << "set [ ...]\n" + << "Set parameter value. Overrides any tracking." + << "See live2D documentation for full list of params" << std::endl; + } + else if (cmdSplit[1] == "clear") + { + std::cout << "clear [ ...]\n" + << "Clear parameter value. Re-enables tracking if it was overridden by \"set\"\n" + << "You can also use \"clear all\" to clear everything" << std::endl; + } + else + { + std::cout << "Unrecognized command" << std::endl; + } + } + else if (cmdSplit[0] == "motion") + { + if (cmdSplit.size() == 3 || cmdSplit.size() == 4) + { + std::unique_lock lock(m_motionMutex, std::defer_lock); + lock.lock(); + m_motionGroup = cmdSplit[1]; + try + { + m_motionNumber = std::stoi(cmdSplit[2]); + if (cmdSplit.size() == 4) + { + m_motionPriority = static_cast(std::stoi(cmdSplit[3])); + } + else + { + m_motionPriority = MotionPriority::normal; + } + } + catch (const std::exception &e) + { + std::cerr << "std::stoi failed" << std::endl; + } + lock.unlock(); + } + else + { + std::cerr << "Incorrect command, expecting 2 or 3 arguments" << std::endl; + std::cerr << "motion motionGroup motionNumber [motionPriority]" << std::endl; + } + } + else if (cmdSplit[0] == "expression") + { + if (cmdSplit.size() == 2) + { + std::unique_lock lock(m_motionMutex, std::defer_lock); + lock.lock(); + m_expression = cmdSplit[1]; + lock.unlock(); + } + else + { + std::cerr << "Incorrect command, expecting 1 argument: expressionName" << std::endl; + } + } + else if (cmdSplit[0] == "set") + { + if (cmdSplit.size() % 2 != 1) + { + // "set param1 value1 param2 value2 ..." + std::cerr << "Incorrect number of arguments for command 'set'" << std::endl; + } + for (std::size_t i = 1; i < cmdSplit.size(); i += 2) + { + try + { + m_overrideMap[cmdSplit[i]] = std::stod(cmdSplit[i + 1]); + } + catch (const std::exception &e) + { + std::cerr << "std::stod failed" << std::endl; + } + + std::cerr << "Debug: setting " << cmdSplit[i] << std::endl; + } + } + else if (cmdSplit[0] == "clear") + { + for (std::size_t i = 1; i < cmdSplit.size(); i++) + { + if (cmdSplit[i] == "all") + { + m_overrideMap.clear(); + break; + } + std::size_t removed = m_overrideMap.erase(cmdSplit[i]); + if (removed == 0) + { + std::cerr << "Warning: key " << cmdSplit[i] << " not found" << std::endl; + } + } + } + else + { + std::cerr << "Unknown command" << std::endl; + } + } +} + MouseCursorTracker::~MouseCursorTracker() { xdo_free(m_xdo); m_getVolumeThread.join(); + m_parseCommandThread.join(); pa_simple_free(m_pulse); } @@ -101,47 +470,42 @@ void MouseCursorTracker::stop(void) m_stop = true; } -MouseCursorTracker::Params MouseCursorTracker::getParams(void) const +MouseCursorTracker::Params MouseCursorTracker::getParams(void) { Params params = Params(); - int xOffset = m_curPos.x - m_cfg.middle.x; - int leftRange = m_cfg.middle.x - m_cfg.left; - int rightRange = m_cfg.right - m_cfg.middle.x; + int xOffset = m_curPos.x - m_cfg.origin.x; + int leftRange = m_cfg.origin.x - m_cfg.left; + int rightRange = m_cfg.right - m_cfg.origin.x; if (xOffset > 0) // i.e. to the right { - params.faceXAngle = 30.0 * xOffset / rightRange; + params.live2d["ParamAngleX"] = 30.0 * xOffset / rightRange; } else // to the left { - params.faceXAngle = 30.0 * xOffset / leftRange; + params.live2d["ParamAngleX"] = 30.0 * xOffset / leftRange; } - int yOffset = m_curPos.y - m_cfg.middle.y; - int topRange = m_cfg.middle.y - m_cfg.top; - int bottomRange = m_cfg.bottom - m_cfg.middle.y; + int yOffset = m_curPos.y - m_cfg.origin.y; + int topRange = m_cfg.origin.y - m_cfg.top; + int bottomRange = m_cfg.bottom - m_cfg.origin.y; if (yOffset > 0) // downwards { - params.faceYAngle = -30.0 * yOffset / bottomRange; + params.live2d["ParamAngleY"] = -30.0 * yOffset / bottomRange; } else // upwards { - params.faceYAngle = -30.0 * yOffset / topRange; + params.live2d["ParamAngleY"] = -30.0 * yOffset / topRange; } - params.faceZAngle = 0; - - params.leftEyeOpenness = 1; - params.rightEyeOpenness = 1; - params.autoBlink = m_cfg.autoBlink; params.autoBreath = m_cfg.autoBreath; - params.randomMotion = m_cfg.randomMotion; + params.randomIdleMotion = m_cfg.randomIdleMotion; params.useLipSync = m_cfg.useLipSync; - params.mouthForm = m_cfg.mouthForm; + params.live2d["ParamMouthForm"] = m_cfg.mouthForm; if (m_cfg.useLipSync) { @@ -156,10 +520,38 @@ MouseCursorTracker::Params MouseCursorTracker::getParams(void) const } } + // Don't block in getParams() + std::unique_lock lock(m_motionMutex, std::try_to_lock); + if (lock.owns_lock()) + { + if (m_motionPriority != MotionPriority::none) + { + params.motionPriority = m_motionPriority; + params.motionGroup = m_motionGroup; + params.motionNumber = m_motionNumber; + + m_motionPriority = MotionPriority::none; + m_motionGroup = ""; + m_motionNumber = 0; + } + params.expression = m_expression; + m_expression = ""; + lock.unlock(); + } // Leave everything else as zero + // Process overrides + for (auto const &x : m_overrideMap) + { + std::string key = x.first; + double val = x.second; + + params.live2d[key] = val; + } + + return params; } @@ -230,11 +622,11 @@ void MouseCursorTracker::parseConfig(std::string cfgPath) throw std::runtime_error("Error parsing autoBreath"); } } - else if (paramName == "randomMotion") + else if (paramName == "randomIdleMotion") { - if (!(ss >> m_cfg.randomMotion)) + if (!(ss >> m_cfg.randomIdleMotion)) { - throw std::runtime_error("Error parsing randomMotion"); + throw std::runtime_error("Error parsing randomIdleMotion"); } } else if (paramName == "useLipSync") @@ -279,18 +671,18 @@ void MouseCursorTracker::parseConfig(std::string cfgPath) throw std::runtime_error("Error parsing screen"); } } - else if (paramName == "middle_x") + else if (paramName == "origin_x") { - if (!(ss >> m_cfg.middle.x)) + if (!(ss >> m_cfg.origin.x)) { - throw std::runtime_error("Error parsing middle_x"); + throw std::runtime_error("Error parsing origin_x"); } } - else if (paramName == "middle_y") + else if (paramName == "origin_y") { - if (!(ss >> m_cfg.middle.y)) + if (!(ss >> m_cfg.origin.y)) { - throw std::runtime_error("Error parsing middle_y"); + throw std::runtime_error("Error parsing origin_y"); } } else if (paramName == "top") @@ -335,7 +727,7 @@ void MouseCursorTracker::populateDefaultConfig(void) m_cfg.sleepMs = 5; m_cfg.autoBlink = true; m_cfg.autoBreath = true; - m_cfg.randomMotion = false; + m_cfg.randomIdleMotion = false; m_cfg.useLipSync = true; m_cfg.lipSyncGain = 10; m_cfg.lipSyncCutOff = 0.15; @@ -347,6 +739,6 @@ void MouseCursorTracker::populateDefaultConfig(void) m_cfg.right = 1919; // These will be the full screen for 1920x1080 m_cfg.screen = 0; - m_cfg.middle = {1600, 870}; // Somewhere near the bottom right + m_cfg.origin = {1600, 870}; // Somewhere near the bottom right } -- 2.7.4