diff --git a/3rdparty/Live2D/Src/LAppLive2D/Inc/LAppLive2DManager.hpp b/3rdparty/Live2D/Src/LAppLive2D/Inc/LAppLive2DManager.hpp index 558f583..8f89cf9 100644 --- a/3rdparty/Live2D/Src/LAppLive2D/Inc/LAppLive2DManager.hpp +++ b/3rdparty/Live2D/Src/LAppLive2D/Inc/LAppLive2DManager.hpp @@ -107,8 +107,15 @@ public: * 至于为什么要这样拆分,只是为了适应底层的模型加载函数
* 你可以选择再上层封装,将传入的路径拆分为路径和文件名,然后调用本函数即可
*/ + [[deprecated("如需使用,请在单线程中使用本函数")]] void LoadModelFromPath(const std::string& modelPath, const std::string& fileName); + // 仅在内存中加载模型,不干扰当前运行状态(供子线程调用) + LAppModel* LoadModelInstance(const std::string& modelPath, const std::string& fileName); + + // 将已经加载好的模型应用到系统中(供主线程调用) + void MountLoadedModel(LAppModel* model); + /** * @brief 次のシーンに切り替える
* サンプルアプリケーションではモデルセットの切り替えを行う。 diff --git a/3rdparty/Live2D/Src/LAppLive2D/Src/LAppLive2DManager.cpp b/3rdparty/Live2D/Src/LAppLive2D/Src/LAppLive2DManager.cpp index 8c61d6f..762b648 100644 --- a/3rdparty/Live2D/Src/LAppLive2D/Src/LAppLive2DManager.cpp +++ b/3rdparty/Live2D/Src/LAppLive2D/Src/LAppLive2DManager.cpp @@ -6,8 +6,8 @@ */ #include "LAppLive2DManager.hpp" -#include -#include +#include +#include #if defined(_WIN32) #include // 保持 Windows 下继续用旧实现 #include @@ -265,8 +265,8 @@ void LAppLive2DManager::OnUpdate() const #include void LAppLive2DManager::LoadModelFromPath(const std::string& modelPath, const std::string& fileName) { - csmString modelPathStr(modelPath.c_str()); - csmString modelJsonName(fileName.c_str()); + const csmString modelPathStr(modelPath.c_str()); + const csmString modelJsonName(fileName.c_str()); ReleaseAllModel(); // 释放当前所有模型 有一个点要注意,在这里先释放然后再Push模型实例,以及加载模型实例 _models.PushBack(new LAppModel()); // 这样在加载的时候都使用的models[0]这一个位置,自行实现模型选择器要注意注意 @@ -275,7 +275,7 @@ void LAppLive2DManager::LoadModelFromPath(const std::string& modelPath, const st // 加载完后根据模型大小来重新设置当前窗口大小 const int width = static_cast(_models[0]->GetModel()->GetCanvasWidthPixel() / 15.0); const int height = static_cast(_models[0]->GetModel()->GetCanvasHeightPixel() / 15.0); - AppContext::GetGLCore()->setWindowSize(width, height); + AppContext::GetGLCore()->setWindowSize(width, height); // 获取GLCore上下文 LAppPal::PrintLogLn("[APP]窗口尺寸重新设置为: W: %d H: %d", width, height); /* * 提供一个半透明表示模型的示例。 @@ -309,6 +309,70 @@ void LAppLive2DManager::LoadModelFromPath(const std::string& modelPath, const st } } +// 纯加载函数 将在子线程运行 +LAppModel* LAppLive2DManager::LoadModelInstance(const std::string& modelPath, const std::string& fileName) +{ + const Csm::csmString modelPathStr(modelPath.c_str()); + const Csm::csmString modelJsonName(fileName.c_str()); + + // 创建新实例 + auto* instance = new LAppModel(); + + // 加载资源 (耗时巨头:IO + OpenGL上传) + // 注意:这里需要当前线程已经绑定了有效的 OpenGL Context + instance->LoadAssets(modelPathStr.GetRawString(), modelJsonName.GetRawString()); + + return instance; +} +// 模型挂载函数 运行在主线程 +void LAppLive2DManager::MountLoadedModel(LAppModel* model) +{ + if (!model) return; // 模型为空 + // 释放旧模型 + ReleaseAllModel(); + // 加入新模型 + _models.PushBack(model); + + // 加载完后根据模型大小来重新设置当前窗口大小 + const int width = static_cast(_models[0]->GetModel()->GetCanvasWidthPixel() / 15.0); + const int height = static_cast(_models[0]->GetModel()->GetCanvasHeightPixel() / 15.0); + + // 确保在主线程调用 UI 相关操作 + if(AppContext::GetGLCore()) { + AppContext::GetGLCore()->setWindowSize(width, height); + } + LAppPal::PrintLogLn("[APP]窗口尺寸重新设置为: W: %d H: %d", width, height); + + // 设置渲染目标等 + { + /* + * 提供一个半透明表示模型的示例。 + * 如果定义了USE_RENDER_TARGET或USE_MODEL_RENDER_TARGET, + * 则将模型绘制到另一个渲染目标上,并将绘制结果作为纹理应用到另一个精灵上。 + */ +#if defined(USE_RENDER_TARGET) + // 如果选择将绘制操作在LAppView持有的目标上进行,则选择此选项 + LAppView::SelectTarget useRenderTarget = LAppView::SelectTarget_ViewFrameBuffer; +#elif defined(USE_MODEL_RENDER_TARGET) + // 如果选择将绘制操作在各个LAppModel持有的目标上进行,则选择此选项 + LAppView::SelectTarget useRenderTarget = LAppView::SelectTarget_ModelFrameBuffer; +#else + // 默认情况下,渲染到主帧缓冲区(通常是直接渲染到屏幕) + LAppView::SelectTarget useRenderTarget = LAppView::SelectTarget_None; +#endif +#if defined(USE_RENDER_TARGET) || defined(USE_MODEL_RENDER_TARGET) + // 作为给模型单独设置α(透明度)的示例,创建另一个模型实例并稍微移动其位置 + _models.PushBack(new LAppModel()); + _models[1]->LoadAssets(modelPath.GetRawString(), modelJsonName.GetRawString()); + _models[1]->GetModelMatrix()->TranslateX(0.2f); +#endif + LAppDelegate::GetInstance()->GetView()->SwitchRenderingTarget(useRenderTarget); + // 当选择其他渲染目标时的背景清除颜色 + constexpr float clearColor[3] = { 1.0f, 1.0f, 1.0f }; + LAppDelegate::GetInstance()->GetView()->SetRenderTargetClearColor(clearColor[0], clearColor[1], clearColor[2]); + } +} + void LAppLive2DManager::NextScene() { diff --git a/CMakeLists.txt b/CMakeLists.txt index c374c2e..883e444 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.30) project(Yosuga VERSION 1.0) set(CMAKE_CXX_STANDARD 20) @@ -51,6 +51,7 @@ find_package(Qt6 COMPONENTS WebSockets Multimedia OpenGLWidgets + Concurrent REQUIRED) find_package(OpenGL REQUIRED) @@ -172,6 +173,7 @@ target_link_libraries(${PROJECT_NAME} Qt::WebSockets Qt::Multimedia Qt::OpenGLWidgets + Qt::Concurrent ) # 添加头文件 diff --git a/main.cpp b/main.cpp index 8ab92e0..03d6271 100644 --- a/main.cpp +++ b/main.cpp @@ -10,6 +10,7 @@ int main(int argc, char *argv[]) { + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); // 允许多个窗口使用同一个OpenGL上下文(用于实现Live2D模型的异步加载) QApplication a(argc, argv); eApp->init(); // 设置云母效果图片 diff --git a/src/Core/Src/GLCore.cpp b/src/Core/Src/GLCore.cpp index 870f1f1..087d9ad 100644 --- a/src/Core/Src/GLCore.cpp +++ b/src/Core/Src/GLCore.cpp @@ -239,10 +239,8 @@ void GLCore::setWindowSize(const int w, const int h) if (this->width() == w && this->height() == h) { return; } - // 调用 QWidget::resize 或 setFixedSize 来改变窗口的实际尺寸 setFixedSize(w, h); - // 调用 setFixedSize 会自动触发 QOpenGLWidget 的 resizeEvent, // 进而调用 resizeGL(w, h),无需手动调用 resizeGL } \ No newline at end of file diff --git a/src/Setting/Inc/ModelPage.h b/src/Setting/Inc/ModelPage.h index 01561ca..7c585b3 100644 --- a/src/Setting/Inc/ModelPage.h +++ b/src/Setting/Inc/ModelPage.h @@ -6,9 +6,7 @@ * @brief 模型页面 * 暂时只做最简单功能切换模型 */ - -#ifndef YOSUGA_MODELPAGE_H -#define YOSUGA_MODELPAGE_H +#pragma once #include "BasePage.h" #include "ElaPushButton.h" @@ -20,7 +18,7 @@ class ElaLineEdit; class ElaPushButton; -class ModelPage : public BasePage +class ModelPage final : public BasePage { Q_OBJECT public: @@ -28,7 +26,7 @@ public: std::pair splitPath(const QString& fullPath); - ~ModelPage(); + ~ModelPage() override; private: @@ -39,12 +37,4 @@ private: QUrl modelFileUrl; QString modelFilePathFirst; QString modelFilePathSecond; - - - }; - - - - -#endif //YOSUGA_MODELPAGE_H diff --git a/src/Setting/Src/HomePage.cpp b/src/Setting/Src/HomePage.cpp index 4fffed3..6ba5cf0 100644 --- a/src/Setting/Src/HomePage.cpp +++ b/src/Setting/Src/HomePage.cpp @@ -52,7 +52,7 @@ HomePage::HomePage(QWidget* parent) urlCard1->setTitlePixelSize(17); urlCard1->setTitleSpacing(25); urlCard1->setSubTitleSpacing(13); - urlCard1->setUrl("https://github.com/Misakiotoha/Yosuga"); + urlCard1->setUrl("https://github.com/Misakityan/Yosuga"); urlCard1->setCardPixmap(QPixmap("Resources/Pic/Others/img.png")); urlCard1->setTitle("Yosuga Github"); urlCard1->setSubTitle("Star++!"); @@ -110,10 +110,10 @@ HomePage::HomePage(QWidget* parent) Q_EMIT modelShopNavigation(); }); ModeShopCard->setCardPixmap(QPixmap("Resources/Pic/Others/Live2D.png")); - ModeShopCard->setTitle("模型商店"); + ModeShopCard->setTitle("模型设置"); ModeShopCard->setSubTitle("属于你的Live2D模型"); ModeShopCard->setInteractiveTips("By Misaki"); - ModeShopCard->setDetailedText("选择你喜欢的Live2D模型,模型来自多个作者,多个平台,有免费也有收费的"); + ModeShopCard->setDetailedText("选择你喜欢的Live2D模型"); // 音频设置卡片 ElaPopularCard* AudioSettingCard = new ElaPopularCard(this); connect(AudioSettingCard, &ElaPopularCard::popularCardButtonClicked, this, [=, this]() { @@ -144,10 +144,6 @@ HomePage::HomePage(QWidget* parent) centerVLayout->addLayout(flowLayout); centerVLayout->addStretch(); addCentralWidget(centralWidget); - - // 初始化提示 - ElaMessageBar::success(ElaMessageBarType::BottomRight, "Success", "初始化成功!", 2000); - qDebug() << "初始化成功"; } HomePage::~HomePage() diff --git a/src/Setting/Src/ModelPage.cpp b/src/Setting/Src/ModelPage.cpp index 79a93a0..52bd120 100644 --- a/src/Setting/Src/ModelPage.cpp +++ b/src/Setting/Src/ModelPage.cpp @@ -3,21 +3,19 @@ // #include "ModelPage.h" -#include #include -#include #include -#include -#include "ElaComboBox.h" #include "ElaMessageBar.h" #include "ElaScrollPageArea.h" #include "ElaText.h" - +#include +#include +#include +#include #include "LAppLive2DManager.hpp" -ModelPage::ModelPage(QWidget* parent) - : BasePage(parent) -{ +ModelPage::ModelPage(QWidget *parent) + : BasePage(parent) { // 预览窗口标题 setWindowTitle("ModelPage"); @@ -29,9 +27,9 @@ ModelPage::ModelPage(QWidget* parent) modelChoosePushButton->setToolTip("选择.model3.json结尾的文件"); modelUsePushButton = new ElaPushButton("使用模型", this); modelUsePushButton->setToolTip("使用选择的模型或Url对应的模型"); - ElaScrollPageArea* modelSetArea = new ElaScrollPageArea(this); - QHBoxLayout* modelSetLayout = new QHBoxLayout(modelSetArea); - ElaText* modelSetText = new ElaText("模型设置", this); + ElaScrollPageArea *modelSetArea = new ElaScrollPageArea(this); + QHBoxLayout *modelSetLayout = new QHBoxLayout(modelSetArea); + ElaText *modelSetText = new ElaText("模型设置", this); modelSetText->setTextPixelSize(15); modelSetLayout->addWidget(modelSetText); modelSetLayout->addWidget(modelUrlEdit); @@ -40,54 +38,116 @@ ModelPage::ModelPage(QWidget* parent) modelSetLayout->addWidget(modelUsePushButton); modelSetLayout->addSpacing(10); connect(modelChoosePushButton, &ElaPushButton::clicked, this, [this]() { - // 获取当前exe所在目录的本地路径 - QString exeDir = QCoreApplication::applicationDirPath(); - // 转换为QUrl格式(自动处理路径分隔符) - QUrl initialDir = QUrl::fromLocalFile(exeDir); + // 创建对话框对象(使用 heap 分配,由 Qt 对象树管理内存) + auto *fileDialog = new QFileDialog(this); + fileDialog->setWindowTitle("选择模型文件"); + fileDialog->setNameFilter("Live2D Model (*.model3.json)"); - // 打开文件选择对话框 - modelFileUrl = QFileDialog::getOpenFileUrl( - this, - "选择模型文件", - initialDir, // 初始目录为当前目录 - "*.model3.json"); + // 设置初始目录 + const QString exeDir = QCoreApplication::applicationDirPath(); // 获取当前exe所在目录的本地路径 + fileDialog->setDirectory(exeDir); // 设置初始目录为当前 exe 所在目录 - // 检查url是否有效 - if(!modelFileUrl.isEmpty()){ - QString t = modelFileUrl.toLocalFile(); - std::pair path = this->splitPath(t); - this->modelFilePathFirst = path.first; - this->modelFilePathSecond = path.second; - this->modelUrlEdit->setText(t); - } - else{ - ElaMessageBar::information(ElaMessageBarType::BottomRight, "模型设置", "似乎并没有选择模型", 800.0, this); - } + // 连接信号:当用户选中文件并点击打开时 + connect(fileDialog, &QFileDialog::fileSelected, this, [this, fileDialog](const QString &file) { + modelFileUrl = QUrl::fromLocalFile(file); + if (!modelFileUrl.isEmpty()) { + const QString t = modelFileUrl.toLocalFile(); + const std::pair path = this->splitPath(t); + this->modelFilePathFirst = path.first; + this->modelFilePathSecond = path.second; + this->modelUrlEdit->setText(t); + ElaMessageBar::success(ElaMessageBarType::BottomRight, "模型设置", "模型选择成功", 2000, this); + } + // 用完即弃,自动清理内存 + fileDialog->deleteLater(); + }); + // 处理取消的情况(防止内存泄漏) + connect(fileDialog, &QFileDialog::rejected, fileDialog, &QObject::deleteLater); + // 显示对话框(非阻塞,不会卡住主界面) + fileDialog->open(); }); connect(modelUsePushButton, &ElaPushButton::clicked, this, [this]() { - if(!modelFileUrl.isEmpty()){ - LAppLive2DManager::GetInstance()->LoadModelFromPath(this->modelFilePathFirst.toStdString(), this->modelFilePathSecond.toStdString()); - } - else{ + // 模型使用 + if (modelFileUrl.isEmpty()) { ElaMessageBar::information(ElaMessageBarType::BottomRight, "模型设置", "似乎并没有选择模型", 800.0, this); + return; } + // UI 状态设置为加载中 + modelUsePushButton->setEnabled(false); // 禁用使用按钮 + modelUsePushButton->setText("加载中"); // 修改按钮文本 + modelChoosePushButton->setEnabled(false); // 禁用选择按钮 + + // 获取路径字符串 (必须按值传递给lambda) + std::string dir = this->modelFilePathFirst.toStdString(); + std::string filename = this->modelFilePathSecond.toStdString(); + + // 启动异步任务 + QFuture future = QtConcurrent::run([dir, filename]() -> LAppModel * { + // 以下代码在子线程执行 + // 创建临时 OpenGL 上下文 + auto *context = new QOpenGLContext(); + // 关键点:设置与全局共享上下文共享 (这样主线程才能看到纹理) + context->setShareContext(QOpenGLContext::globalShareContext()); + if (!context->create()) { + delete context; + return nullptr; + } + // 创建离屏表面 (因为子线程没有窗口,需要一个假的绘制表面) + auto *surface = new QOffscreenSurface(); + surface->setFormat(context->format()); + surface->create(); + // 绑定上下文 + if (!context->makeCurrent(surface)) { + delete surface; + delete context; + return nullptr; + } + // 执行真正的耗时加载 + // 调用我们在 Manager 里新写的函数 + LAppModel *model = LAppLive2DManager::GetInstance()->LoadModelInstance(dir, filename); + // 清理子线程资源 + context->doneCurrent(); + delete surface; + delete context; + + return model; + }); + + // 监控任务结束 + auto *watcher = new QFutureWatcher(); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher]() { + // 下面的代码在主线程中执行 + LAppModel *newModel = watcher->result(); + if (newModel) { + // 调用挂载函数,瞬间完成切换 + LAppLive2DManager::GetInstance()->MountLoadedModel(newModel); + ElaMessageBar::success(ElaMessageBarType::BottomRight, "成功", "模型加载完成", 2000, this); + } else { + ElaMessageBar::error(ElaMessageBarType::BottomRight, "错误", "模型加载失败 (OpenGL环境异常)", 2000, this); + } + // 恢复 UI + modelUsePushButton->setEnabled(true); + modelUsePushButton->setText("使用模型"); + modelChoosePushButton->setEnabled(true); + watcher->deleteLater(); + }); + // 开始监控 + watcher->setFuture(future); }); - QWidget* centralWidget = new QWidget(this); - centralWidget->setWindowTitle("模型商店"); - QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget); + QWidget *centralWidget = new QWidget(this); + centralWidget->setWindowTitle("模型设置"); + QVBoxLayout *centerLayout = new QVBoxLayout(centralWidget); centerLayout->addWidget(modelSetArea); centerLayout->addStretch(); centerLayout->setContentsMargins(0, 0, 0, 0); addCentralWidget(centralWidget, true, true, 0); - } // 返回 pair<目录路径, 文件名> -std::pair ModelPage::splitPath(const QString& fullPath) -{ - QFileInfo fileInfo(fullPath); +std::pair ModelPage::splitPath(const QString &fullPath) { + const QFileInfo fileInfo(fullPath); // 获取目录部分(自动处理末尾斜杠) QString dirPath = fileInfo.dir().absolutePath() + "/"; @@ -98,7 +158,5 @@ std::pair ModelPage::splitPath(const QString& fullPath) return {dirPath, fileName}; } -ModelPage::~ModelPage() -{ - -} \ No newline at end of file +ModelPage::~ModelPage() { +} diff --git a/src/Setting/Src/Setting.cpp b/src/Setting/Src/Setting.cpp index dc7c792..dc331f2 100644 --- a/src/Setting/Src/Setting.cpp +++ b/src/Setting/Src/Setting.cpp @@ -79,7 +79,7 @@ void Setting::initNavigationBar() addPageNode("主页", homePage, ElaIconType::House); // 添加模型商店节点 - addPageNode("模型商店", modelPage, ElaIconType::Shop); + addPageNode("模型设置", modelPage, ElaIconType::Shop); // 添加网络连接设置节点 addPageNode("连接设置", networkPage, ElaIconType::NetworkWired);