diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5255f865b..d1d810374 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,7 +23,7 @@ jobs: run: sudo apt-get -y update - sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev + sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev libqt5opengl5 libqt5opengl5-dev - name: Configure and Build run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index a9d543e83..bc2e77d4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -305,7 +305,7 @@ endif() include(QtVersionlessBackport) if(Launcher_QT_VERSION_MAJOR EQUAL 5) set(QT_VERSION_MAJOR 5) - find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth) + find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth OpenGL) find_package(Qt5 COMPONENTS DBus) list(APPEND Launcher_QT_DBUS Qt5::DBus) @@ -321,7 +321,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth) + find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth OpenGL) find_package(Qt6 COMPONENTS DBus) list(APPEND Launcher_QT_DBUS Qt6::DBus) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a375e0bdf..6714631c3 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -810,6 +810,7 @@ SET(LAUNCHER_SOURCES resources/flat/flat.qrc resources/flat_white/flat_white.qrc resources/documents/documents.qrc + resources/shaders/shaders.qrc ../${Launcher_Branding_LogoQRC} # Icons @@ -1068,6 +1069,13 @@ SET(LAUNCHER_SOURCES ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h + ui/dialogs/skins/draw/SkinOpenGLWindow.h + ui/dialogs/skins/draw/SkinOpenGLWindow.cpp + ui/dialogs/skins/draw/Scene.h + ui/dialogs/skins/draw/Scene.cpp + ui/dialogs/skins/draw/BoxGeometry.h + ui/dialogs/skins/draw/BoxGeometry.cpp + # GUI - widgets ui/widgets/CheckComboBox.cpp ui/widgets/CheckComboBox.h @@ -1239,6 +1247,7 @@ qt_add_resources(LAUNCHER_RESOURCES resources/iOS/iOS.qrc resources/flat/flat.qrc resources/documents/documents.qrc + resources/shaders/shaders.qrc ../${Launcher_Branding_LogoQRC} ) @@ -1289,6 +1298,7 @@ target_link_libraries(Launcher_logic Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::NetworkAuth + Qt${QT_VERSION_MAJOR}::OpenGL ${Launcher_QT_DBUS} ${Launcher_QT_LIBS} ) diff --git a/launcher/main.cpp b/launcher/main.cpp index 35f545f52..c41c510dd 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -84,6 +84,8 @@ int main(int argc, char* argv[]) Q_INIT_RESOURCE(iOS); Q_INIT_RESOURCE(flat); Q_INIT_RESOURCE(flat_white); + + Q_INIT_RESOURCE(shaders); return app.exec(); } case Application::Failed: diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index 017cb8dc2..124b69c85 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -31,7 +31,7 @@ SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QA m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher.reset(new QFileSystemWatcher(this)); - is_watching = false; + m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); directoryChanged(path); @@ -39,12 +39,12 @@ SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QA void SkinList::startWatching() { - if (is_watching) { + if (m_isWatching) { return; } update(); - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) { + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) { qDebug() << "Started watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); @@ -54,11 +54,11 @@ void SkinList::startWatching() void SkinList::stopWatching() { save(); - if (!is_watching) { + if (!m_isWatching) { return; } - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) { + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); @@ -142,7 +142,7 @@ bool SkinList::update() std::sort(newSkins.begin(), newSkins.end(), [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); beginResetModel(); - m_skin_list.swap(newSkins); + m_skinList.swap(newSkins); endResetModel(); if (needsSave) save(); @@ -158,7 +158,7 @@ void SkinList::directoryChanged(const QString& path) if (m_dir.absolutePath() != new_dir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); - if (is_watching) + if (m_isWatching) stopWatching(); startWatching(); } @@ -172,9 +172,9 @@ void SkinList::fileChanged(const QString& path) if (!checkfile.exists()) return; - for (int i = 0; i < m_skin_list.count(); i++) { - if (m_skin_list[i].getPath() == checkfile.absoluteFilePath()) { - m_skin_list[i].refresh(); + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].getPath() == checkfile.absoluteFilePath()) { + m_skinList[i].refresh(); dataChanged(index(i), index(i)); break; } @@ -235,12 +235,17 @@ QVariant SkinList::data(const QModelIndex& index, int role) const int row = index.row(); - if (row < 0 || row >= m_skin_list.size()) + if (row < 0 || row >= m_skinList.size()) return QVariant(); - auto skin = m_skin_list[row]; + auto skin = m_skinList[row]; switch (role) { - case Qt::DecorationRole: - return skin.getTexture(); + case Qt::DecorationRole: { + auto preview = skin.getPreview(); + if (preview.isNull()) { + preview = skin.getTexture(); + } + return preview; + } case Qt::DisplayRole: return skin.name(); case Qt::UserRole: @@ -254,7 +259,7 @@ QVariant SkinList::data(const QModelIndex& index, int role) const int SkinList::rowCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : m_skin_list.size(); + return parent.isValid() ? 0 : m_skinList.size(); } void SkinList::installSkins(const QStringList& iconFiles) @@ -284,8 +289,8 @@ QString SkinList::installSkin(const QString& file, const QString& name) int SkinList::getSkinIndex(const QString& key) const { - for (int i = 0; i < m_skin_list.count(); i++) { - if (m_skin_list[i].name() == key) { + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].name() == key) { return i; } } @@ -297,7 +302,7 @@ const SkinModel* SkinList::skin(const QString& key) const int idx = getSkinIndex(key); if (idx == -1) return nullptr; - return &m_skin_list[idx]; + return &m_skinList[idx]; } SkinModel* SkinList::skin(const QString& key) @@ -305,22 +310,22 @@ SkinModel* SkinList::skin(const QString& key) int idx = getSkinIndex(key); if (idx == -1) return nullptr; - return &m_skin_list[idx]; + return &m_skinList[idx]; } -bool SkinList::deleteSkin(const QString& key, const bool trash) +bool SkinList::deleteSkin(const QString& key, bool trash) { int idx = getSkinIndex(key); if (idx != -1) { - auto s = m_skin_list[idx]; + auto s = m_skinList[idx]; if (trash) { if (FS::trash(s.getPath(), nullptr)) { - m_skin_list.remove(idx); + m_skinList.remove(idx); save(); return true; } } else if (QFile::remove(s.getPath())) { - m_skin_list.remove(idx); + m_skinList.remove(idx); save(); return true; } @@ -332,7 +337,7 @@ void SkinList::save() { QJsonObject doc; QJsonArray arr; - for (auto s : m_skin_list) { + for (auto s : m_skinList) { arr << s.toJSON(); } doc["skins"] = arr; @@ -346,8 +351,8 @@ void SkinList::save() int SkinList::getSelectedAccountSkin() { const auto& skin = m_acct->accountData()->minecraftProfile.skin; - for (int i = 0; i < m_skin_list.count(); i++) { - if (m_skin_list[i].getURL() == skin.url) { + for (int i = 0; i < m_skinList.count(); i++) { + if (m_skinList[i].getURL() == skin.url) { return i; } } @@ -361,9 +366,9 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) } int row = idx.row(); - if (row < 0 || row >= m_skin_list.size()) + if (row < 0 || row >= m_skinList.size()) return false; - auto& skin = m_skin_list[row]; + auto& skin = m_skinList[row]; auto newName = value.toString(); if (skin.name() != newName) { skin.rename(newName); @@ -375,18 +380,18 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) void SkinList::updateSkin(SkinModel* s) { auto done = false; - for (auto i = 0; i < m_skin_list.size(); i++) { - if (m_skin_list[i].getPath() == s->getPath()) { - m_skin_list[i].setCapeId(s->getCapeId()); - m_skin_list[i].setModel(s->getModel()); - m_skin_list[i].setURL(s->getURL()); + for (auto i = 0; i < m_skinList.size(); i++) { + if (m_skinList[i].getPath() == s->getPath()) { + m_skinList[i].setCapeId(s->getCapeId()); + m_skinList[i].setModel(s->getModel()); + m_skinList[i].setURL(s->getURL()); done = true; break; } } if (!done) { - beginInsertRows(QModelIndex(), m_skin_list.count(), m_skin_list.count() + 1); - m_skin_list.append(*s); + beginInsertRows(QModelIndex(), m_skinList.count(), m_skinList.count() + 1); + m_skinList.append(*s); endInsertRows(); } save(); diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h index 66af6a17b..e77269d57 100644 --- a/launcher/minecraft/skins/SkinList.h +++ b/launcher/minecraft/skins/SkinList.h @@ -43,7 +43,7 @@ class SkinList : public QAbstractListModel { virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; - bool deleteSkin(const QString& key, const bool trash); + bool deleteSkin(const QString& key, bool trash); void installSkins(const QStringList& iconFiles); QString installSkin(const QString& file, const QString& name = {}); @@ -73,8 +73,8 @@ class SkinList : public QAbstractListModel { private: shared_qobject_ptr m_watcher; - bool is_watching; - QVector m_skin_list; + bool m_isWatching; + QVector m_skinList; QDir m_dir; MinecraftAccountPtr m_acct; }; \ No newline at end of file diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index 937864e2c..b609bc6c7 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -18,17 +18,91 @@ #include "SkinModel.h" #include -#include #include -#include #include "FileSystem.h" #include "Json.h" -SkinModel::SkinModel(QString path) : m_path(path), m_texture(path), m_model(Model::CLASSIC) {} +static QImage improveSkin(const QImage& skin) +{ + if (skin.size() == QSize(64, 32)) { // old format + QImage newSkin = QImage(QSize(64, 64), skin.format()); + newSkin.fill(Qt::transparent); + QPainter p(&newSkin); + p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head + + auto leg = skin.copy(QRect(0, 16, 16, 16)); + p.drawImage(QPoint(16, 48), leg); // copy leg + + auto arm = skin.copy(QRect(40, 16, 16, 16)); + p.drawImage(QPoint(32, 48), arm); // copy arm + return newSkin; + } + return skin; +} +static QImage getSkin(const QString path) +{ + return improveSkin(QImage(path)); +} + +static QImage generatePreviews(QImage texture, bool slim) +{ + QImage preview(36, 36, QImage::Format_ARGB32); + preview.fill(Qt::transparent); + QPainter paint(&preview); + + // head + paint.drawImage(4, 2, texture.copy(8, 8, 8, 8)); + paint.drawImage(4, 2, texture.copy(40, 8, 8, 8)); + // torso + paint.drawImage(4, 10, texture.copy(20, 20, 8, 12)); + paint.drawImage(4, 10, texture.copy(20, 36, 8, 12)); + // right leg + paint.drawImage(4, 22, texture.copy(4, 20, 4, 12)); + paint.drawImage(4, 22, texture.copy(4, 36, 4, 12)); + // left leg + paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); + paint.drawImage(8, 22, texture.copy(20, 52, 4, 12)); + + auto armWidth = slim ? 3 : 4; + auto armPosX = slim ? 1 : 0; + // right arm + paint.drawImage(armPosX, 10, texture.copy(44, 20, armWidth, 12)); + paint.drawImage(armPosX, 10, texture.copy(44, 36, armWidth, 12)); + // left arm + paint.drawImage(12, 10, texture.copy(36, 52, armWidth, 12)); + paint.drawImage(12, 10, texture.copy(52, 52, armWidth, 12)); + + // back + // head + paint.drawImage(24, 2, texture.copy(24, 8, 8, 8)); + paint.drawImage(24, 2, texture.copy(56, 8, 8, 8)); + // torso + paint.drawImage(24, 10, texture.copy(32, 20, 8, 12)); + paint.drawImage(24, 10, texture.copy(32, 36, 8, 12)); + // right leg + paint.drawImage(24, 22, texture.copy(12, 20, 4, 12)); + paint.drawImage(24, 22, texture.copy(12, 36, 4, 12)); + // left leg + paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); + paint.drawImage(28, 22, texture.copy(28, 52, 4, 12)); + + // right arm + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 20, armWidth, 12)); + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 36, armWidth, 12)); + // left arm + paint.drawImage(32, 10, texture.copy(40 + armWidth, 52, armWidth, 12)); + paint.drawImage(32, 10, texture.copy(56 + armWidth, 52, armWidth, 12)); + + return preview; +} +SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_model(Model::CLASSIC) +{ + m_preview = generatePreviews(m_texture, false); +} SkinModel::SkinModel(QDir skinDir, QJsonObject obj) - : m_cape_id(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) + : m_capeId(Json::ensureString(obj, "capeId")), m_model(Model::CLASSIC), m_url(Json::ensureString(obj, "url")) { auto name = Json::ensureString(obj, "name"); @@ -36,7 +110,8 @@ SkinModel::SkinModel(QDir skinDir, QJsonObject obj) m_model = Model::SLIM; } m_path = skinDir.absoluteFilePath(name) + ".png"; - m_texture = QPixmap(m_path); + m_texture = QImage(getSkin(m_path)); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } QString SkinModel::name() const @@ -55,7 +130,7 @@ QJsonObject SkinModel::toJSON() const { QJsonObject obj; obj["name"] = name(); - obj["capeId"] = m_cape_id; + obj["capeId"] = m_capeId; obj["url"] = m_url; obj["model"] = getModelString(); return obj; @@ -76,3 +151,13 @@ bool SkinModel::isValid() const { return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; } +void SkinModel::refresh() +{ + m_texture = getSkin(m_path); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} +void SkinModel::setModel(Model model) +{ + m_model = model; + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} diff --git a/launcher/minecraft/skins/SkinModel.h b/launcher/minecraft/skins/SkinModel.h index 46e9d6cf1..711d7edb8 100644 --- a/launcher/minecraft/skins/SkinModel.h +++ b/launcher/minecraft/skins/SkinModel.h @@ -19,8 +19,8 @@ #pragma once #include +#include #include -#include class SkinModel { public: @@ -35,23 +35,25 @@ class SkinModel { QString getModelString() const; bool isValid() const; QString getPath() const { return m_path; } - QPixmap getTexture() const { return m_texture; } - QString getCapeId() const { return m_cape_id; } + QImage getTexture() const { return m_texture; } + QImage getPreview() const { return m_preview; } + QString getCapeId() const { return m_capeId; } Model getModel() const { return m_model; } QString getURL() const { return m_url; } bool rename(QString newName); - void setCapeId(QString capeID) { m_cape_id = capeID; } - void setModel(Model model) { m_model = model; } + void setCapeId(QString capeID) { m_capeId = capeID; } + void setModel(Model model); void setURL(QString url) { m_url = url; } - void refresh() { m_texture = QPixmap(m_path); } + void refresh(); QJsonObject toJSON() const; private: QString m_path; - QPixmap m_texture; - QString m_cape_id; + QImage m_texture; + QImage m_preview; + QString m_capeId; Model m_model; QString m_url; }; \ No newline at end of file diff --git a/launcher/resources/shaders/fshader.glsl b/launcher/resources/shaders/fshader.glsl new file mode 100644 index 000000000..d6a93db5d --- /dev/null +++ b/launcher/resources/shaders/fshader.glsl @@ -0,0 +1,20 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +// https://code.qt.io/cgit/qt/qtbase.git/tree/examples/opengl/cube/fshader.glsl +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform sampler2D texture; + +varying vec2 v_texcoord; + +void main() +{ + // Set fragment color from texture + vec4 texColor = texture2D(texture, v_texcoord); + if (texColor.a < 0.1) discard; // Optional: Discard fully transparent pixels + gl_FragColor = texColor; +} diff --git a/launcher/resources/shaders/shaders.qrc b/launcher/resources/shaders/shaders.qrc new file mode 100644 index 000000000..835e0fea7 --- /dev/null +++ b/launcher/resources/shaders/shaders.qrc @@ -0,0 +1,6 @@ + + + vshader.glsl + fshader.glsl + + \ No newline at end of file diff --git a/launcher/resources/shaders/vshader.glsl b/launcher/resources/shaders/vshader.glsl new file mode 100644 index 000000000..2d5e2db30 --- /dev/null +++ b/launcher/resources/shaders/vshader.glsl @@ -0,0 +1,26 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +// https://code.qt.io/cgit/qt/qtbase.git/tree/examples/opengl/cube/vshader.glsl +#ifdef GL_ES +// Set default precision to medium +precision mediump int; +precision mediump float; +#endif + +uniform mat4 mvp_matrix; +uniform mat4 model_matrix; + +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 v_texcoord; + +void main() +{ + // Calculate vertex position in screen space + gl_Position = mvp_matrix * model_matrix * a_position; + + // Pass texture coordinate to fragment shader + // Value will be automatically interpolated to fragments inside polygon faces + v_texcoord = a_texcoord; +} diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index f112e1acf..2127fc9cc 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,6 +17,7 @@ */ #include "SkinManageDialog.h" +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" #include "ui_SkinManageDialog.h" #include @@ -52,13 +53,15 @@ #include "ui/instanceview/InstanceDelegate.h" SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) - : QDialog(parent), m_acct(acct), ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) + : QDialog(parent), m_acct(acct), m_ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) { - ui->setupUi(this); + m_ui->setupUi(this); + + m_skinPreview = new SkinOpenGLWindow(this, palette().color(QPalette::Normal, QPalette::Base)); setWindowModality(Qt::WindowModal); - auto contentsWidget = ui->listView; + auto contentsWidget = m_ui->listView; contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setIconSize(QSize(48, 48)); @@ -88,28 +91,31 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection))); - connect(ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); + connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); setupCapes(); - ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); + m_ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); - ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); + + m_ui->skinLayout->insertWidget(0, QWidget::createWindowContainer(m_skinPreview, this)); } SkinManageDialog::~SkinManageDialog() { - delete ui; + delete m_ui; + delete m_skinPreview; } void SkinManageDialog::activated(QModelIndex index) { - m_selected_skin = index.data(Qt::UserRole).toString(); + m_selectedSkinKey = index.data(Qt::UserRole).toString(); accept(); } -void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +void SkinManageDialog::selectionChanged(QItemSelection selected, [[maybe_unused]] QItemSelection deselected) { if (selected.empty()) return; @@ -117,19 +123,20 @@ void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); if (key.isEmpty()) return; - m_selected_skin = key; - auto skin = m_list.skin(key); - if (!skin || !skin->isValid()) + m_selectedSkinKey = key; + auto skin = getSelectedSkin(); + if (!skin) return; - ui->selectedModel->setPixmap(skin->getTexture().scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); - ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId())); - ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); - ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); + + m_skinPreview->updateScene(skin); + m_ui->capeCombo->setCurrentIndex(m_capesIdx.value(skin->getCapeId())); + m_ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); + m_ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); } void SkinManageDialog::delayed_scroll(QModelIndex model_index) { - auto contentsWidget = ui->listView; + auto contentsWidget = m_ui->listView; contentsWidget->scrollTo(model_index); } @@ -152,23 +159,19 @@ void SkinManageDialog::on_fileBtn_clicked() } } -QPixmap previewCape(QPixmap capeImage) +QPixmap previewCape(QImage capeImage) { - QPixmap preview = QPixmap(10, 16); - QPainter painter(&preview); - painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); - return preview.scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation); + return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); } - void SkinManageDialog::setupCapes() { // FIXME: add a model for this, download/refresh the capes on demand auto& accountData = *m_acct->accountData(); int index = 0; - ui->capeCombo->addItem(tr("No Cape"), QVariant()); + m_ui->capeCombo->addItem(tr("No Cape"), QVariant()); auto currentCape = accountData.minecraftProfile.currentCape; if (currentCape.isEmpty()) { - ui->capeCombo->setCurrentIndex(index); + m_ui->capeCombo->setCurrentIndex(index); } auto capesDir = FS::PathCombine(m_list.getDir(), "capes"); @@ -177,9 +180,9 @@ void SkinManageDialog::setupCapes() for (auto& cape : accountData.minecraftProfile.capes) { auto path = FS::PathCombine(capesDir, cape.id + ".png"); if (cape.data.size()) { - QPixmap capeImage; + QImage capeImage; if (capeImage.loadFromData(cape.data, "PNG") && capeImage.save(path)) { - m_capes[cape.id] = previewCape(capeImage); + m_capes[cape.id] = capeImage; continue; } } @@ -197,46 +200,48 @@ void SkinManageDialog::setupCapes() } for (auto& cape : accountData.minecraftProfile.capes) { index++; - QPixmap capeImage; + QImage capeImage; if (!m_capes.contains(cape.id)) { auto path = FS::PathCombine(capesDir, cape.id + ".png"); if (QFileInfo(path).exists() && capeImage.load(path)) { - capeImage = previewCape(capeImage); m_capes[cape.id] = capeImage; } } if (!capeImage.isNull()) { - ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + m_ui->capeCombo->addItem(previewCape(capeImage), cape.alias, cape.id); } else { - ui->capeCombo->addItem(cape.alias, cape.id); + m_ui->capeCombo->addItem(cape.alias, cape.id); } - m_capes_idx[cape.id] = index; + m_capesIdx[cape.id] = index; } } void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) { - auto id = ui->capeCombo->currentData(); + auto id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { - ui->capeImage->setPixmap(cape.scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); } - if (auto skin = m_list.skin(m_selected_skin); skin) { + m_skinPreview->updateCape(cape); + if (auto skin = getSelectedSkin(); skin) { skin->setCapeId(id.toString()); + m_skinPreview->updateScene(skin); } } void SkinManageDialog::on_steveBtn_toggled(bool checked) { - if (auto skin = m_list.skin(m_selected_skin); skin) { + if (auto skin = getSelectedSkin(); skin) { skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM); + m_skinPreview->updateScene(skin); } } void SkinManageDialog::accept() { - auto skin = m_list.skin(m_selected_skin); + auto skin = m_list.skin(m_selectedSkinKey); if (!skin) { reject(); return; @@ -286,15 +291,15 @@ void SkinManageDialog::on_resetBtn_clicked() void SkinManageDialog::show_context_menu(const QPoint& pos) { QMenu myMenu(tr("Context menu"), this); - myMenu.addAction(ui->action_Rename_Skin); - myMenu.addAction(ui->action_Delete_Skin); + myMenu.addAction(m_ui->action_Rename_Skin); + myMenu.addAction(m_ui->action_Delete_Skin); - myMenu.exec(ui->listView->mapToGlobal(pos)); + myMenu.exec(m_ui->listView->mapToGlobal(pos)); } bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) { - if (obj == ui->listView) { + if (obj == m_ui->listView) { if (ev->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(ev); switch (keyEvent->key()) { @@ -314,22 +319,22 @@ bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked) { - if (!m_selected_skin.isEmpty()) { - ui->listView->edit(ui->listView->currentIndex()); + if (!m_selectedSkinKey.isEmpty()) { + m_ui->listView->edit(m_ui->listView->currentIndex()); } } void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) { - if (m_selected_skin.isEmpty()) + if (m_selectedSkinKey.isEmpty()) return; - if (m_list.getSkinIndex(m_selected_skin) == m_list.getSelectedAccountSkin()) { + if (m_list.getSkinIndex(m_selectedSkinKey) == m_list.getSelectedAccountSkin()) { CustomMessageBox::selectable(this, tr("Delete error"), tr("Can not delete skin that is in use."), QMessageBox::Warning)->exec(); return; } - auto skin = m_list.skin(m_selected_skin); + auto skin = m_list.skin(m_selectedSkinKey); if (!skin) return; @@ -341,15 +346,15 @@ void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) ->exec(); if (response == QMessageBox::Yes) { - if (!m_list.deleteSkin(m_selected_skin, true)) { - m_list.deleteSkin(m_selected_skin, false); + if (!m_list.deleteSkin(m_selectedSkinKey, true)) { + m_list.deleteSkin(m_selectedSkinKey, false); } } } void SkinManageDialog::on_urlBtn_clicked() { - auto url = QUrl(ui->urlLine->text()); + auto url = QUrl(m_ui->urlLine->text()); if (!url.isValid()) { CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show(); return; @@ -366,13 +371,13 @@ void SkinManageDialog::on_urlBtn_clicked() if (!s.isValid()) { CustomMessageBox::selectable(this, tr("URL is not a valid skin"), QFileInfo::exists(path) ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.") - : tr("Unable to download the skin: '%1'.").arg(ui->urlLine->text()), + : tr("Unable to download the skin: '%1'.").arg(m_ui->urlLine->text()), QMessageBox::Critical) ->show(); QFile::remove(path); return; } - ui->urlLine->setText(""); + m_ui->urlLine->setText(""); if (QFileInfo(path).suffix().isEmpty()) { QFile::rename(path, path + ".png"); } @@ -405,7 +410,7 @@ class WaitTask : public Task { void SkinManageDialog::on_userBtn_clicked() { - auto user = ui->urlLine->text(); + auto user = m_ui->urlLine->text(); if (user.isEmpty()) { return; } @@ -499,7 +504,7 @@ void SkinManageDialog::on_userBtn_clicked() QFile::remove(path); return; } - ui->urlLine->setText(""); + m_ui->urlLine->setText(""); s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setURL(mcProfile.skin.url); if (m_capes.contains(mcProfile.currentCape)) { @@ -513,14 +518,22 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event) QWidget::resizeEvent(event); QSize s = size() * (1. / 3); - if (auto skin = m_list.skin(m_selected_skin); skin) { - if (skin->isValid()) { - ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); - } - } - auto id = ui->capeCombo->currentData(); + auto id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { - ui->capeImage->setPixmap(cape.scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); } } + +SkinModel* SkinManageDialog::getSelectedSkin() +{ + if (auto skin = m_list.skin(m_selectedSkinKey); skin && skin->isValid()) { + return skin; + } + return nullptr; +} + +QHash SkinManageDialog::capes() +{ + return m_capes; +} diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.h b/launcher/ui/dialogs/skins/SkinManageDialog.h index cdb37a513..c6a6c9fcd 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.h +++ b/launcher/ui/dialogs/skins/SkinManageDialog.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,18 +24,22 @@ #include "minecraft/auth/MinecraftAccount.h" #include "minecraft/skins/SkinList.h" +#include "minecraft/skins/SkinModel.h" +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" namespace Ui { class SkinManageDialog; } - -class SkinManageDialog : public QDialog { +class SkinManageDialog : public QDialog, public SkinProvider { Q_OBJECT public: explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct); virtual ~SkinManageDialog(); void resizeEvent(QResizeEvent* event) override; + virtual SkinModel* getSelectedSkin() override; + virtual QHash capes() override; + public slots: void selectionChanged(QItemSelection, QItemSelection); void activated(QModelIndex); @@ -56,10 +60,12 @@ class SkinManageDialog : public QDialog { private: void setupCapes(); + private: MinecraftAccountPtr m_acct; - Ui::SkinManageDialog* ui; + Ui::SkinManageDialog* m_ui; SkinList m_list; - QString m_selected_skin; - QHash m_capes; - QHash m_capes_idx; + QString m_selectedSkinKey; + QHash m_capes; + QHash m_capesIdx; + SkinOpenGLWindow* m_skinPreview = nullptr; }; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui index c77eeaaa3..7e8b4bc46 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.ui +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -19,17 +19,7 @@ - - - - - - false - - - Qt::AlignCenter - - + diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp new file mode 100644 index 000000000..9a5ad1ce2 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "BoxGeometry.h" + +#include +#include +#include +#include + +struct VertexData { + QVector4D position; + QVector2D texCoord; + VertexData(const QVector4D& pos, const QVector2D& tex) : position(pos), texCoord(tex) {} +}; + +// For cube we would need only 8 vertices but we have to +// duplicate vertex for each face because texture coordinate +// is different. +static const QVector vertices = { + // Vertex data for face 0 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v0 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v1 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v2 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v3 + // Vertex data for face 1 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v4 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v5 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v6 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v7 + + // Vertex data for face 2 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v8 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v9 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v10 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v11 + + // Vertex data for face 3 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v12 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v13 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v14 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v15 + + // Vertex data for face 4 + QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v16 + QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v17 + QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v18 + QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v19 + + // Vertex data for face 5 + QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v20 + QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v21 + QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v22 + QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v23 +}; + +// Indices for drawing cube faces using triangle strips. +// Triangle strips can be connected by duplicating indices +// between the strips. If connecting strips have opposite +// vertex order then last index of the first strip and first +// index of the second strip needs to be duplicated. If +// connecting strips have same vertex order then only last +// index of the first strip needs to be duplicated. +static const QVector indices = { + 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) + 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) + 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) + 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15) + 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19) + 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) +}; + +static const QVector planeVertices = { + { QVector4D(-1.0f, -1.0f, -0.5f, 1.0f), QVector2D(0.0f, 0.0f) }, // Bottom-left + { QVector4D(1.0f, -1.0f, -0.5f, 1.0f), QVector2D(1.0f, 0.0f) }, // Bottom-right + { QVector4D(-1.0f, 1.0f, -0.5f, 1.0f), QVector2D(0.0f, 1.0f) }, // Top-left + { QVector4D(1.0f, 1.0f, -0.5f, 1.0f), QVector2D(1.0f, 1.0f) }, // Top-right +}; +static const QVector planeIndices = { + 0, 1, 2, 3, 3 // Face 0 - triangle strip ( v0, v1, v2, v3) +}; + +QVector transformVectors(const QMatrix4x4& matrix, const QVector& vectors) +{ + QVector transformedVectors; + transformedVectors.reserve(vectors.size()); + + for (const QVector4D& vec : vectors) { + if (!matrix.isIdentity()) { + transformedVectors.append(matrix * vec); + } else { + transformedVectors.append(vec); + } + } + + return transformedVectors; +} + +// Function to calculate UV coordinates +// this is pure magic (if something is wrong with textures this is at fault) +QVector getCubeUVs(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) +{ + auto toFaceVertices = [textureHeight, textureWidth](float x1, float y1, float x2, float y2) -> QVector { + return { + QVector2D(x1 / textureWidth, 1.0 - y2 / textureHeight), + QVector2D(x2 / textureWidth, 1.0 - y2 / textureHeight), + QVector2D(x2 / textureWidth, 1.0 - y1 / textureHeight), + QVector2D(x1 / textureWidth, 1.0 - y1 / textureHeight), + }; + }; + + auto top = toFaceVertices(u + depth, v, u + width + depth, v + depth); + auto bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth); + auto left = toFaceVertices(u, v + depth, u + depth, v + depth + height); + auto front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height); + auto right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth); + auto back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth); + + auto uvRight = { + right[0], + right[1], + right[3], + right[2], + }; + auto uvLeft = { + left[0], + left[1], + left[3], + left[2], + }; + auto uvTop = { + top[0], + top[1], + top[3], + top[2], + }; + auto uvBottom = { + bottom[3], + bottom[2], + bottom[0], + bottom[1], + }; + auto uvFront = { + front[0], + front[1], + front[3], + front[2], + }; + auto uvBack = { + back[0], + back[1], + back[3], + back[2], + }; + // Create a new array to hold the modified UV data + QVector uvData; + uvData.reserve(24); + + // Iterate over the arrays and copy the data to newUVData + for (const auto& uvArray : { uvFront, uvRight, uvBack, uvLeft, uvBottom, uvTop }) { + uvData.append(uvArray); + } + + return uvData; +} + +namespace opengl { +BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) : m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position) +{ + initializeOpenGLFunctions(); + + // Generate 2 VBOs + m_vertexBuf.create(); + m_indexBuf.create(); +} + +BoxGeometry::BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize) + : BoxGeometry(size, position) +{ + initGeometry(uv.x(), uv.y(), textureDim.x(), textureDim.y(), textureDim.z(), textureSize.width(), textureSize.height()); +} + +BoxGeometry::~BoxGeometry() +{ + m_vertexBuf.destroy(); + m_indexBuf.destroy(); +} + +void BoxGeometry::draw(QOpenGLShaderProgram* program) +{ + // Tell OpenGL which VBOs to use + program->setUniformValue("model_matrix", m_matrix); + m_vertexBuf.bind(); + m_indexBuf.bind(); + + // Offset for position + quintptr offset = 0; + + // Tell OpenGL programmable pipeline how to locate vertex position data + int vertexLocation = program->attributeLocation("a_position"); + program->enableAttributeArray(vertexLocation); + program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 4, sizeof(VertexData)); + + // Offset for texture coordinate + offset += sizeof(QVector4D); + // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data + int texcoordLocation = program->attributeLocation("a_texcoord"); + program->enableAttributeArray(texcoordLocation); + program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData)); + + // Draw cube geometry using indices from VBO 1 + glDrawElements(GL_TRIANGLE_STRIP, m_indecesCount, GL_UNSIGNED_SHORT, nullptr); +} + +void BoxGeometry::initGeometry(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) +{ + auto textureCord = getCubeUVs(u, v, width, height, depth, textureWidth, textureHeight); + + // this should not be needed to be done on each render for most of the objects + QMatrix4x4 transformation; + transformation.translate(m_position); + transformation.scale(m_size); + auto positions = transformVectors(transformation, vertices); + + QVector verticesData; + verticesData.reserve(positions.size()); // Reserve space for efficiency + + for (int i = 0; i < positions.size(); ++i) { + verticesData.append(VertexData(positions[i], textureCord[i])); + } + + // Transfer vertex data to VBO 0 + m_vertexBuf.bind(); + m_vertexBuf.allocate(verticesData.constData(), verticesData.size() * sizeof(VertexData)); + + // Transfer index data to VBO 1 + m_indexBuf.bind(); + m_indexBuf.allocate(indices.constData(), indices.size() * sizeof(GLushort)); + m_indecesCount = indices.size(); +} + +void BoxGeometry::rotate(float angle, const QVector3D& vector) +{ + m_matrix.rotate(angle, vector); +} + +BoxGeometry* BoxGeometry::Plane() +{ + auto b = new BoxGeometry(QVector3D(), QVector3D()); + + // Transfer vertex data to VBO 0 + b->m_vertexBuf.bind(); + b->m_vertexBuf.allocate(planeVertices.constData(), planeVertices.size() * sizeof(VertexData)); + + // Transfer index data to VBO 1 + b->m_indexBuf.bind(); + b->m_indexBuf.allocate(planeIndices.constData(), planeIndices.size() * sizeof(GLushort)); + b->m_indecesCount = planeIndices.size(); + + return b; +} +} // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.h b/launcher/ui/dialogs/skins/draw/BoxGeometry.h new file mode 100644 index 000000000..1a245bc14 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace opengl { +class BoxGeometry : protected QOpenGLFunctions { + public: + BoxGeometry(QVector3D size, QVector3D position); + BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize = { 64, 64 }); + static BoxGeometry* Plane(); + virtual ~BoxGeometry(); + + void draw(QOpenGLShaderProgram* program); + + void initGeometry(float u, float v, float width, float height, float depth, float textureWidth = 64, float textureHeight = 64); + void rotate(float angle, const QVector3D& vector); + + private: + QOpenGLBuffer m_vertexBuf; + QOpenGLBuffer m_indexBuf; + QVector3D m_size; + QVector3D m_position; + QMatrix4x4 m_matrix; + GLsizei m_indecesCount; +}; +} // namespace opengl diff --git a/launcher/ui/dialogs/skins/draw/Scene.cpp b/launcher/ui/dialogs/skins/draw/Scene.cpp new file mode 100644 index 000000000..45d0ba191 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/Scene.cpp @@ -0,0 +1,134 @@ + +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ui/dialogs/skins/draw/Scene.h" +namespace opengl { +Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim), m_capeVisible(!cape.isNull()) +{ + m_staticComponents = { + // head + new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)), + new opengl::BoxGeometry(QVector3D(9, 9, 9), QVector3D(0, 4, 0), QPoint(32, 0), QVector3D(8, 8, 8)), + // body + new opengl::BoxGeometry(QVector3D(8, 12, 4), QVector3D(0, -6, 0), QPoint(16, 16), QVector3D(8, 12, 4)), + new opengl::BoxGeometry(QVector3D(8.5, 12.5, 4.5), QVector3D(0, -6, 0), QPoint(16, 32), QVector3D(8, 12, 4)), + // right leg + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-1.9, -18, -0.1), QPoint(0, 16), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-1.9, -18, -0.1), QPoint(0, 32), QVector3D(4, 12, 4)), + // left leg + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(1.9, -18, -0.1), QPoint(16, 48), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(1.9, -18, -0.1), QPoint(0, 48), QVector3D(4, 12, 4)), + }; + m_normalArms = { + // Right Arm + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-6, -6, 0), QPoint(40, 16), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-6, -6, 0), QPoint(40, 32), QVector3D(4, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(6, -6, 0), QPoint(32, 48), QVector3D(4, 12, 4)), + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(6, -6, 0), QPoint(48, 48), QVector3D(4, 12, 4)), + }; + + m_slimArms = { + // Right Arm + new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(-5.5, -6, 0), QPoint(40, 16), QVector3D(3, 12, 4)), + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(-5.5, -6, 0), QPoint(40, 32), QVector3D(3, 12, 4)), + // Left Arm + new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(5.5, -6, 0), QPoint(32, 48), QVector3D(3, 12, 4)), + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(5.5, -6, 0), QPoint(48, 48), QVector3D(3, 12, 4)), + }; + + m_cape = new opengl::BoxGeometry(QVector3D(10, 16, 1), QVector3D(0, -8, 2.5), QPoint(0, 0), QVector3D(10, 16, 1), QSize(64, 32)); + m_cape->rotate(10.8, QVector3D(1, 0, 0)); + m_cape->rotate(180, QVector3D(0, 1, 0)); + + // texture init + m_skinTexture = new QOpenGLTexture(skin.mirrored()); + m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_skinTexture->setMagnificationFilter(QOpenGLTexture::Nearest); + + m_capeTexture = new QOpenGLTexture(cape.mirrored()); + m_capeTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_capeTexture->setMagnificationFilter(QOpenGLTexture::Nearest); +} +Scene::~Scene() +{ + for (auto array : { m_staticComponents, m_normalArms, m_slimArms }) { + for (auto g : array) { + delete g; + } + } + delete m_cape; + + m_skinTexture->destroy(); + delete m_skinTexture; + + m_capeTexture->destroy(); + delete m_capeTexture; +} + +void Scene::draw(QOpenGLShaderProgram* program) +{ + m_skinTexture->bind(); + program->setUniformValue("texture", 0); + for (auto toDraw : { m_staticComponents, m_slim ? m_slimArms : m_normalArms }) { + for (auto g : toDraw) { + g->draw(program); + } + } + m_skinTexture->release(); + if (m_capeVisible) { + m_capeTexture->bind(); + program->setUniformValue("texture", 0); + m_cape->draw(program); + m_capeTexture->release(); + } +} + +void updateTexture(QOpenGLTexture* texture, const QImage& img) +{ + if (texture) { + if (texture->isBound()) + texture->release(); + texture->destroy(); + texture->create(); + texture->setSize(img.width(), img.height()); + texture->setData(img); + texture->setMinificationFilter(QOpenGLTexture::Nearest); + texture->setMagnificationFilter(QOpenGLTexture::Nearest); + } +} + +void Scene::setSkin(const QImage& skin) +{ + updateTexture(m_skinTexture, skin.mirrored()); +} + +void Scene::setMode(bool slim) +{ + m_slim = slim; +} +void Scene::setCape(const QImage& cape) +{ + updateTexture(m_capeTexture, cape.mirrored()); +} +void Scene::setCapeVisible(bool visible) +{ + m_capeVisible = visible; +} +} // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/Scene.h b/launcher/ui/dialogs/skins/draw/Scene.h new file mode 100644 index 000000000..de683a659 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/Scene.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "ui/dialogs/skins/draw/BoxGeometry.h" + +#include +namespace opengl { +class Scene { + public: + Scene(const QImage& skin, bool slim, const QImage& cape); + virtual ~Scene(); + + void draw(QOpenGLShaderProgram* program); + void setSkin(const QImage& skin); + void setCape(const QImage& cape); + void setMode(bool slim); + void setCapeVisible(bool visible); + + private: + QVector m_staticComponents; + QVector m_normalArms; + QVector m_slimArms; + BoxGeometry* m_cape = nullptr; + QOpenGLTexture* m_skinTexture = nullptr; + QOpenGLTexture* m_capeTexture = nullptr; + bool m_slim = false; + bool m_capeVisible = false; +}; +} // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp new file mode 100644 index 000000000..97fe44175 --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" + +#include +#include +#include +#include +#include + +#include "minecraft/skins/SkinModel.h" +#include "rainbow.h" +#include "ui/dialogs/skins/draw/BoxGeometry.h" +#include "ui/dialogs/skins/draw/Scene.h" + +SkinOpenGLWindow::SkinOpenGLWindow(SkinProvider* parent, QColor color) + : QOpenGLWindow(), QOpenGLFunctions(), m_baseColor(color), m_parent(parent) +{ + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setDepthBufferSize(24); + setFormat(format); +} + +SkinOpenGLWindow::~SkinOpenGLWindow() +{ + // Make sure the context is current when deleting the texture + // and the buffers. + makeCurrent(); + // double check if resources were initialized because they are not + // initialized together with the object + if (m_scene) { + delete m_scene; + } + if (m_background) { + delete m_background; + } + if (m_backgroundTexture) { + if (m_backgroundTexture->isCreated()) { + m_backgroundTexture->destroy(); + } + delete m_backgroundTexture; + } + if (m_program) { + if (m_program->isLinked()) { + m_program->release(); + } + m_program->removeAllShaders(); + delete m_program; + } + doneCurrent(); +} + +void SkinOpenGLWindow::mousePressEvent(QMouseEvent* e) +{ + // Save mouse press position + m_mousePosition = QVector2D(e->pos()); + m_isMousePressed = true; +} + +void SkinOpenGLWindow::mouseMoveEvent(QMouseEvent* event) +{ + if (m_isMousePressed) { + int dx = event->x() - m_mousePosition.x(); + int dy = event->y() - m_mousePosition.y(); + + m_yaw += dx * 0.5f; + m_pitch += dy * 0.5f; + + // Normalize yaw to keep it manageable + if (m_yaw > 360.0f) + m_yaw -= 360.0f; + else if (m_yaw < 0.0f) + m_yaw += 360.0f; + + m_mousePosition = QVector2D(event->pos()); + update(); // Trigger a repaint + } +} + +void SkinOpenGLWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* e) +{ + m_isMousePressed = false; +} + +void SkinOpenGLWindow::initializeGL() +{ + initializeOpenGLFunctions(); + + glClearColor(0, 0, 1, 1); + + initShaders(); + + generateBackgroundTexture(32, 32, 1); + + QImage skin, cape; + bool slim = false; + if (m_parent) { + if (auto s = m_parent->getSelectedSkin()) { + skin = s->getTexture(); + slim = s->getModel() == SkinModel::SLIM; + cape = m_parent->capes().value(s->getCapeId(), {}); + } + } + + m_scene = new opengl::Scene(skin, slim, cape); + m_background = opengl::BoxGeometry::Plane(); + glEnable(GL_TEXTURE_2D); +} + +void SkinOpenGLWindow::initShaders() +{ + m_program = new QOpenGLShaderProgram(this); + // Compile vertex shader + if (!m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader.glsl")) + close(); + + // Compile fragment shader + if (!m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) + close(); + + // Link shader pipeline + if (!m_program->link()) + close(); + + // Bind shader pipeline for use + if (!m_program->bind()) + close(); +} + +void SkinOpenGLWindow::resizeGL(int w, int h) +{ + // Calculate aspect ratio + qreal aspect = qreal(w) / qreal(h ? h : 1); + + const qreal zNear = .1, zFar = 1000., fov = 45; + + // Reset projection + m_projection.setToIdentity(); + + // Set perspective projection + m_projection.perspective(fov, aspect, zNear, zFar); +} + +void SkinOpenGLWindow::paintGL() +{ + // Clear color and depth buffer + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Enable depth buffer + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + + // Enable back face culling + glEnable(GL_CULL_FACE); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + m_program->bind(); + + renderBackground(); + // Calculate model view transformation + QMatrix4x4 matrix; + float yawRad = qDegreesToRadians(m_yaw); + float pitchRad = qDegreesToRadians(m_pitch); + matrix.lookAt(QVector3D( // + m_distance * qCos(pitchRad) * qCos(yawRad), // + m_distance * qSin(pitchRad) - 8, // + m_distance * qCos(pitchRad) * qSin(yawRad)), + QVector3D(0, -8, 0), QVector3D(0, 1, 0)); + + // Set modelview-projection matrix + m_program->setUniformValue("mvp_matrix", m_projection * matrix); + + m_scene->draw(m_program); + m_program->release(); +} + +void SkinOpenGLWindow::updateScene(SkinModel* skin) +{ + if (skin && m_scene) { + m_scene->setMode(skin->getModel() == SkinModel::SLIM); + m_scene->setSkin(skin->getTexture()); + update(); + } +} +void SkinOpenGLWindow::updateCape(const QImage& cape) +{ + if (m_scene) { + m_scene->setCapeVisible(!cape.isNull()); + m_scene->setCape(cape); + update(); + } +} + +QColor calculateContrastingColor(const QColor& color) +{ + constexpr float contrast = 0.2; + auto luma = Rainbow::luma(color); + if (luma < 0.5) { + return Rainbow::lighten(color, contrast); + } else { + return Rainbow::darken(color, contrast); + } +} + +QImage generateChessboardImage(int width, int height, int tileSize, QColor baseColor) +{ + QImage image(width, height, QImage::Format_RGB888); + auto white = baseColor; + auto black = calculateContrastingColor(baseColor); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + bool isWhite = ((x / tileSize) % 2) == ((y / tileSize) % 2); + image.setPixelColor(x, y, isWhite ? white : black); + } + } + return image; +} + +void SkinOpenGLWindow::generateBackgroundTexture(int width, int height, int tileSize) +{ + m_backgroundTexture = new QOpenGLTexture(generateChessboardImage(width, height, tileSize, m_baseColor)); + m_backgroundTexture->setMinificationFilter(QOpenGLTexture::Nearest); + m_backgroundTexture->setMagnificationFilter(QOpenGLTexture::Nearest); +} + +void SkinOpenGLWindow::renderBackground() +{ + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); // Disable depth buffer writing + m_backgroundTexture->bind(); + QMatrix4x4 matrix; + m_program->setUniformValue("mvp_matrix", matrix); + m_program->setUniformValue("texture", 0); + m_background->draw(m_program); + m_backgroundTexture->release(); + glDepthMask(GL_TRUE); // Re-enable depth buffer writing + glEnable(GL_DEPTH_TEST); +} + +void SkinOpenGLWindow::wheelEvent(QWheelEvent* event) +{ + // Adjust distance based on scroll + int delta = event->angleDelta().y(); // Positive for scroll up, negative for scroll down + m_distance -= delta * 0.01f; // Adjust sensitivity factor + m_distance = qMax(16.f, m_distance); // Clamp distance + update(); // Trigger a repaint +} diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h new file mode 100644 index 000000000..e2c32da0f --- /dev/null +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "minecraft/skins/SkinModel.h" +#include "ui/dialogs/skins/draw/BoxGeometry.h" +#include "ui/dialogs/skins/draw/Scene.h" + +class SkinProvider { + public: + virtual ~SkinProvider() = default; + virtual SkinModel* getSelectedSkin() = 0; + virtual QHash capes() = 0; +}; +class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions { + Q_OBJECT + + public: + SkinOpenGLWindow(SkinProvider* parent, QColor color); + virtual ~SkinOpenGLWindow(); + + void updateScene(SkinModel* skin); + void updateCape(const QImage& cape); + + protected: + void mousePressEvent(QMouseEvent* e) override; + void mouseReleaseEvent(QMouseEvent* e) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + + void initializeGL() override; + void resizeGL(int w, int h) override; + void paintGL() override; + + void initShaders(); + + void generateBackgroundTexture(int width, int height, int tileSize); + void renderBackground(); + + private: + QOpenGLShaderProgram* m_program; + opengl::Scene* m_scene = nullptr; + + QMatrix4x4 m_projection; + + QVector2D m_mousePosition; + + bool m_isMousePressed = false; + float m_distance = 48; + float m_yaw = 90; // Horizontal rotation angle + float m_pitch = 0; // Vertical rotation angle + + opengl::BoxGeometry* m_background = nullptr; + QOpenGLTexture* m_backgroundTexture = nullptr; + QColor m_baseColor; + SkinProvider* m_parent = nullptr; +};