From 702b083e4762fa37f5436059aedb543397381ba7 Mon Sep 17 00:00:00 2001 From: Misaki Date: Thu, 25 Dec 2025 17:35:12 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=96=B0=E5=A2=9E=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E7=82=B9=E5=87=BB=E6=8E=A5=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E4=B8=BAWindows=E6=B7=BB=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E5=AE=8C=E7=BE=8E=E7=9A=84=E9=80=8F=E6=98=8E=E5=8C=BA=E5=9F=9F?= =?UTF-8?q?=E9=BC=A0=E6=A0=87=E9=80=8F=E7=A9=BF=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E5=8F=8ALinux=E7=9A=84=E5=9F=BA=E6=9C=AC=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E5=8C=BA=E5=9F=9F=E8=AF=86=E5=88=AB=E5=8A=9F=E8=83=BD?= =?UTF-8?q?(=E5=8D=B3=E9=9D=9E=E5=AE=8C=E7=BE=8E=E9=80=8F=E4=BC=A0)=202.?= =?UTF-8?q?=20=E6=9B=B4=E6=96=B0=E4=BA=86LICENSE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Live2D/Src/LAppLive2D/Inc/LAppModel.hpp | 21 ++++ .../Live2D/Src/LAppLive2D/Src/LAppModel.cpp | 70 ++++++++++++++ .../Live2D/Src/LAppLive2D/Src/LAppView.cpp | 34 +++---- LICENSE | 17 +++- README.md | 26 ++++- src/Core/Inc/GLCore.h | 7 +- src/Core/Src/GLCore.cpp | 96 +++++++++++++++---- src/Setting/Src/ModelPage.cpp | 1 + 8 files changed, 226 insertions(+), 46 deletions(-) diff --git a/3rdparty/Live2D/Src/LAppLive2D/Inc/LAppModel.hpp b/3rdparty/Live2D/Src/LAppLive2D/Inc/LAppModel.hpp index 86b21f6..597de01 100644 --- a/3rdparty/Live2D/Src/LAppLive2D/Inc/LAppModel.hpp +++ b/3rdparty/Live2D/Src/LAppLive2D/Inc/LAppModel.hpp @@ -80,7 +80,19 @@ public: */ void StopLipSync(); + /** + * @brief 获取模型是否有命中区域定义 + * @return 是否有命中区域 + */ + [[nodiscard]] bool HasHitAreas() const; + /** + * @brief 检测点是否在模型的任何可见部分上 + * @param x 视图坐标X + * @param y 视图坐标Y + * @return 是否命中 + */ + [[nodiscard]] bool IsPointOnModel(Csm::csmFloat32 x, Csm::csmFloat32 y); /** * @brief model3.jsonが置かれたディレクトリとファイルパスからモデルを生成する \n @@ -177,6 +189,15 @@ protected: */ void DoDraw(); +private: + /** + * @brief 使用Drawable检测(当没有命中区域时使用) + * @param x 视图坐标X + * @param y 视图坐标Y + * @return 是否命中 + */ + [[nodiscard]] bool IsPointOnDrawable(Csm::csmFloat32 x, Csm::csmFloat32 y); + private: /** * @brief model3.jsonからモデルを生成する。
diff --git a/3rdparty/Live2D/Src/LAppLive2D/Src/LAppModel.cpp b/3rdparty/Live2D/Src/LAppLive2D/Src/LAppModel.cpp index 045bb8d..88d8a02 100644 --- a/3rdparty/Live2D/Src/LAppLive2D/Src/LAppModel.cpp +++ b/3rdparty/Live2D/Src/LAppLive2D/Src/LAppModel.cpp @@ -82,6 +82,76 @@ LAppModel::~LAppModel() } delete(_modelSetting); } + +bool LAppModel::HasHitAreas() const { + return _modelSetting && _modelSetting->GetHitAreasCount() > 0; +} + +bool LAppModel::IsPointOnModel(const csmFloat32 x, const csmFloat32 y) +{ + if (_model == nullptr || _opacity < 0.1f) + { + return false; + } + // 如果有命中区域,使用HitTest(是否存在命中区域,这取决于模型是否定义这两部分信息) + /* 通常是这样的信息,在.model3.json当中 + "HitAreas": [ + { + "Id": "HitAreaHead", + "Name": "Head" + }, + { + "Id": "HitAreaBody", + "Name": "Body" + } + ] + */ + if (HasHitAreas()) + { + // 检查常用命中区域 + const bool hit = HitTest(LAppDefine::HitAreaNameHead, x, y) || + HitTest(LAppDefine::HitAreaNameBody, x, y); + if (_debugMode && hit) + { + LAppPal::PrintLogLn("[APP]Hit model via HitArea at (%.2f, %.2f)", x, y); + } + return hit; + } + // 如果没有命中区域,使用Drawable检测 + return IsPointOnDrawable(x, y); +} + +bool LAppModel::IsPointOnDrawable(const csmFloat32 x, const csmFloat32 y) +{ + if (_model == nullptr || _opacity < 0.01f) // 接近完全透明 + { + return false; + } + // 获取所有Drawable的数量 + const csmInt32 drawableCount = _model->GetDrawableCount(); + // 遍历所有Drawable + for (csmInt32 i = 0; i < drawableCount; ++i) + { + // 获取Drawable的ID + const CubismIdHandle drawableId = _model->GetDrawableId(i); + // 检查Drawable是否可见 + if (_model->GetDrawableDynamicFlagIsVisible(i)) + { + // 使用CubismUserModel提供的IsHit函数检测 + if (IsHit(drawableId, x, y)) + { + if (_debugMode) + { + const csmChar* drawableName = _model->GetDrawableId(i)->GetString().GetRawString(); + LAppPal::PrintLogLn("[APP]Hit drawable: %s at (%.2f, %.2f)", + drawableName, x, y); + } + return true; + } + } + } + return false; +} void LAppModel::LoadAssets(const csmChar* dir, const csmChar* fileName) { diff --git a/3rdparty/Live2D/Src/LAppLive2D/Src/LAppView.cpp b/3rdparty/Live2D/Src/LAppLive2D/Src/LAppView.cpp index 54b4d6e..b221c2c 100644 --- a/3rdparty/Live2D/Src/LAppLive2D/Src/LAppView.cpp +++ b/3rdparty/Live2D/Src/LAppLive2D/Src/LAppView.cpp @@ -269,35 +269,23 @@ float LAppView::TransformScreenY(float deviceY) const #include "Id/CubismIdManager.hpp" bool LAppView::IsModelHit(const float deviceX, const float deviceY) const { const LAppLive2DManager* live2DManager = LAppLive2DManager::GetInstance(); - if (!live2DManager || live2DManager->GetModelNum() == 0) { + if (!live2DManager || live2DManager->GetModelNum() == 0) + { return false; } - // 坐标转换(设备坐标 -> Live2D View 坐标) - const csmFloat32 viewX = _deviceToScreen->TransformX(deviceX); - const csmFloat32 viewY = _deviceToScreen->TransformY(deviceY); + // 转换到视图坐标 + const csmFloat32 viewX = TransformViewX(deviceX); + const csmFloat32 viewY = TransformViewY(deviceY); - // 正确获取 ID Handle(在 SDK 初始化后调用) - static const Csm::CubismId* bodyId = nullptr; - static const Csm::CubismId* headId = nullptr; - if (!bodyId) { - bodyId = CubismFramework::GetIdManager()->GetId("Body"); - headId = CubismFramework::GetIdManager()->GetId("Head"); + // 遍历所有模型 + LAppModel* model = live2DManager->GetModel(0); + if (model && model->IsPointOnModel(viewX, viewY)) // 调用二次封装的IsPointOnModel函数 + { + return true; } - // 遍历所有模型进行碰撞检测 - const csmUint32 modelCount = live2DManager->GetModelNum(); - for (csmUint32 i = 0; i < modelCount; ++i) { - LAppModel* model = live2DManager->GetModel(i); - if (!model || !model->GetModel()) continue; - - // 检查命中区域(修正:使用 SDK 提供的 ID) - if (model->IsHit(bodyId, viewX, viewY) || model->IsHit(headId, viewX, viewY)) { - return true; // 击中模型 - } - } - - return false; // 未击中模型 + return false; } void LAppView::PreModelDraw(LAppModel& refModel) diff --git a/LICENSE b/LICENSE index 1e3557a..bd1f566 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,21 @@ -MIT License Copyright (c) 2025 Misakiotoha +This application contains the Live2D Cubism SDK developed by Live2D Inc. +of which the copyrights are held by Live2D Inc. +If this application is utilized as a primary element of a business* and its annual sales made directly or indirectly by this application exceed 20 million JPY, +you shall be obliged to execute a Publication License Agreement with Live2D Inc. +and pay required license fees. You shall also be obliged to immediately notify Live2D Inc. +when the annual sales exceed 20 million JPY. +Any violation of these obligations means the utilization of Live2D Cubism SDK beyond the scope permitted by Live2D Inc. +with regard to this application and the infringement of intellectual property rights of Live2D Inc., +and you may receive legal claims from Live2D Inc. + +*"Utilizing this application as a primary element of a business" includes but is not limited to operating a VTuber streaming business. +It does not include streaming video contents for sales promotion. + + + +MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c446c26..55bf4e4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![GitHub issues](https://img.shields.io/github/issues/Misakityan/Yosuga) ![GitHub stars](https://img.shields.io/github/stars/Misakityan/Yosuga?style=social) -本项目使用CMake构建,基于C++Qt6以及Live2D官方SDK实现桌面宠物 +本项目使用CMake构建,基于C++Qt6.6.3以及Live2D官方SDK(CubismSdkForNative-5-r.4.1)实现Live2D桌面宠物 (本项目由Yosuga[Qt5] 发展更新而来,项目架构与代码都有所不同,最显著的特点是本项目支持多平台) 环境为: @@ -37,7 +37,7 @@ make ``` 注意:本项目只是Yosuga的客户端部分,完整的还包括服务端 -- 服务端项目地址: +- 服务端项目地址见: 当前支持平台(已测试过的): - Windows: Windows 10 @@ -46,3 +46,25 @@ make 相关教程见BiliBili:https://www.bilibili.com/video/BV1TtkHYpEDA/?spm_id_from=333.1387.homepage.video_card.click&vd_source=d66e155c7b27c10078bc67965ea1989e 实现效果: + + +关于授权
+本项目采用多重授权结构: + +1. 原创代码部分:MIT License + src/*.* + +2. 依赖库: + - Qt 6.6.3:LGPLv3 License + (提供源码获取方式:https://www.qt.io/download) + Qt部分采用动态链接方式 + - Live2D Cubism SDK:Live2D Proprietary Software License + (需遵守销售额限制及授权协议) + Live2D部分采用静态链接 + 动态链接方式 + 本应用程序包含由Live2D Inc.开发的Live2D Cubism SDK,其版权由Live2D Inc.持有。 + 如果本应用程序被用作业务的主要元素*,并且其直接或间接产生的年销售额超过2000万日元,您需与Live2D Inc.签订单独的出版许可协议并支付许可费。 + 此外,当您的年销售额超过2000万日元时,请您尽快与我们联系。 + 请注意,如果您违反该条款,您将超出本应用程序允许的使用范围,这会造成对Live2D Inc.的知识产权侵犯,可能会导致公司的法律索赔。 + *本应用程序作为业务的主要元素使用时,包括但不限于虚拟主播的直播业务。 + 这不包括应用软件用于发布产品宣传视频的情况。 +3. 整体项目:受上述所有许可证约束 \ No newline at end of file diff --git a/src/Core/Inc/GLCore.h b/src/Core/Inc/GLCore.h index 947a29e..cec516b 100644 --- a/src/Core/Inc/GLCore.h +++ b/src/Core/Inc/GLCore.h @@ -80,5 +80,10 @@ private: bool isLeftPressed; /// 鼠标左键是否按下 bool isRightPressed; /// 鼠标右键是否按下 QPoint currentPos; /// 当前鼠标位置 - +#ifdef Q_OS_WIN +#include +private: + HWND hwnd; // Windows窗口句柄 + void setWindowTransparentForMouse(bool transparent); +#endif }; \ No newline at end of file diff --git a/src/Core/Src/GLCore.cpp b/src/Core/Src/GLCore.cpp index 94a6912..dd025c7 100644 --- a/src/Core/Src/GLCore.cpp +++ b/src/Core/Src/GLCore.cpp @@ -48,7 +48,6 @@ GLCore::GLCore(int w, int h, QWidget *parent) QFontDatabase::addApplicationFont("Resources/Font/ElaAwesome.ttf"); QApplication::setFont(QFont("Microsoft YaHei", 13)); - // new一些必要的对象 contextMenu = new Menu(this); @@ -74,6 +73,10 @@ GLCore::GLCore(int w, int h, QWidget *parent) }); frameTimer->start(static_cast((1.0 / frameRate) * 1000)); // 使用成员变量计算间隔 +#ifdef Q_OS_WIN + // 保存窗口句柄 + hwnd = reinterpret_cast(this->winId()); +#endif // 启用鼠标跟踪,不启用的话鼠标按下才会回调mouseMoveEvent函数 this->setMouseTracking(true); @@ -160,14 +163,37 @@ void GLCore::closeEvent(QCloseEvent* event) event->accept(); // 确保关闭事件被接受 } +#ifdef Q_OS_WIN +void GLCore::setWindowTransparentForMouse(bool transparent) +{ + if (!hwnd) return; + + LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + + if (transparent) { + // 启用鼠标穿透 + exStyle |= WS_EX_TRANSPARENT; + exStyle |= WS_EX_LAYERED; + } else { + // 禁用鼠标穿透 + exStyle &= ~WS_EX_TRANSPARENT; + exStyle &= ~WS_EX_LAYERED; + } + + SetWindowLong(hwnd, GWL_EXSTYLE, exStyle); + // 刷新窗口 + SetWindowPos(hwnd, 0, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); +} +#endif + void GLCore::mouseMoveEvent(QMouseEvent* event) { - LAppDelegate::GetInstance()->GetView()->OnTouchesMoved( - static_cast(event->position().x()), - static_cast(event->position().y()) - ); + const float x = static_cast(event->position().x()); + const float y = static_cast(event->position().y()); + LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(x, y); // 将当前鼠标位置传递给LAppDelegate - if (isLeftPressed) { + if (isLeftPressed) { // 鼠标左键按下 const QPoint newPos = event->globalPos() - currentPos; this->move(newPos); } @@ -175,32 +201,65 @@ void GLCore::mouseMoveEvent(QMouseEvent* event) void GLCore::mousePressEvent(QMouseEvent* event) { - LAppDelegate::GetInstance()->GetView()->OnTouchesBegan( - static_cast(event->position().x()), - static_cast(event->position().y()) - ); + const float x = static_cast(event->position().x()); + const float y = static_cast(event->position().y()); + // 检测是否在模型上 + bool onModel = false; + if (LAppDelegate::GetInstance() && LAppDelegate::GetInstance()->GetView()) { + onModel = LAppDelegate::GetInstance()->GetView()->IsModelHit(x, y); + } if (event->button() == Qt::LeftButton) { - this->isLeftPressed = true; + LAppDelegate::GetInstance()->GetView()->OnTouchesBegan(x, y); this->currentPos = event->globalPos() - this->frameGeometry().topLeft(); + if (onModel) { + // 窗口拖动 + this->isLeftPressed = true; +#ifdef Q_OS_WIN + // 确保窗口不穿透 + setWindowTransparentForMouse(false); +#endif + } else { + // 透明区域:透传(只有WIndows完美实现了,Linux由于平台差异,只是简单实现,并没有完美透传功能) +#ifdef Q_OS_WIN + // 设置窗口为鼠标穿透 + setWindowTransparentForMouse(true); + + // 发送鼠标按下事件到底层窗口 + POINT pt = { event->globalPos().x(), event->globalPos().y() }; + HWND hWndBelow = WindowFromPoint(pt); + if (hWndBelow && hWndBelow != hwnd) { + // 转换坐标 + ScreenToClient(hWndBelow, &pt); + + // 发送鼠标按下消息 + PostMessage(hWndBelow, WM_LBUTTONDOWN, + MK_LBUTTON, MAKELPARAM(pt.x, pt.y)); + PostMessage(hWndBelow, WM_LBUTTONUP, + 0, MAKELPARAM(pt.x, pt.y)); + } + + // 恢复窗口不穿透状态(下一次鼠标移动时会重新检测) + QTimer::singleShot(100, this, [this]() { + setWindowTransparentForMouse(false); + }); +#endif + this->isLeftPressed = false; + } } // TODO: 右键菜单等 if (event->button() == Qt::RightButton) { - // 在鼠标右键点击的位置创建菜单,显示自定义右键菜单 contextMenu->showMenu(event->globalPos()); this->isRightPressed = true; } - - } void GLCore::mouseReleaseEvent(QMouseEvent* event) { - LAppDelegate::GetInstance()->GetView()->OnTouchesEnded( - static_cast(event->position().x()), - static_cast(event->position().y()) - ); + const float x = static_cast(event->position().x()); + const float y = static_cast(event->position().y()); + LAppDelegate::GetInstance()->GetView()->OnTouchesEnded(x, y); if (event->button() == Qt::LeftButton) { isLeftPressed = false; @@ -208,7 +267,6 @@ void GLCore::mouseReleaseEvent(QMouseEvent* event) if (event->button() == Qt::RightButton) { isRightPressed = false; } - } void GLCore::initializeGL() diff --git a/src/Setting/Src/ModelPage.cpp b/src/Setting/Src/ModelPage.cpp index a861121..0f0fade 100644 --- a/src/Setting/Src/ModelPage.cpp +++ b/src/Setting/Src/ModelPage.cpp @@ -43,6 +43,7 @@ ModelPage::ModelPage(QWidget *parent) // 创建滑块控件和标签 ElaText *modelSizeText = new ElaText("模型大小比例设置", this); + modelSizeText->setToolTip("实时调整模型大小"); modelSizeText->setTextPixelSize(15); modelSlider = new ElaSlider(this); // 滑块(用于设置模型实时大小) modelSlider->setRange(0, 99); // 设置范围