1. 新增模型区域点击接口,同时为Windows添加了完美的透明区域鼠标透穿功能,以及Linux的基本点击区域识别功能(即非完美透传)

2. 更新了LICENSE
This commit is contained in:
Misaki
2025-12-25 17:35:12 +08:00
parent e9a9d483db
commit 702b083e47
8 changed files with 226 additions and 46 deletions
+21
View File
@@ -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からモデルを生成する。<br>
+70
View File
@@ -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)
{
+11 -23
View File
@@ -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)
+16 -1
View File
@@ -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
+24 -2
View File
@@ -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
相关教程见BiliBilihttps://www.bilibili.com/video/BV1TtkHYpEDA/?spm_id_from=333.1387.homepage.video_card.click&vd_source=d66e155c7b27c10078bc67965ea1989e
实现效果:
关于授权 <br>
本项目采用多重授权结构:
1. 原创代码部分:MIT License
src/*.*
2. 依赖库:
- Qt 6.6.3LGPLv3 License
(提供源码获取方式:https://www.qt.io/download
Qt部分采用动态链接方式
- Live2D Cubism SDKLive2D Proprietary Software License
(需遵守销售额限制及授权协议)
Live2D部分采用静态链接 + 动态链接方式
本应用程序包含由Live2D Inc.开发的Live2D Cubism SDK,其版权由Live2D Inc.持有。
如果本应用程序被用作业务的主要元素*,并且其直接或间接产生的年销售额超过2000万日元,您需与Live2D Inc.签订单独的出版许可协议并支付许可费。
此外,当您的年销售额超过2000万日元时,请您尽快与我们联系。
请注意,如果您违反该条款,您将超出本应用程序允许的使用范围,这会造成对Live2D Inc.的知识产权侵犯,可能会导致公司的法律索赔。
*本应用程序作为业务的主要元素使用时,包括但不限于虚拟主播的直播业务。
这不包括应用软件用于发布产品宣传视频的情况。
3. 整体项目:受上述所有许可证约束
+6 -1
View File
@@ -80,5 +80,10 @@ private:
bool isLeftPressed; /// 鼠标左键是否按下
bool isRightPressed; /// 鼠标右键是否按下
QPoint currentPos; /// 当前鼠标位置
#ifdef Q_OS_WIN
#include <windows.h>
private:
HWND hwnd; // Windows窗口句柄
void setWindowTransparentForMouse(bool transparent);
#endif
};
+77 -19
View File
@@ -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<int>((1.0 / frameRate) * 1000)); // 使用成员变量计算间隔
#ifdef Q_OS_WIN
// 保存窗口句柄
hwnd = reinterpret_cast<HWND>(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<float>(event->position().x()),
static_cast<float>(event->position().y())
);
const float x = static_cast<float>(event->position().x());
const float y = static_cast<float>(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<float>(event->position().x()),
static_cast<float>(event->position().y())
);
const float x = static_cast<float>(event->position().x());
const float y = static_cast<float>(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<float>(event->position().x()),
static_cast<float>(event->position().y())
);
const float x = static_cast<float>(event->position().x());
const float y = static_cast<float>(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()
+1
View File
@@ -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); // 设置范围