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);