aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonas Karlsson <[email protected]>2025-07-02 11:17:09 +0200
committerJonas Karlsson <[email protected]>2025-07-02 12:00:17 +0200
commit1faeac6a18d9398a493ec9b008f46d80005bc2e9 (patch)
tree71aa1a6c1e0e9be09f4a3c57e1566005b2014712
parent8c233e8f25fe51a013bfe4a78e6a18e650708c92 (diff)
Add lightmapviewer toolHEADdev
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.txt1
-rw-r--r--tools/lightmapviewer/CMakeLists.txt47
-rw-r--r--tools/lightmapviewer/LightmapViewer.qml257
-rw-r--r--tools/lightmapviewer/grid.pngbin0 -> 194 bytes
-rw-r--r--tools/lightmapviewer/lightmapfile.cpp35
-rw-r--r--tools/lightmapviewer/lightmapfile.h37
-rw-r--r--tools/lightmapviewer/lightmapimageprovider.cpp110
-rw-r--r--tools/lightmapviewer/lightmapimageprovider.h21
-rw-r--r--tools/lightmapviewer/lightmapviewer.cpp76
-rw-r--r--tools/lightmapviewer/lightmapviewerhelpers.cpp160
-rw-r--r--tools/lightmapviewer/lightmapviewerhelpers.h16
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
new file mode 100644
index 00000000..626d585f
--- /dev/null
+++ b/tools/lightmapviewer/grid.png
Binary files differ
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