diff options
author | Jonas Karlsson <[email protected]> | 2025-07-02 11:17:09 +0200 |
---|---|---|
committer | Jonas Karlsson <[email protected]> | 2025-07-02 12:00:17 +0200 |
commit | 1faeac6a18d9398a493ec9b008f46d80005bc2e9 (patch) | |
tree | 71aa1a6c1e0e9be09f4a3c57e1566005b2014712 | |
parent | 8c233e8f25fe51a013bfe4a78e6a18e650708c92 (diff) |
This is a debugging tool for viewing Qt's baked lightmap files. While
users can use this tool, it is intended for development only.
The tool is both a GUI tool for viewing lightmaps and a command line
application that accepts flags for printing info and extracting the
content of the lightmap file.
Pick-to: 6.10
Change-Id: I95279e248ea82e0e1384bbf880dcecc7af778dfe
Reviewed-by: Laszlo Agocs <[email protected]>
-rw-r--r-- | tools/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tools/lightmapviewer/CMakeLists.txt | 47 | ||||
-rw-r--r-- | tools/lightmapviewer/LightmapViewer.qml | 257 | ||||
-rw-r--r-- | tools/lightmapviewer/grid.png | bin | 0 -> 194 bytes | |||
-rw-r--r-- | tools/lightmapviewer/lightmapfile.cpp | 35 | ||||
-rw-r--r-- | tools/lightmapviewer/lightmapfile.h | 37 | ||||
-rw-r--r-- | tools/lightmapviewer/lightmapimageprovider.cpp | 110 | ||||
-rw-r--r-- | tools/lightmapviewer/lightmapimageprovider.h | 21 | ||||
-rw-r--r-- | tools/lightmapviewer/lightmapviewer.cpp | 76 | ||||
-rw-r--r-- | tools/lightmapviewer/lightmapviewerhelpers.cpp | 160 | ||||
-rw-r--r-- | tools/lightmapviewer/lightmapviewerhelpers.h | 16 |
11 files changed, 760 insertions, 0 deletions
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 0adef6c8..2372a24b 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -22,3 +22,4 @@ endif() if(QT_FEATURE_localserver) add_subdirectory(materialeditor) endif() +add_subdirectory(lightmapviewer) diff --git a/tools/lightmapviewer/CMakeLists.txt b/tools/lightmapviewer/CMakeLists.txt new file mode 100644 index 00000000..51ce2a5a --- /dev/null +++ b/tools/lightmapviewer/CMakeLists.txt @@ -0,0 +1,47 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_get_tool_target_name(target_name lightmapviewer) + +set(lightmapviewer_uri "QtQuick3D.lightmapviewer") +set(lightmapviewer_asset_prefix "/qt-project.org/imports/QtQuick3D/lightmapviewer") + +qt_internal_add_tool(${target_name} + TOOLS_TARGET Quick3D + SOURCES + lightmapfile.cpp + lightmapfile.h + lightmapimageprovider.cpp + lightmapimageprovider.h + lightmapviewer.cpp + lightmapviewerhelpers.cpp + lightmapviewerhelpers.h + LIBRARIES + Qt::Core + Qt::Gui + Qt::Quick + Qt::Quick3D + Qt::Quick3DPrivate + Qt::Quick3DRuntimeRender + Qt::Quick3DAssetImportPrivate + Qt::Quick3DRuntimeRenderPrivate +) + +qt_internal_return_unless_building_tools() + +qt_internal_add_resource(${target_name} "assets" + PREFIX + ${lightmapviewer_asset_prefix} + FILES + grid.png +) + +qt_internal_add_qml_module(${target_name} + VERSION 1.0 + URI ${lightmapviewer_uri} + QML_FILES + LightmapViewer.qml + NO_PLUGIN + IMPORTS + QtQuick3D +) diff --git a/tools/lightmapviewer/LightmapViewer.qml b/tools/lightmapviewer/LightmapViewer.qml new file mode 100644 index 00000000..e15e0510 --- /dev/null +++ b/tools/lightmapviewer/LightmapViewer.qml @@ -0,0 +1,257 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import QtQuick.Dialogs + +import LightmapFile 1.0 + +ApplicationWindow { + width: 640 + height: 480 + visible: true + title: qsTr("Lightmap Viewer") + + id: window + + property string selectedKey: listView.model[0] + property real imageZoom: 1 + property real imageCenterX: 0 + property real imageCenterY: 0 + + function clampImagePosition() { + // If the image is smaller than the scroll view, center it + if (image.width <= scrollView.width) { + imageCenterX = 0 + } else { + const maxOffsetX = (image.width - scrollView.width) / 2 + imageCenterX = Math.max(-maxOffsetX, Math.min(imageCenterX, + maxOffsetX)) + } + + if (image.height <= scrollView.height) { + imageCenterY = 0 + } else { + const maxOffsetY = (image.height - scrollView.height) / 2 + imageCenterY = Math.max(-maxOffsetY, Math.min(imageCenterY, + maxOffsetY)) + } + } + + header: ToolBar { + RowLayout { + Button { + text: qsTr("Open Lightmap") + onClicked: fileDialog.open() + } + + Rectangle { + width: 1 + color: "darkgray" + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + } + + Label { + text: "Zoom: " + window.imageZoom.toFixed(1) + } + + Rectangle { + width: 1 + color: "darkgray" + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + } + + Switch { + id: alphaSwitch + padding: 0 + checked: true + text: "Alpha" + } + + Rectangle { + width: 1 + color: "darkgray" + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + } + + Text { + text: "Path: " + LightmapFile.source + } + } + } + + FileDialog { + id: fileDialog + onAccepted: { + LightmapFile.source = selectedFile + LightmapFile.loadData() + } + } + + Shortcut { + sequences: [StandardKey.Open] + onActivated: { + fileDialog.open() + } + } + + SplitView { + anchors.fill: parent + orientation: Qt.Horizontal + + focus: true + Keys.onPressed: event => { + if (event.key === Qt.Key_Up) { + listView.currentIndex = Math.max( + 0, listView.currentIndex - 1) + selectedKey = listView.model[listView.currentIndex] + } else if (event.key === Qt.Key_Down) { + listView.currentIndex = Math.min( + listView.model.length - 1, + listView.currentIndex + 1) + selectedKey = listView.model[listView.currentIndex] + } + clampImagePosition() + } + ListView { + id: listView + SplitView.preferredWidth: 100 + SplitView.minimumWidth: 50 + model: LightmapFile.dataList + delegate: Text { + text: modelData + MouseArea { + anchors.fill: parent + onClicked: { + listView.currentIndex = index + selectedKey = modelData // Select this item + } + } + } + highlight: Rectangle { + color: "lightsteelblue" + radius: 1 + } + } + + Rectangle { + id: scrollView + clip: true + color: "black" + + property real lastMouseX: 0 + property real lastMouseY: 0 + + onWidthChanged: { + clampImagePosition() + } + onHeightChanged: { + clampImagePosition() + } + + MouseArea { + id: mouseArea + property bool dragging: false + anchors.fill: parent + onPressed: mouse => { + scrollView.lastMouseX = mouse.x + scrollView.lastMouseY = mouse.y + dragging = true + } + onReleased: mouse => { + dragging = false + } + + onPositionChanged: mouse => { + var dx = mouse.x - scrollView.lastMouseX + var dy = mouse.y - scrollView.lastMouseY + + scrollView.lastMouseX = mouse.x + scrollView.lastMouseY = mouse.y + + imageCenterX += dx + imageCenterY += dy + + clampImagePosition() + } + cursorShape: mouseArea.dragging ? Qt.ClosedHandCursor : Qt.ArrowCursor + + onWheel: event => { + const oldZoom = imageZoom + const zoomDelta = event.angleDelta.y / 256 + const newZoom = Math.max( + 1, Math.min(32, oldZoom + zoomDelta)) + + if (newZoom === oldZoom) + return + + // Adjust center offset so the same point remains at the center + const scaleFactor = newZoom / oldZoom + imageCenterX *= scaleFactor + imageCenterY *= scaleFactor + + imageZoom = newZoom + clampImagePosition() + + event.accepted = true + } + } + + Image { + id: baseGrid + anchors.fill: scrollView + source: "grid.png" + fillMode: Image.Tile + opacity: 0.75 + } + + Rectangle { + width: image.width + (border.width * 2) + height: image.height + (border.width * 2) + x: image.x - border.width + y: image.y - border.width + color: "white" // This is the border color + + border.width: 0 + border.color: "white" + opacity: 0.25 + } + + Image { + id: image + x: Math.round(parent.width / 2 - width / 2) + imageCenterX + y: Math.round(parent.height / 2 - height / 2) + imageCenterY + source: `image://lightmaps/key=${selectedKey}&file=${LightmapFile.source}&alpha=${alphaSwitch.checked}` + onWidthChanged: clampImagePosition() + onHeightChanged: clampImagePosition() + fillMode: Image.PreserveAspectFit + smooth: false + antialiasing: false + + // Let the image scale visibly + width: sourceSize.width * imageZoom + height: sourceSize.height * imageZoom + } + } + } + + DropArea { + id: dropArea + anchors.fill: parent + onEntered: (drag) => { + drag.accept(Qt.LinkAction) + } + // Just take first url if several + onDropped: (drop) => { + if (drop.hasUrls) { + LightmapFile.source = drop.urls[0] + LightmapFile.loadData() + } + } + } +} diff --git a/tools/lightmapviewer/grid.png b/tools/lightmapviewer/grid.png Binary files differnew file mode 100644 index 00000000..626d585f --- /dev/null +++ b/tools/lightmapviewer/grid.png diff --git a/tools/lightmapviewer/lightmapfile.cpp b/tools/lightmapviewer/lightmapfile.cpp new file mode 100644 index 00000000..4340b769 --- /dev/null +++ b/tools/lightmapviewer/lightmapfile.cpp @@ -0,0 +1,35 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "lightmapfile.h" + +#include <QFile> +#include <QTextStream> +#include <QtQuick3DRuntimeRender/private/qssglightmapio_p.h> + +LightmapFile::LightmapFile(QObject *parent) : QObject { parent } { } + +QStringList LightmapFile::dataList() const +{ + return m_dataList; +} + +void LightmapFile::loadData() +{ + QSharedPointer<QSSGLightmapLoader> loader = QSSGLightmapLoader::open(m_source.toLocalFile()); + m_dataList = loader ? loader->getKeys() : QStringList(); + emit dataListChanged(); +} + +QUrl LightmapFile::source() const +{ + return m_source; +} + +void LightmapFile::setSource(const QUrl &newSource) +{ + if (m_source == newSource) + return; + m_source = newSource; + emit sourceChanged(); +} diff --git a/tools/lightmapviewer/lightmapfile.h b/tools/lightmapviewer/lightmapfile.h new file mode 100644 index 00000000..01bd63b4 --- /dev/null +++ b/tools/lightmapviewer/lightmapfile.h @@ -0,0 +1,37 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef LIGHTMAPFILE_H +#define LIGHTMAPFILE_H + +#include <QObject> +#include <QList> +#include <QUrl> + +class LightmapFile : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringList dataList READ dataList NOTIFY dataListChanged) + Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged FINAL) + +public: + explicit LightmapFile(QObject *parent = nullptr); + + QStringList dataList() const; + + Q_INVOKABLE void loadData(); + + QUrl source() const; + void setSource(const QUrl &newSource); + +signals: + void dataListChanged(); + + void sourceChanged(); + +private: + QStringList m_dataList; + QUrl m_source; +}; + +#endif // LIGHTMAPFILE_H diff --git a/tools/lightmapviewer/lightmapimageprovider.cpp b/tools/lightmapviewer/lightmapimageprovider.cpp new file mode 100644 index 00000000..07645052 --- /dev/null +++ b/tools/lightmapviewer/lightmapimageprovider.cpp @@ -0,0 +1,110 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "lightmapimageprovider.h" + +#include "lightmapviewerhelpers.h" + +#include <QUrlQuery> +#include <QtQuick3DRuntimeRender/private/qssglightmapio_p.h> + +LightmapImageProvider::LightmapImageProvider() : QQuickImageProvider(QQuickImageProvider::Image) +{ + // Generate error image + constexpr int width = 100; + m_errorImage = QImage(QSize(width, width), QImage::Format::Format_RGB888); + m_errorImage.fill(QColorConstants::White); + for (int i = 0; i < width; i++) { + m_errorImage.setPixelColor(i, i, QColorConstants::Red); + m_errorImage.setPixelColor(width - i - 1, i, QColorConstants::Red); + } +} + +enum class LightmapDataType { Unset, F32, U32 }; + +QImage LightmapImageProvider::requestImage(const QString &id, QSize *size, const QSize & /*requestedSize*/) +{ + if (size) + *size = QSize(100, 100); + + const QUrlQuery query(QUrl("lightmap://?" + id)); + const QString key = query.queryItemValue("key"); + const QUrl filePath = query.queryItemValue("file"); + const bool useAlpha = query.queryItemValue("alpha") == QStringLiteral("true"); + + const QString meshPrefix = QStringLiteral("_mesh"); + const QString maskSuffix = QStringLiteral("_mask"); + const QString finalSuffix = QStringLiteral("_final"); + const QString directSuffix = QStringLiteral("_direct"); + const QString indirectSuffix = QStringLiteral("_indirect"); + const QString metadataSuffix = QStringLiteral("_metadata"); + + QString baseKey = key; + LightmapDataType dataType = LightmapDataType::Unset; + if (key.endsWith(maskSuffix)) { + baseKey = key.chopped(maskSuffix.length()); + dataType = LightmapDataType::U32; + } else if (key.endsWith(directSuffix)) { + baseKey = key.chopped(directSuffix.length()); + dataType = LightmapDataType::F32; + } else if (key.endsWith(indirectSuffix)) { + baseKey = key.chopped(indirectSuffix.length()); + dataType = LightmapDataType::F32; + } else if (key.endsWith(finalSuffix)) { + baseKey = key.chopped(finalSuffix.length()); + dataType = LightmapDataType::F32; + } else if (key.endsWith(metadataSuffix)) { + return m_errorImage; + } else if (key.startsWith(meshPrefix)) { + return m_errorImage; + } else { + // assume f32 image + baseKey = key; + dataType = LightmapDataType::F32; + } + + QSharedPointer<QSSGLightmapLoader> loader = QSSGLightmapLoader::open(filePath.toLocalFile()); + + if (!loader) + return m_errorImage; + + QVariantMap metadata = loader->readMetadata(baseKey); + if (metadata.isEmpty()) + return m_errorImage; + bool ok = false; + const int width = metadata[QStringLiteral("width")].toInt(&ok); + if (!ok) + return m_errorImage; + const int height = metadata[QStringLiteral("height")].toInt(&ok); + if (!ok) + return m_errorImage; + + QImage::Format format = QImage::Format_Invalid; + QByteArray array; + if (dataType == LightmapDataType::U32) { + array = loader->readU32Image(key); + if (array.size() != qsizetype(sizeof(quint32) * width * height)) + return m_errorImage; + format = QImage::Format_RGBA8888; + LightmapViewerHelpers::maskToBBGRColor(array, useAlpha); + } else if (dataType == LightmapDataType::F32) { + array = loader->readF32Image(key); + if (array.size() != qsizetype(4 * sizeof(float) * width * height)) + return m_errorImage; + if (!useAlpha) { + std::array<float, 4> *rgbas = reinterpret_cast<std::array<float, 4> *>(array.data()); + for (int i = 0, n = array.size() / (4 * sizeof(float)); i < n; ++i) + rgbas[i][3] = 1.0f; + } + format = QImage::Format_RGBA32FPx4; + } else { + return m_errorImage; + } + + // Everything should be OK here + if (size) + *size = QSize(width, height); + QImage result(width, height, format); + memcpy(result.bits(), array.data(), array.size()); + return result; +} diff --git a/tools/lightmapviewer/lightmapimageprovider.h b/tools/lightmapviewer/lightmapimageprovider.h new file mode 100644 index 00000000..9afa0cc9 --- /dev/null +++ b/tools/lightmapviewer/lightmapimageprovider.h @@ -0,0 +1,21 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef LIGHTMAPIMAGEPROVIDER_H +#define LIGHTMAPIMAGEPROVIDER_H + +#include <QImage> +#include <QQuickImageProvider> + +class LightmapImageProvider : public QQuickImageProvider +{ +public: + LightmapImageProvider(); + + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; + +private: + QImage m_errorImage; +}; + +#endif // LIGHTMAPIMAGEPROVIDER_H diff --git a/tools/lightmapviewer/lightmapviewer.cpp b/tools/lightmapviewer/lightmapviewer.cpp new file mode 100644 index 00000000..bb1b9d8c --- /dev/null +++ b/tools/lightmapviewer/lightmapviewer.cpp @@ -0,0 +1,76 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "lightmapfile.h" +#include "lightmapimageprovider.h" +#include "lightmapviewerhelpers.h" + +#include <QCoreApplication> +#include <QGuiApplication> +#include <QQmlApplicationEngine> +#include <QCommandLineParser> +#include <QCommandLineOption> +#include <QDebug> + +int main(int argc, char *argv[]) +{ + QString input; + + // We Handle all the command line parts first in its own QCoreApplication + // since we don't want to create a gui application which would not work if + // running through ssh for instance. + { + QCoreApplication app(argc, argv); + QCoreApplication::setApplicationName("Qt LightmapViewer"); + QCoreApplication::setApplicationVersion("1.0"); + + QCommandLineParser parser; + parser.setApplicationDescription("A tool for viewing Qt's baked lightmaps"); + parser.addHelpOption(); + parser.addVersionOption(); + + QCommandLineOption printOption("print", "Print the content of the lightmap"); + QCommandLineOption extractOption("extract", "Extract the lightmap contents into the current working directory"); + parser.addOption(printOption); + parser.addOption(extractOption); + + parser.addPositionalArgument("input", "Optional path to lightmap"); + + parser.process(app); + + bool doPrint = parser.isSet(printOption); + bool doExtract = parser.isSet(extractOption); + + const QStringList positionalArgs = parser.positionalArguments(); + input = positionalArgs.isEmpty() ? QString() : positionalArgs.first(); + + if (doPrint && input.isEmpty()) { + qFatal() << "Print option selected with no lightmap."; + return 1; + } + + if (doExtract && input.isEmpty()) { + qFatal() << "Extract option selected with no lightmap."; + return 1; + } + + if (doPrint || doExtract) + return !LightmapViewerHelpers::processLightmap(input, doPrint, doExtract); + } + + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; + QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection); + + LightmapFile *file = new LightmapFile(); + if (!input.isEmpty()) + file->setSource(QUrl::fromLocalFile(input)); + file->loadData(); + qmlRegisterSingletonInstance("LightmapFile", 1, 0, "LightmapFile", file); + + LightmapImageProvider *provider = new LightmapImageProvider; + engine.addImageProvider(QLatin1String("lightmaps"), provider); + engine.loadFromModule("QtQuick3D.lightmapviewer", "LightmapViewer"); + + return app.exec(); +} diff --git a/tools/lightmapviewer/lightmapviewerhelpers.cpp b/tools/lightmapviewer/lightmapviewerhelpers.cpp new file mode 100644 index 00000000..48eae741 --- /dev/null +++ b/tools/lightmapviewer/lightmapviewerhelpers.cpp @@ -0,0 +1,160 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "lightmapviewerhelpers.h" + +#include <QtQuick3DRuntimeRender/private/qssglightmapio_p.h> + +#include <QRgb> +#include <QVector> +#include <QImage> +#include <QFile> +#include <QJsonObject> +#include <QDir> + +static QRgb numberToBBGRColor(quint32 i, quint32 N, bool useAlpha) +{ + if (i < 1 || i > N) { + return qRgba(0, 0, 0, useAlpha ? 0 : 0xff); + } + + int range = N - 1; // exclude 0 + double t = static_cast<double>(i - 1) / range; + + quint8 r = 0, g = 0, b = 0; + + if (t < 0.5) { + // Blue -> Green + double t2 = t / 0.5; // normalize 0..1 + g = 255 * t2; + b = 255 * (1.0 - t2); + } else { + // Green -> Red + double t2 = (t - 0.5) / 0.5; + r = 255 * t2; + g = 255 * (1.0 - t2); + } + + return qRgba(r, g, b, 0xff); +} + +void LightmapViewerHelpers::maskToBBGRColor(QByteArray &array, bool useAlpha) +{ + QVector<quint32> uints; + uints.resize(array.size() / sizeof(quint32)); + memcpy(uints.data(), array.data(), array.size()); + + quint32 maxN = 0; + for (quint32 v : uints) { + maxN = qMax(maxN, v); + } + for (quint32 &vRef : uints) { + vRef = numberToBBGRColor(vRef, maxN, useAlpha); + } + memcpy(array.data(), uints.data(), array.size()); +} + +bool LightmapViewerHelpers::processLightmap(const QString &filename, bool print, bool extract) +{ + bool success = true; + QSharedPointer<QSSGLightmapLoader> loader = QSSGLightmapLoader::open(filename); + + if (!loader) { + return false; + } + + if (QDir dir; !dir.exists("meshes") && (!dir.mkpath("meshes") || !dir.mkpath("images"))) { + qInfo() << "Failed to create folders"; + return false; + } + + int numImagesSaved = 0; + int numMeshesSaved = 0; + + QList<QString> keys = loader->getKeys(); + + if (print) + qInfo() << "-- Keys --"; + + QVector<QString> baseKeys; + QVector<QString> meshKeys; + + for (const QString &key : std::as_const(keys)) { + if (print) + qInfo() << key; + + if (key.endsWith(QByteArrayLiteral("_metadata"))) { + baseKeys.push_back(key.chopped(9)); + } else if (key.startsWith(QByteArrayLiteral("_mesh"))) { + meshKeys.push_back(key); + } + } + + if (print) + qInfo() << "-- Values --"; + + // Extract meshes + if (extract) { + for (const QString &key : meshKeys) { + const QByteArray meshData = loader->readData(key); + QFile meshFile(QString("meshes/" + key + ".mesh")); + if (meshFile.open(QFile::WriteOnly)) { + meshFile.write(meshData); + meshFile.close(); + ++numMeshesSaved; + } else { + success = false; + qInfo() << key << "->" << "FAILED TO WRITE"; + } + } + } + + for (const QString &key : baseKeys) { + const QString key_metadata = key + QStringLiteral("_metadata"); + const QString key_mask = key + QStringLiteral("_mask"); + const QString key_direct = key + QStringLiteral("_direct"); + const QString key_indirect = key + QStringLiteral("_indirect"); + const QString key_final = key + QStringLiteral("_final"); + + int width = 0; + int height = 0; + + if (keys.contains(key_metadata)) { + QVariantMap map = loader->readMetadata(key); + if (print) { + qInfo() << key_metadata << ":"; + qInfo().noquote() << QJsonDocument(QJsonObject::fromVariantMap(map)).toJson(QJsonDocument::Indented).trimmed(); + } + width = map[QStringLiteral("width")].toInt(); + height = map[QStringLiteral("height")].toInt(); + } else { + success = false; + qInfo() << key << ": expected metadata key, skipping."; + continue; + } + + if (extract) { + if (keys.contains(key_mask)) { + QByteArray data = loader->readU32Image(key_mask); + maskToBBGRColor(data); + QImage img = QImage(reinterpret_cast<uchar *>(data.data()), width, height, QImage::Format_RGBA8888); + img.save(QString("images/" + key_mask + ".png")); + ++numImagesSaved; + } + for (const QString &imageKey : { key_direct, key_indirect, key_final }) { + if (keys.contains(imageKey)) { + QByteArray data = loader->readF32Image(imageKey); + QImage img = QImage(reinterpret_cast<uchar *>(data.data()), width, height, QImage::Format_RGBA32FPx4); + img.save(QString("images/" + imageKey + ".png")); + ++numImagesSaved; + } + } + } + } + + if (extract) { + qInfo() << "Saved" << numImagesSaved << "images to 'images' and " << numMeshesSaved << "meshes to 'meshes'"; + } + + return success; +} diff --git a/tools/lightmapviewer/lightmapviewerhelpers.h b/tools/lightmapviewer/lightmapviewerhelpers.h new file mode 100644 index 00000000..028360cf --- /dev/null +++ b/tools/lightmapviewer/lightmapviewerhelpers.h @@ -0,0 +1,16 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef LIGHTMAPVIEWERHELPERS_H +#define LIGHTMAPVIEWERHELPERS_H + +#include <QByteArray> +#include <QString> + +struct LightmapViewerHelpers +{ + static void maskToBBGRColor(QByteArray &array, bool useAlpha = true); + static bool processLightmap(const QString &filename, bool print, bool extract); +}; + +#endif // LIGHTMAPVIEWERHELPERS_H |