Revision control

++ // 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);
++ }
++
++ QString LightmapViewerHelpers::lightmapTagToString(QSSGLightmapIODataTag tag)
++ {
++ switch (tag) {
++ case QSSGLightmapIODataTag::Unset:
++ return QStringLiteral("Unset");
++ case QSSGLightmapIODataTag::Mask:
++ return QStringLiteral("Mask");
++ case QSSGLightmapIODataTag::Texture_Final:
++ return QStringLiteral("Texture_Final");
++ case QSSGLightmapIODataTag::Texture_Direct:
++ return QStringLiteral("Texture_Direct");
++ case QSSGLightmapIODataTag::Texture_Indirect:
++ return QStringLiteral("Texture_Indirect");
++ case QSSGLightmapIODataTag::Metadata:
++ return QStringLiteral("Metadata");
++ case QSSGLightmapIODataTag::SceneMetadata:
++ return QStringLiteral("SceneMetadata");
++ case QSSGLightmapIODataTag::Mesh:
++ return QStringLiteral("Mesh");
++ case QSSGLightmapIODataTag::OriginalScale:
++ return QStringLiteral("OriginalScale");
++ case QSSGLightmapIODataTag::Count:
++ break;
++ }
++ return QStringLiteral("Invalid");
++ }
++
++ QSSGLightmapIODataTag LightmapViewerHelpers::stringToLightmapTag(const QString &tag)
++ {
++ if (tag == QStringLiteral("Unset"))
++ return QSSGLightmapIODataTag::Unset;
++ if (tag == QStringLiteral("Mask"))
++ return QSSGLightmapIODataTag::Mask;
++ if (tag == QStringLiteral("Texture_Final"))
++ return QSSGLightmapIODataTag::Texture_Final;
++ if (tag == QStringLiteral("Texture_Direct"))
++ return QSSGLightmapIODataTag::Texture_Direct;
++ if (tag == QStringLiteral("Texture_Indirect"))
++ return QSSGLightmapIODataTag::Texture_Indirect;
++ if (tag == QStringLiteral("Metadata"))
++ return QSSGLightmapIODataTag::Metadata;
++ if (tag == QStringLiteral("SceneMetadata"))
++ return QSSGLightmapIODataTag::SceneMetadata;
++ if (tag == QStringLiteral("Mesh"))
++ return QSSGLightmapIODataTag::Mesh;
++ if (tag == QStringLiteral("OriginalScale"))
++ return QSSGLightmapIODataTag::OriginalScale;
++
++ qWarning() << "Could not match tag for: " << tag;
++ return QSSGLightmapIODataTag::Unset;
++ }
++
++ LightmapViewerHelpers::SceneMetadata LightmapViewerHelpers::processSceneMetadata(const QVariantMap &map)
++ {
++ SceneMetadata sceneMetadata;
++ if (map.isEmpty())
++ return sceneMetadata;
++
++ const auto doc = QJsonDocument::fromVariant(map);
++ const auto obj = doc.object();
++
++ sceneMetadata.qtVersion = obj.value("qt_version").toString();
++ const auto bakeStartTs = obj.value("bake_start_time").toInteger();
++ if (bakeStartTs > 0) {
++ sceneMetadata.bakeStartTime = QDateTime::fromMSecsSinceEpoch(bakeStartTs)
++ .toString(Qt::DateFormat::TextDate);
++
++ const qint64 bakeEndTs = obj.value("bake_end_time").toInteger();
++ const qint64 denoiseStartTs = obj.value("denoise_start_time").toInteger();
++ const qint64 denoiseEndTs = obj.value("denoise_start_time").toInteger();
++
++ qint64 seconds = ((bakeEndTs - bakeStartTs) + (denoiseEndTs - denoiseStartTs)) / 1000;
++ qint64 minutes = seconds / 60;
++ const qint64 hours = minutes / 60;
++ seconds %= 60;
++ minutes %= 60;
++
++ if (hours)
++ sceneMetadata.bakeDuration = QString("%1h %2m %3s")
++ .arg(hours)
++ .arg(minutes)
++ .arg(seconds);
++ else if (minutes)
++ sceneMetadata.bakeDuration = QString("%1m %2s").arg(minutes).arg(seconds);
++ else
++ sceneMetadata.bakeDuration = QString("%1s").arg(seconds);
++ }
++
++ if (obj.contains("options") && obj.value("options").isObject()) {
++ const auto options = obj.value("options").toObject();
++ sceneMetadata.options.reserve(options.size());
++ for (auto it = options.begin(); it != options.end(); ++it) {
++ QVariantMap row;
++ row.insert(QStringLiteral("key"), it.key());
++
++ const QJsonValue &val = it.value();
++ if (val.isDouble()) {
++ // Max 6 decimals
++ row.insert(QStringLiteral("value"), QString::number(val.toDouble(), 'g', 6));
++ } else {
++ row.insert(QStringLiteral("value"), val.toVariant());
++ }
++
++ sceneMetadata.options.push_back(row);
++ }
++ }
++
++ return sceneMetadata;
++ }
++
++ QVariantList LightmapViewerHelpers::processMetadata(const QString &key, const QVariantMap &map)
++ {
++ if (map.isEmpty())
++ return {};
++
++
++ auto appendRow = [](QVariantList &map, const QString &key, const QString &value) {
++ QVariantMap row;
++ row.insert("key", key);
++ row.insert("value", value);
++ map.push_back(row);
++ };
++
++ QVariantList metadata;
++
++ appendRow(metadata, "key", key);
++
++ if (map.contains("mesh_key")) {
++ appendRow(metadata, "meshKey", map["mesh_key"].toString());
++ }
++
++ if (map.contains("height") && map.contains("width")) {
++
++ appendRow(metadata, "Lightmap pixel size", QStringLiteral("%1x%2").
++ arg(map["width"].toString()).arg(
++ map["height"].toString()));
++ }
++
++ return metadata;
++ }
++
++ 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 (extract) {
++ QDir dir;
++ for (const char *path : { "meshes", "images", "images/masks", "images/direct", "images/indirect", "images/final" }) {
++ if (!dir.mkpath(path)) {
++ qInfo() << "Failed to create folders";
++ return false;
++ }
++ }
++ }
++
++ int numImagesSaved = 0;
++ int numMeshesSaved = 0;
++
++ QList<std::pair<QString, QSSGLightmapIODataTag>> keys = loader->getKeys();
++
++ if (print)
++ qInfo() << "-- Keys --";
++
++ QVector<QString> meshKeys;
++
++ QVariantMap sceneMetadata;
++ for (const auto &[key, tag] : std::as_const(keys)) {
++ if (tag == QSSGLightmapIODataTag::SceneMetadata) {
++ sceneMetadata = loader->readMap(key, tag);
++ continue;
++ }
++ QString tagString = LightmapViewerHelpers::lightmapTagToString(tag);
++
++ if (print)
++ qInfo() << key << ":" << tagString;
++
++ if (tag == QSSGLightmapIODataTag::Mesh)
++ meshKeys.push_back(key);
++ }
++
++ if (print)
++ qInfo() << "-- Values --";
++
++ // Extract meshes
++ if (extract) {
++ for (const QString &key : meshKeys) {
++ const QByteArray meshData = loader->readData(key, QSSGLightmapIODataTag::Mesh);
++ 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 auto &[key, tag] : std::as_const(keys)) {
++ if (tag != QSSGLightmapIODataTag::Metadata)
++ continue;
++
++ QVariantMap map = loader->readMap(key, tag);
++ if (print) {
++ qInfo() << key << ":";
++ qInfo().noquote() << QJsonDocument(QJsonObject::fromVariantMap(map)).toJson(QJsonDocument::Indented).trimmed();
++ }
++
++ int width = map[QStringLiteral("width")].toInt();
++ int height = map[QStringLiteral("height")].toInt();
++
++ if (extract) {
++ if (keys.contains(std::make_pair(key, QSSGLightmapIODataTag::Mask))) {
++ QByteArray data = loader->readU32Image(key, QSSGLightmapIODataTag::Mask);
++ maskToBBGRColor(data);
++ QImage img = QImage(reinterpret_cast<uchar *>(data.data()), width, height, QImage::Format_RGBA8888);
++ img.save(QString("images/masks/" + key + ".png"));
++ ++numImagesSaved;
++ }
++ for (const auto &[texTag, dir] : std::array {
++ std::pair { QSSGLightmapIODataTag::Texture_Direct, QStringLiteral("direct") },
++ std::pair { QSSGLightmapIODataTag::Texture_Indirect, QStringLiteral("indirect") },
++ std::pair { QSSGLightmapIODataTag::Texture_Final, QStringLiteral("final") },
++ }) {
++ if (!keys.contains(std::make_pair(key, texTag)))
++ continue;
++ QByteArray data = loader->readF32Image(key, texTag);
++ QImage img = QImage(reinterpret_cast<uchar *>(data.data()), width, height, QImage::Format_RGBA32FPx4);
++ img.save(QString("images/" + dir + "/" + key + ".png"));
++ ++numImagesSaved;
++ }
++ }
++ }
++
++ if (print && !sceneMetadata.isEmpty()) {
++ const auto processed = processSceneMetadata(sceneMetadata);
++
++ qInfo() << "-- Scene metadata --";
++ qInfo() << QStringLiteral("Baked with Qt version: %1")
++ .arg(!processed.qtVersion.isEmpty() ? processed.qtVersion : "-");
++ qInfo() << QStringLiteral("Bake initiated at: %1")
++ .arg(!processed.bakeStartTime.isEmpty() ? processed.bakeStartTime : "-");
++ qInfo() << QStringLiteral("Bake duration: %1")
++ .arg(!processed.bakeDuration.isEmpty() ? processed.bakeDuration : "-");
++ qInfo() << "Options used:";
++ for (const auto &option : processed.options) {
++ const auto map = option.toMap();
++ qInfo() << map.value("key").toString() << ": " << map.value("value").toString();
++ }
++ }
++
++ if (extract) {
++ qInfo() << "Saved" << numImagesSaved << "images to 'images' and " << numMeshesSaved << "meshes to 'meshes'";
++ }
++
++ return success;
++ }