Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qssglightmapper.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5
7#include <QtQuick3DRuntimeRender/private/qssgrenderer_p.h>
8#include <QtQuick3DRuntimeRender/private/qssgrhiquadrenderer_p.h>
9#include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
10#include "../qssgrendercontextcore.h"
11#include <QtQuick3DUtils/private/qssgutils_p.h>
12
13#ifdef QT_QUICK3D_HAS_LIGHTMAPPER
14#include <QtCore/qfuture.h>
15#include <QtCore/qfileinfo.h>
16#include <QtConcurrent/qtconcurrentrun.h>
17#include <QRandomGenerator>
18#include <qsimd.h>
19#include <embree3/rtcore.h>
20#include <QtQuick3DRuntimeRender/private/qssglightmapio_p.h>
21#include <QDir>
22#include <QBuffer>
23#include <QWaitCondition>
24#include <QMutex>
25#include <QTemporaryFile>
26#if QT_CONFIG(opengl)
27#include <QOffscreenSurface>
28#include <QOpenGLContext>
29#endif
30#endif
31
33
34using namespace Qt::StringLiterals;
35
36// References:
37// https://ndotl.wordpress.com/2018/08/29/baking-artifact-free-lightmaps/
38// https://www.scratchapixel.com/lessons/3d-basic-rendering/global-illumination-path-tracing/
39// https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/gdc2018-precomputedgiobalilluminationinfrostbite.pdf
40// https://therealmjp.github.io/posts/new-blog-series-lightmap-baking-and-spherical-gaussians/
41// https://computergraphics.stackexchange.com/questions/2316/is-russian-roulette-really-the-answer
42// https://computergraphics.stackexchange.com/questions/4664/does-cosine-weighted-hemisphere-sampling-still-require-ndotl-when-calculating-co
43// https://www.rorydriscoll.com/2009/01/07/better-sampling/
44// https://github.com/TheRealMJP/BakingLab
45// https://github.com/candycat1992/LightmapperToy
46// https://github.com/godotengine/
47// https://github.com/jpcy/xatlas
48
49#ifdef QT_QUICK3D_HAS_LIGHTMAPPER
50
51static constexpr int GAUSS_HALF_KERNEL_SIZE = 3;
52static constexpr int DIRECT_MAP_UPSCALE_FACTOR = 4;
53static constexpr int MAX_TILE_SIZE = 1024;
54static constexpr quint32 PIXEL_VOID = 0; // Pixel not part of any mask
55static constexpr quint32 PIXEL_UNSET = -1; // Pixel part of mask, but not yet set
56static constexpr char KEY_SCENE_METADATA[] = "_qt_scene_metadata";
57
58static void floodFill(quint32 *maskUintPtr, const int rows, const int cols)
59{
60 quint32 targetColor = 1;
61 QList<std::array<int, 2>> stack;
62 stack.reserve(rows * cols);
63 for (int y0 = 0; y0 < rows; y0++) {
64 for (int x0 = 0; x0 < cols; x0++) {
65 bool filled = false;
66 stack.push_back({ x0, y0 });
67 while (!stack.empty()) {
68 const auto [x, y] = stack.takeLast();
69 const int idx = cols * y + x;
70 const quint32 value = maskUintPtr[idx];
71
72 // If the target color is already the same as the replacement color, no need to proceed
73 if (value != PIXEL_UNSET)
74 continue;
75
76 // Fill the current cell with the replacement color
77 maskUintPtr[idx] = targetColor;
78 filled = true;
79
80 // Push the neighboring cells onto the stack
81 if (x + 1 < cols)
82 stack.push_back({ x + 1, y });
83 if (x > 0)
84 stack.push_back({ x - 1, y });
85 if (y + 1 < rows)
86 stack.push_back({ x, y + 1 });
87 if (y > 0)
88 stack.push_back({ x, y - 1 });
89 }
90
91 if (filled) {
92 do {
93 targetColor++;
94 } while (targetColor == PIXEL_VOID || targetColor == PIXEL_UNSET);
95 }
96 }
97 }
98}
99
100static QString formatDuration(quint64 milliseconds, bool showMilliseconds = true)
101{
102 const quint64 partMilliseconds = milliseconds % 1000;
103 const quint64 partSeconds = (milliseconds / 1000) % 60;
104 const quint64 partMinutes = (milliseconds / 60000) % 60;
105 const quint64 partHours = (milliseconds / 3600000) % 60;
106
107 if (partHours > 0) {
108 return showMilliseconds
109 ? QStringLiteral("%1h %2m %3s %4ms").arg(partHours).arg(partMinutes).arg(partSeconds).arg(partMilliseconds)
110 : QStringLiteral("%1h %2m %3s").arg(partHours).arg(partMinutes).arg(partSeconds);
111 }
112 if (partMinutes > 0) {
113 return showMilliseconds ? QStringLiteral("%1m %2s %3ms").arg(partMinutes).arg(partSeconds).arg(partMilliseconds)
114 : QStringLiteral("%1m %2s").arg(partMinutes).arg(partSeconds);
115 }
116 if (partSeconds > 0) {
117 return showMilliseconds ? QStringLiteral("%1s %2ms").arg(partSeconds).arg(partMilliseconds)
118 : QStringLiteral("%1s").arg(partSeconds);
119 }
120 return showMilliseconds ? QStringLiteral("%1ms").arg(partMilliseconds) : QStringLiteral("0s");
121}
122
123enum class Stage {
124 Direct = 0,
125 Indirect = 1,
126 Denoise = 2
127};
128
129struct ProgressTracker
130{
131 void initBake(quint32 numIndirectSamples, quint32 numIndirectBounces)
132 {
133 // Just guesstimating the relative work loads here
134 const double direct = 2;
135 const double indirect = numIndirectSamples * numIndirectBounces;
136 const double denoise = 1;
137 const double combined = direct + indirect + denoise;
138
139 fractionDirect = qMax(direct / combined, 0.02); // Make direct and denoise at least 2% for cosmetics
140 fractionDenoise = qMax(denoise / combined, 0.02);
141 fractionIndirect = qMax(1.0 - fractionDirect - fractionDenoise, 0.0);
142 }
143
144 void initDenoise()
145 {
146 fractionDirect = 0;
147 fractionDenoise = 1;
148 fractionIndirect = 0;
149 }
150
151 void setTotalDirectTiles(quint32 totalDirectTilesNew)
152 {
153 totalDirectTiles = totalDirectTilesNew;
154 }
155
156 void setStage(Stage stageNew)
157 {
158 if (stage == stageNew)
159 return;
160 stage = stageNew;
161 if (stage == Stage::Indirect)
162 indirectTimer.start();
163 }
164
165 double getEstimatedTimeRemaining()
166 {
167 double estimatedTimeRemaining = -1.0;
168 if (stage == Stage::Indirect && indirectTimer.isValid()) {
169 double totalElapsed = indirectTimer.elapsed();
170 double fullEstimate = static_cast<double>(totalElapsed) / progressIndirect;
171 estimatedTimeRemaining = (1.0 - progressIndirect) * fullEstimate;
172 }
173 return estimatedTimeRemaining;
174 }
175
176 double getProgress()
177 {
178 return progress;
179 }
180
181 void directTileDone()
182 {
183 Q_ASSERT(stage == Stage::Direct);
184 directTilesDone++;
185 progress = (fractionDirect * directTilesDone) / qMax(1u, totalDirectTiles);
186 }
187
188 void denoisedModelDone(int i, int n)
189 {
190 Q_ASSERT(stage == Stage::Denoise);
191 progress = fractionDirect + fractionIndirect + (fractionDenoise * double(i) / n);
192 }
193
194 void indirectTexelDone(quint64 i, quint64 n)
195 {
196 Q_ASSERT(stage == Stage::Indirect);
197 progressIndirect = double(i) / n;
198 progress = fractionDirect + (fractionIndirect * progressIndirect);
199 }
200
201private:
202 double fractionDirect = 0;
203 double fractionIndirect = 0;
204 double fractionDenoise = 0;
205 double progress = 0;
206 double progressIndirect = 0;
207 quint32 totalDirectTiles = 0;
208 quint32 directTilesDone = 0;
209 Stage stage = Stage::Direct;
210 QElapsedTimer indirectTimer;
211};
212
213struct QSSGLightmapperPrivate
214{
215 explicit QSSGLightmapperPrivate() = default;
216
217 QSSGLightmapperOptions options;
218 QString outputPath;
219 QVector<QSSGBakedLightingModel> bakedLightingModels;
220 QRhi::Implementation rhiBackend = QRhi::Null;
221 std::unique_ptr<QSSGRenderContextInterface> rhiCtxInterface;
222 std::unique_ptr<QSSGRenderer> renderer;
223
224 // For the main thread to wait on the lightmapper being initialized
225 QWaitCondition initCondition;
226 QMutex initMutex;
227
228 QSSGLightmapper::Callback outputCallback;
229 QSSGLightmapper::BakingControl bakingControl;
230 QElapsedTimer totalTimer;
231
232 struct SubMeshInfo {
233 quint32 offset = 0;
234 quint32 count = 0;
235 unsigned int geomId = RTC_INVALID_GEOMETRY_ID;
236 QVector4D baseColor;
237 QSSGRenderImage *baseColorNode = nullptr;
238 QRhiTexture *baseColorMap = nullptr;
239 QVector3D emissiveFactor;
240 QSSGRenderImage *emissiveNode = nullptr;
241 QRhiTexture *emissiveMap = nullptr;
242 QSSGRenderImage *normalMapNode = nullptr;
243 QRhiTexture *normalMap = nullptr;
244 float normalStrength = 0.0f;
245 float opacity = 0.0f;
246 };
247 using SubMeshInfoList = QVector<SubMeshInfo>;
248 QVector<SubMeshInfoList> subMeshInfos;
249
250 struct DrawInfo {
251 QSize lightmapSize;
252 QByteArray vertexData;
253 quint32 vertexStride;
254 QByteArray indexData;
255 QRhiCommandBuffer::IndexFormat indexFormat = QRhiCommandBuffer::IndexUInt32;
256 quint32 positionOffset = UINT_MAX;
257 QRhiVertexInputAttribute::Format positionFormat = QRhiVertexInputAttribute::Float;
258 quint32 normalOffset = UINT_MAX;
259 QRhiVertexInputAttribute::Format normalFormat = QRhiVertexInputAttribute::Float;
260 quint32 uvOffset = UINT_MAX;
261 QRhiVertexInputAttribute::Format uvFormat = QRhiVertexInputAttribute::Float;
262 quint32 lightmapUVOffset = UINT_MAX;
263 QRhiVertexInputAttribute::Format lightmapUVFormat = QRhiVertexInputAttribute::Float;
264 quint32 tangentOffset = UINT_MAX;
265 QRhiVertexInputAttribute::Format tangentFormat = QRhiVertexInputAttribute::Float;
266 quint32 binormalOffset = UINT_MAX;
267 QRhiVertexInputAttribute::Format binormalFormat = QRhiVertexInputAttribute::Float;
268 int meshIndex = -1; // Maps to an index in meshInfos;
269 QVector3D scale; // Stored in metadata
270 };
271 QVector<DrawInfo> drawInfos; // per model
272 QVector<QByteArray> meshes;
273
274 struct Light {
275 enum {
276 Directional,
277 Point,
278 Spot
279 } type;
280 bool indirectOnly;
281 QVector3D direction;
282 QVector3D color;
283 QVector3D worldPos;
284 float cosConeAngle;
285 float cosInnerConeAngle;
286 float constantAttenuation;
287 float linearAttenuation;
288 float quadraticAttenuation;
289 };
290 QVector<Light> lights;
291
292 RTCDevice rdev = nullptr;
293 RTCScene rscene = nullptr;
294
295 struct RasterResult {
296 bool success = false;
297 int width = 0;
298 int height = 0;
299 QByteArray worldPositions; // vec4
300 QByteArray normals; // vec4
301 QByteArray baseColors; // vec4, static color * texture map value (both linear)
302 QByteArray emissions; // vec4, static factor * emission map value
303 };
304
305 struct ModelTexel {
306 QVector3D worldPos;
307 QVector3D normal;
308 QVector4D baseColor; // static color * texture map value (both linear)
309 QVector3D emission; // static factor * emission map value
310 bool isValid() const { return !worldPos.isNull() && !normal.isNull(); }
311 };
312
313 QVector<QVector<ModelTexel>> modelTexels; // commit geom
314 QVector<bool> modelHasBaseColorTransparency;
315 quint32 emissiveModelCount = 0; // Models that has any ModelTexel with emission > 0.
316 QVector<quint32> numValidTexels;
317
318 QVector<int> geomLightmapMap; // [geomId] -> index in lightmaps (NB lightmap is per-model, geomId is per-submesh)
319 QVector<float> subMeshOpacityMap; // [geomId] -> opacity
320
321 bool denoiseOnly = false;
322
323 double totalProgress = 0; // [0-1]
324 qint64 estimatedTimeRemaining = -1; // ms
325 quint64 indirectTexelsTotal = 0;
326 quint64 indirectTexelsDone = 0;
327
328 inline const ModelTexel &texelForLightmapUV(unsigned int geomId, float u, float v) const
329 {
330 // find the hit texel in the lightmap for the model to which the submesh with geomId belongs
331 const int modelIdx = geomLightmapMap[geomId];
332 QSize texelSize = drawInfos[modelIdx].lightmapSize;
333 u = qBound(0.0f, u, 1.0f);
334 // flip V, CPU-side data is top-left based
335 v = 1.0f - qBound(0.0f, v, 1.0f);
336
337 const int w = texelSize.width();
338 const int h = texelSize.height();
339 const int x = qBound(0, int(w * u), w - 1);
340 const int y = qBound(0, int(h * v), h - 1);
341 const int texelIdx = x + y * w;
342
343 return modelTexels[modelIdx][texelIdx];
344 }
345
346 bool userCancelled();
347 void sendOutputInfo(QSSGLightmapper::BakingStatus type,
348 std::optional<QString> msg,
349 bool outputToConsole = true,
350 bool outputConsoleTimeRemanining = false);
351 void updateStage(const QString &newStage);
352 bool commitGeometry();
353 bool prepareLightmaps();
354 bool verifyLights() const;
355 QVector<QVector3D> computeDirectLight(int lmIdx);
356 QVector<QVector3D> computeIndirectLight(int lmIdx,
357 int wgSizePerGroup,
358 int wgCount);
359 bool storeMeshes(QSharedPointer<QSSGLightmapWriter> tempFile);
360
361 RasterResult rasterizeLightmap(int lmIdx,
362 QSize outputSize,
363 QVector2D minUVRegion = QVector2D(0, 0),
364 QVector2D maxUVRegion = QVector2D(1, 1));
365
366 bool storeSceneMetadata(QSharedPointer<QSSGLightmapWriter> writer);
367 bool storeMetadata(int lmIdx, QSharedPointer<QSSGLightmapWriter> tempFile);
368 bool storeScale(int lmIdx, QSharedPointer<QSSGLightmapWriter> tempFile);
369 bool storeDirectLightData(int lmIdx, const QVector<QVector3D> &directLight, QSharedPointer<QSSGLightmapWriter> tempFile);
370 bool storeIndirectLightData(int lmIdx, const QVector<QVector3D> &indirectLight, QSharedPointer<QSSGLightmapWriter> tempFile);
371 bool storeMaskImage(int lmIdx, QSharedPointer<QSSGLightmapWriter> tempFile);
372
373 bool denoiseLightmaps();
374
375 QVector3D sampleDirectLight(QVector3D worldPos, QVector3D normal, bool allLight) const;
376 QByteArray dilate(const QSize &pixelSize, const QByteArray &image);
377
378 QString stage = QStringLiteral("Initializing");
379
380 ProgressTracker progressTracker;
381 qint64 bakeStartTime = 0;
382};
383
384// Used to output progress ETA during baking.
385// Have to do it this way because we are blocking on the render thread, so no event loop
386// for regular timers.
387class TimerThread : public QThread {
388 Q_OBJECT
389public:
390 TimerThread(QObject *parent = nullptr)
391 : QThread(parent), intervalMs(1000), stopped(false) {}
392
393 ~TimerThread() {
394 stop();
395 wait();
396 }
397
398 void setInterval(int ms) {
399 intervalMs = ms;
400 }
401
402 void setCallback(const std::function<void()>& func) {
403 callback = func;
404 }
405
406 void stop() {
407 stopped = true;
408 }
409
410protected:
411 void run() override {
412 int elapsed = 0;
413 while (!stopped) {
414 msleep(100);
415 if (stopped) break;
416
417 elapsed += 100;
418 if (elapsed >= intervalMs && callback) {
419 callback();
420 elapsed = 0;
421 }
422 }
423 }
424
425private:
426 int intervalMs;
427 std::function<void()> callback;
428 std::atomic<bool> stopped;
429};
430
431static const int LM_SEAM_BLEND_ITER_COUNT = 4;
432
433QSSGLightmapper::QSSGLightmapper() : d(new QSSGLightmapperPrivate())
434{
435#ifdef __SSE2__
436 _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
437 _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
438#endif
439}
440
441QSSGLightmapper::~QSSGLightmapper()
442{
443 reset();
444 delete d;
445
446#ifdef __SSE2__
447 _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_OFF);
448 _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_OFF);
449#endif
450}
451
452void QSSGLightmapper::reset()
453{
454 d->bakedLightingModels.clear();
455 d->subMeshInfos.clear();
456 d->drawInfos.clear();
457 d->lights.clear();
458
459 d->modelHasBaseColorTransparency.clear();
460 d->emissiveModelCount = 0;
461 d->meshes.clear();
462
463 d->geomLightmapMap.clear();
464 d->subMeshOpacityMap.clear();
465
466 if (d->rscene) {
467 rtcReleaseScene(d->rscene);
468 d->rscene = nullptr;
469 }
470 if (d->rdev) {
471 rtcReleaseDevice(d->rdev);
472 d->rdev = nullptr;
473 }
474
475 d->bakingControl.cancelled = false;
476 d->totalProgress = 0.0;
477 d->estimatedTimeRemaining = -1;
478}
479
480void QSSGLightmapper::setOptions(const QSSGLightmapperOptions &options)
481{
482 d->options = options;
483}
484
485void QSSGLightmapper::setOutputCallback(Callback callback)
486{
487 d->outputCallback = callback;
488}
489
490qsizetype QSSGLightmapper::add(const QSSGBakedLightingModel &model)
491{
492 d->bakedLightingModels.append(model);
493 return d->bakedLightingModels.size() - 1;
494}
495
496void QSSGLightmapper::setRhiBackend(QRhi::Implementation backend)
497{
498 d->rhiBackend = backend;
499}
500
501void QSSGLightmapper::setDenoiseOnly(bool value)
502{
503 d->denoiseOnly = value;
504}
505
506static void embreeErrFunc(void *, RTCError error, const char *str)
507{
508 qWarning("lm: Embree error: %d: %s", error, str);
509}
510
511static const unsigned int NORMAL_SLOT = 0;
512static const unsigned int LIGHTMAP_UV_SLOT = 1;
513
514static void embreeFilterFunc(const RTCFilterFunctionNArguments *args)
515{
516 RTCHit *hit = reinterpret_cast<RTCHit *>(args->hit);
517 QSSGLightmapperPrivate *d = static_cast<QSSGLightmapperPrivate *>(args->geometryUserPtr);
518 RTCGeometry geom = rtcGetGeometry(d->rscene, hit->geomID);
519
520 // convert from barycentric and overwrite u and v in hit with the result
521 rtcInterpolate0(geom, hit->primID, hit->u, hit->v, RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, LIGHTMAP_UV_SLOT, &hit->u, 2);
522
523 const float opacity = d->subMeshOpacityMap[hit->geomID];
524 const int modelIdx = d->geomLightmapMap[hit->geomID];
525 if (opacity < 1.0f || d->modelHasBaseColorTransparency[modelIdx]) {
526 const QSSGLightmapperPrivate::ModelTexel &texel(d->texelForLightmapUV(hit->geomID, hit->u, hit->v));
527
528 // In addition to material.opacity, take at least the base color (both
529 // the static color and the value from the base color map, if there is
530 // one) into account. Opacity map, alpha cutoff, etc. are ignored.
531 const float alpha = opacity * texel.baseColor.w();
532
533 // Ignore the hit if the alpha is low enough. This is not exactly perfect,
534 // but better than nothing. An object with an opacity lower than the
535 // threshold will act is if it was not there, as far as the intersection is
536 // concerned. So then the object won't cast shadows for example.
537 if (alpha < d->options.opacityThreshold)
538 args->valid[0] = 0;
539 }
540}
541
542static QByteArray meshToByteArray(const QSSGMesh::Mesh &mesh)
543{
544 QByteArray meshData;
545 QBuffer buffer(&meshData);
546 buffer.open(QIODevice::WriteOnly);
547 mesh.save(&buffer);
548
549 return meshData;
550}
551
552// Function to extract a scale from a transform matrix
553static QVector3D extractScale(const QMatrix4x4 &transform)
554{
555 Q_ASSERT(transform.isAffine());
556
557 // Extract scale factors by computing the length of the basis vectors (columns)
558 const QVector4D col0 = transform.column(0);
559 const QVector4D col1 = transform.column(1);
560 const QVector4D col2 = transform.column(2);
561
562 const float scaleX = QVector3D(col0[0], col0[1], col0[2]).length();
563 const float scaleY = QVector3D(col1[0], col1[1], col1[2]).length();
564 const float scaleZ = QVector3D(col2[0], col2[1], col2[2]).length();
565
566 return QVector3D(scaleX, scaleY, scaleZ);
567}
568
569
570static QMatrix4x4 constructScaleMatrix(const QVector3D &scale)
571{
572 // Construct a scale-only matrix
573 QMatrix4x4 scaleMatrix;
574 scaleMatrix.data()[0 * 4 + 0] = scale.x();
575 scaleMatrix.data()[1 * 4 + 1] = scale.y();
576 scaleMatrix.data()[2 * 4 + 2] = scale.z();
577 return scaleMatrix;
578}
579
580bool QSSGLightmapperPrivate::commitGeometry()
581{
582 if (bakedLightingModels.isEmpty()) {
583 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No models with usedInBakedLighting, cannot bake"));
584 return false;
585 }
586
587 sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Preparing geometry..."));
588 QElapsedTimer geomPrepTimer;
589 geomPrepTimer.start();
590
591 const auto &bufferManager(renderer->contextInterface()->bufferManager());
592
593 const int bakedLightingModelCount = bakedLightingModels.size();
594 subMeshInfos.resize(bakedLightingModelCount);
595 drawInfos.resize(bakedLightingModelCount);
596 modelTexels.resize(bakedLightingModelCount);
597 modelHasBaseColorTransparency.resize(bakedLightingModelCount, false);
598
599 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
600 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
601 if (lm.renderables.isEmpty()) {
602 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No submeshes, model %1 cannot be lightmapped").
603 arg(lm.model->lightmapKey));
604 return false;
605 }
606 if (lm.model->skin || lm.model->skeleton) {
607 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Skinned models not supported: %1").
608 arg(lm.model->lightmapKey));
609 return false;
610 }
611
612 subMeshInfos[lmIdx].reserve(lm.renderables.size());
613 for (const QSSGRenderableObjectHandle &handle : std::as_const(lm.renderables)) {
614 Q_ASSERT(handle.obj->type == QSSGRenderableObject::Type::DefaultMaterialMeshSubset
615 || handle.obj->type == QSSGRenderableObject::Type::CustomMaterialMeshSubset);
616 QSSGSubsetRenderable *renderableObj = static_cast<QSSGSubsetRenderable *>(handle.obj);
617 SubMeshInfo info;
618 info.offset = renderableObj->subset.offset;
619 info.count = renderableObj->subset.count;
620 info.opacity = renderableObj->opacity;
621 if (handle.obj->type == QSSGRenderableObject::Type::DefaultMaterialMeshSubset) {
622 const QSSGRenderDefaultMaterial *defMat = static_cast<const QSSGRenderDefaultMaterial *>(&renderableObj->material);
623 info.baseColor = defMat->color;
624 info.emissiveFactor = defMat->emissiveColor;
625 if (defMat->colorMap) {
626 info.baseColorNode = defMat->colorMap;
627 QSSGRenderImageTexture texture = bufferManager->loadRenderImage(defMat->colorMap);
628 info.baseColorMap = texture.m_texture;
629 }
630 if (defMat->emissiveMap) {
631 info.emissiveNode = defMat->emissiveMap;
632 QSSGRenderImageTexture texture = bufferManager->loadRenderImage(defMat->emissiveMap);
633 info.emissiveMap = texture.m_texture;
634 }
635 if (defMat->normalMap) {
636 info.normalMapNode = defMat->normalMap;
637 QSSGRenderImageTexture texture = bufferManager->loadRenderImage(defMat->normalMap);
638 info.normalMap = texture.m_texture;
639 info.normalStrength = defMat->bumpAmount;
640 }
641 } else {
642 info.baseColor = QVector4D(1.0f, 1.0f, 1.0f, 1.0f);
643 info.emissiveFactor = QVector3D(0.0f, 0.0f, 0.0f);
644 }
645 subMeshInfos[lmIdx].append(info);
646 }
647
648 QMatrix4x4 worldTransform;
649 QMatrix3x3 normalMatrix;
650 QSSGSubsetRenderable *renderableObj = static_cast<QSSGSubsetRenderable *>(lm.renderables.first().obj);
651 worldTransform = renderableObj->modelContext.globalTransform;
652 normalMatrix = renderableObj->modelContext.normalMatrix;
653 const QVector3D scale = extractScale(worldTransform);
654 const QMatrix4x4 scaleTransform = constructScaleMatrix(scale);
655
656 DrawInfo &drawInfo(drawInfos[lmIdx]);
657 drawInfo.scale = scale;
658 QSSGMesh::Mesh mesh;
659
660 if (lm.model->geometry)
661 mesh = bufferManager->loadMeshData(lm.model->geometry);
662 else
663 mesh = bufferManager->loadMeshData(lm.model->meshPath);
664
665 if (!mesh.isValid()) {
666 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning,
667 QStringLiteral("Failed to load geometry for model %1").arg(lm.model->lightmapKey));
668 return false;
669 }
670
671 QElapsedTimer unwrapTimer;
672 unwrapTimer.start();
673 // Use scene texelsPerUnit if the model's texelsPerUnit is unset (< 0)
674 const float texelsPerUnit = lm.model->texelsPerUnit <= 0.0f ? options.texelsPerUnit : lm.model->texelsPerUnit;
675 if (!mesh.createLightmapUVChannel(texelsPerUnit, scaleTransform)) {
676 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to do lightmap UV unwrapping for model %1").
677 arg(lm.model->lightmapKey));
678 return false;
679 }
680 sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
681 QStringLiteral("Lightmap UV unwrap done for model %1 in %2")
682 .arg(lm.model->lightmapKey, formatDuration(unwrapTimer.elapsed())));
683
684 if (lm.model->hasLightmap()) {
685 QByteArray meshData = meshToByteArray(mesh);
686
687 int meshIndex = -1;
688 bool doAdd = true;
689 for (int i = 0; i < meshes.size(); ++i) {
690 if (meshData == meshes[i]) {
691 doAdd = false;
692 meshIndex = i;
693 }
694 }
695
696 if (doAdd) {
697 meshes.push_back(meshData);
698 meshIndex = meshes.size() - 1;
699 }
700 drawInfo.meshIndex = meshIndex;
701 }
702
703 drawInfo.lightmapSize = mesh.subsets().constFirst().lightmapSizeHint;
704 drawInfo.vertexData = mesh.vertexBuffer().data;
705 drawInfo.vertexStride = mesh.vertexBuffer().stride;
706 drawInfo.indexData = mesh.indexBuffer().data;
707
708 if (drawInfo.vertexData.isEmpty()) {
709 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No vertex data for model %1").arg(lm.model->lightmapKey));
710 return false;
711 }
712 if (drawInfo.indexData.isEmpty()) {
713 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No index data for model %1").arg(lm.model->lightmapKey));
714 return false;
715 }
716
717 switch (mesh.indexBuffer().componentType) {
718 case QSSGMesh::Mesh::ComponentType::UnsignedInt16:
719 drawInfo.indexFormat = QRhiCommandBuffer::IndexUInt16;
720 break;
721 case QSSGMesh::Mesh::ComponentType::UnsignedInt32:
722 drawInfo.indexFormat = QRhiCommandBuffer::IndexUInt32;
723 break;
724 default:
725 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Unknown index component type %1 for model %2").
726 arg(int(mesh.indexBuffer().componentType)).
727 arg(lm.model->lightmapKey));
728 break;
729 }
730
731 const auto vertexBuffer = mesh.vertexBuffer();
732 for (const QSSGMesh::Mesh::VertexBufferEntry &vbe : vertexBuffer.entries) {
733 if (vbe.name == QSSGMesh::MeshInternal::getPositionAttrName()) {
734 drawInfo.positionOffset = vbe.offset;
735 drawInfo.positionFormat = QSSGRhiHelpers::toVertexInputFormat(QSSGRenderComponentType(vbe.componentType), vbe.componentCount);
736 } else if (vbe.name == QSSGMesh::MeshInternal::getNormalAttrName()) {
737 drawInfo.normalOffset = vbe.offset;
738 drawInfo.normalFormat = QSSGRhiHelpers::toVertexInputFormat(QSSGRenderComponentType(vbe.componentType), vbe.componentCount);
739 } else if (vbe.name == QSSGMesh::MeshInternal::getUV0AttrName()) {
740 drawInfo.uvOffset = vbe.offset;
741 drawInfo.uvFormat = QSSGRhiHelpers::toVertexInputFormat(QSSGRenderComponentType(vbe.componentType), vbe.componentCount);
742 } else if (vbe.name == QSSGMesh::MeshInternal::getLightmapUVAttrName()) {
743 drawInfo.lightmapUVOffset = vbe.offset;
744 drawInfo.lightmapUVFormat = QSSGRhiHelpers::toVertexInputFormat(QSSGRenderComponentType(vbe.componentType), vbe.componentCount);
745 } else if (vbe.name == QSSGMesh::MeshInternal::getTexTanAttrName()) {
746 drawInfo.tangentOffset = vbe.offset;
747 drawInfo.tangentFormat = QSSGRhiHelpers::toVertexInputFormat(QSSGRenderComponentType(vbe.componentType), vbe.componentCount);
748 } else if (vbe.name == QSSGMesh::MeshInternal::getTexBinormalAttrName()) {
749 drawInfo.binormalOffset = vbe.offset;
750 drawInfo.binormalFormat = QSSGRhiHelpers::toVertexInputFormat(QSSGRenderComponentType(vbe.componentType), vbe.componentCount);
751 }
752 }
753
754 if (!(drawInfo.positionOffset != UINT_MAX && drawInfo.normalOffset != UINT_MAX)) {
755 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Could not figure out position and normal attribute offsets for model %1").
756 arg(lm.model->lightmapKey));
757 return false;
758 }
759
760 // We will manually access and massage the data, so cannot just work with arbitrary formats.
761 if (!(drawInfo.positionFormat == QRhiVertexInputAttribute::Float3
762 && drawInfo.normalFormat == QRhiVertexInputAttribute::Float3))
763 {
764 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Position or normal attribute format is not as expected (float3) for model %1").
765 arg(lm.model->lightmapKey));
766 return false;
767 }
768
769 if (drawInfo.lightmapUVOffset == UINT_MAX) {
770 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Could not figure out lightmap UV attribute offset for model %1").
771 arg(lm.model->lightmapKey));
772 return false;
773 }
774
775 if (drawInfo.lightmapUVFormat != QRhiVertexInputAttribute::Float2) {
776 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Lightmap UV attribute format is not as expected (float2) for model %1").
777 arg(lm.model->lightmapKey));
778 return false;
779 }
780
781 // UV0 is optional
782 if (drawInfo.uvOffset != UINT_MAX) {
783 if (drawInfo.uvFormat != QRhiVertexInputAttribute::Float2) {
784 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("UV0 attribute format is not as expected (float2) for model %1").
785 arg(lm.model->lightmapKey));
786 return false;
787 }
788 }
789 // tangent and binormal are optional too
790 if (drawInfo.tangentOffset != UINT_MAX) {
791 if (drawInfo.tangentFormat != QRhiVertexInputAttribute::Float3) {
792 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Tangent attribute format is not as expected (float3) for model %1").
793 arg(lm.model->lightmapKey));
794 return false;
795 }
796 }
797 if (drawInfo.binormalOffset != UINT_MAX) {
798 if (drawInfo.binormalFormat != QRhiVertexInputAttribute::Float3) {
799 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Binormal attribute format is not as expected (float3) for model %1").
800 arg(lm.model->lightmapKey));
801 return false;
802 }
803 }
804
805 if (drawInfo.indexFormat == QRhiCommandBuffer::IndexUInt16) {
806 drawInfo.indexFormat = QRhiCommandBuffer::IndexUInt32;
807 QByteArray newIndexData(drawInfo.indexData.size() * 2, Qt::Uninitialized);
808 const quint16 *s = reinterpret_cast<const quint16 *>(drawInfo.indexData.constData());
809 size_t sz = drawInfo.indexData.size() / 2;
810 quint32 *p = reinterpret_cast<quint32 *>(newIndexData.data());
811 while (sz--)
812 *p++ = *s++;
813 drawInfo.indexData = newIndexData;
814 }
815
816 // Bake in the world transform.
817 {
818 char *vertexBase = drawInfo.vertexData.data();
819 const qsizetype sz = drawInfo.vertexData.size();
820 for (qsizetype offset = 0; offset < sz; offset += drawInfo.vertexStride) {
821 char *posPtr = vertexBase + offset + drawInfo.positionOffset;
822 float *fPosPtr = reinterpret_cast<float *>(posPtr);
823 QVector3D pos(fPosPtr[0], fPosPtr[1], fPosPtr[2]);
824 char *normalPtr = vertexBase + offset + drawInfo.normalOffset;
825 float *fNormalPtr = reinterpret_cast<float *>(normalPtr);
826 QVector3D normal(fNormalPtr[0], fNormalPtr[1], fNormalPtr[2]);
827 pos = worldTransform.map(pos);
828 normal = QSSGUtils::mat33::transform(normalMatrix, normal).normalized();
829 *fPosPtr++ = pos.x();
830 *fPosPtr++ = pos.y();
831 *fPosPtr++ = pos.z();
832 *fNormalPtr++ = normal.x();
833 *fNormalPtr++ = normal.y();
834 *fNormalPtr++ = normal.z();
835 }
836 }
837 } // end loop over models used in the lightmap
838
839 rdev = rtcNewDevice(nullptr);
840 if (!rdev) {
841 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create Embree device"));
842 return false;
843 }
844
845 rtcSetDeviceErrorFunction(rdev, embreeErrFunc, nullptr);
846
847 rscene = rtcNewScene(rdev);
848
849 unsigned int geomId = 1;
850
851 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
852 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
853
854 // While Light.castsShadow and Model.receivesShadows are irrelevant for
855 // baked lighting (they are effectively ignored, shadows are always
856 // there with baked direct lighting), Model.castsShadows is something
857 // we can and should take into account.
858 if (!lm.model->castsShadows)
859 continue;
860
861 const DrawInfo &drawInfo(drawInfos[lmIdx]);
862 const char *vbase = drawInfo.vertexData.constData();
863 const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
864
865 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
866 RTCGeometry geom = rtcNewGeometry(rdev, RTC_GEOMETRY_TYPE_TRIANGLE);
867 rtcSetGeometryVertexAttributeCount(geom, 2);
868 quint32 *ip = static_cast<quint32 *>(rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(uint32_t), subMeshInfo.count / 3));
869 for (quint32 i = 0; i < subMeshInfo.count; ++i)
870 *ip++ = i;
871 float *vp = static_cast<float *>(rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 3 * sizeof(float), subMeshInfo.count));
872 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
873 const quint32 idx = *(ibase + subMeshInfo.offset + i);
874 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
875 *vp++ = *src++;
876 *vp++ = *src++;
877 *vp++ = *src++;
878 }
879 vp = static_cast<float *>(rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, NORMAL_SLOT, RTC_FORMAT_FLOAT3, 3 * sizeof(float), subMeshInfo.count));
880 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
881 const quint32 idx = *(ibase + subMeshInfo.offset + i);
882 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
883 *vp++ = *src++;
884 *vp++ = *src++;
885 *vp++ = *src++;
886 }
887 vp = static_cast<float *>(rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, LIGHTMAP_UV_SLOT, RTC_FORMAT_FLOAT2, 2 * sizeof(float), subMeshInfo.count));
888 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
889 const quint32 idx = *(ibase + subMeshInfo.offset + i);
890 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
891 *vp++ = *src++;
892 *vp++ = *src++;
893 }
894 rtcCommitGeometry(geom);
895 rtcSetGeometryIntersectFilterFunction(geom, embreeFilterFunc);
896 rtcSetGeometryUserData(geom, this);
897 rtcAttachGeometryByID(rscene, geom, geomId);
898 subMeshInfo.geomId = geomId++;
899 rtcReleaseGeometry(geom);
900 }
901 }
902
903 rtcCommitScene(rscene);
904
905 RTCBounds bounds;
906 rtcGetSceneBounds(rscene, &bounds);
907 QVector3D lowerBound(bounds.lower_x, bounds.lower_y, bounds.lower_z);
908 QVector3D upperBound(bounds.upper_x, bounds.upper_y, bounds.upper_z);
909 qDebug() << "[lm] Bounds in world space for raytracing scene:" << lowerBound << upperBound;
910
911 const unsigned int geomIdBasedMapSize = geomId;
912 // Need fast lookup, hence indexing by geomId here. geomId starts from 1,
913 // meaning index 0 will be unused, but that's ok.
914 geomLightmapMap.fill(-1, geomIdBasedMapSize);
915 subMeshOpacityMap.fill(0.0f, geomIdBasedMapSize);
916
917 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
918 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
919 if (!lm.model->castsShadows) // only matters if it's in the raytracer scene
920 continue;
921 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx])
922 subMeshOpacityMap[subMeshInfo.geomId] = subMeshInfo.opacity;
923 }
924
925 sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Geometry ready. Time taken: %1").arg(formatDuration(geomPrepTimer.elapsed())));
926 return true;
927}
928
929QSSGLightmapperPrivate::RasterResult QSSGLightmapperPrivate::rasterizeLightmap(int lmIdx, QSize outputSize, QVector2D minUVRegion, QVector2D maxUVRegion)
930{
931 QSSGLightmapperPrivate::RasterResult result;
932
933 QSSGRhiContext *rhiCtx = rhiCtxInterface->rhiContext().get();
934 QRhi *rhi = rhiCtx->rhi();
935 QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
936
937 const DrawInfo &bakeModelDrawInfo(drawInfos[lmIdx]);
938 const bool hasUV0 = bakeModelDrawInfo.uvOffset != UINT_MAX;
939 const bool hasTangentAndBinormal = bakeModelDrawInfo.tangentOffset != UINT_MAX
940 && bakeModelDrawInfo.binormalOffset != UINT_MAX;
941
942 QRhiVertexInputLayout inputLayout;
943 inputLayout.setBindings({ QRhiVertexInputBinding(bakeModelDrawInfo.vertexStride) });
944
945 std::unique_ptr<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, bakeModelDrawInfo.vertexData.size()));
946 if (!vbuf->create()) {
947 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create vertex buffer"));
948 return result;
949 }
950 std::unique_ptr<QRhiBuffer> ibuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, bakeModelDrawInfo.indexData.size()));
951 if (!ibuf->create()) {
952 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create index buffer"));
953 return result;
954 }
955 QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
956 resUpd->uploadStaticBuffer(vbuf.get(), bakeModelDrawInfo.vertexData.constData());
957 resUpd->uploadStaticBuffer(ibuf.get(), bakeModelDrawInfo.indexData.constData());
958 QRhiTexture *dummyTexture = rhiCtx->dummyTexture({}, resUpd);
959 cb->resourceUpdate(resUpd);
960
961 std::unique_ptr<QRhiTexture> positionData(rhi->newTexture(QRhiTexture::RGBA32F, outputSize, 1,
962 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
963 if (!positionData->create()) {
964 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for positions"));
965 return result;
966 }
967 std::unique_ptr<QRhiTexture> normalData(rhi->newTexture(QRhiTexture::RGBA32F, outputSize, 1,
968 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
969 if (!normalData->create()) {
970 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for normals"));
971 return result;
972 }
973 std::unique_ptr<QRhiTexture> baseColorData(rhi->newTexture(QRhiTexture::RGBA32F, outputSize, 1,
974 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
975 if (!baseColorData->create()) {
976 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for base color"));
977 return result;
978 }
979 std::unique_ptr<QRhiTexture> emissionData(rhi->newTexture(QRhiTexture::RGBA32F, outputSize, 1,
980 QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
981 if (!emissionData->create()) {
982 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for emissive color"));
983 return result;
984 }
985
986 std::unique_ptr<QRhiRenderBuffer> ds(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, outputSize));
987 if (!ds->create()) {
988 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create depth-stencil buffer"));
989 return result;
990 }
991
992 QRhiColorAttachment posAtt(positionData.get());
993 QRhiColorAttachment normalAtt(normalData.get());
994 QRhiColorAttachment baseColorAtt(baseColorData.get());
995 QRhiColorAttachment emissionAtt(emissionData.get());
996 QRhiTextureRenderTargetDescription rtDesc;
997 rtDesc.setColorAttachments({ posAtt, normalAtt, baseColorAtt, emissionAtt });
998 rtDesc.setDepthStencilBuffer(ds.get());
999
1000 std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
1001 std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
1002 rt->setRenderPassDescriptor(rpDesc.get());
1003 if (!rt->create()) {
1004 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create texture render target"));
1005 return result;
1006 }
1007
1008 static const int UBUF_SIZE = 64;
1009 const int subMeshCount = subMeshInfos[lmIdx].size();
1010 const int alignedUbufSize = rhi->ubufAligned(UBUF_SIZE);
1011 const int totalUbufSize = alignedUbufSize * subMeshCount;
1012 std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, totalUbufSize));
1013 if (!ubuf->create()) {
1014 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create uniform buffer of size %1").arg(totalUbufSize));
1015 return result;
1016 }
1017
1018 // Must ensure that the final image is identical with all graphics APIs,
1019 // regardless of how the Y axis goes in the image and normalized device
1020 // coordinate systems.
1021 qint32 flipY = rhi->isYUpInFramebuffer() ? 0 : 1;
1022 if (rhi->isYUpInNDC())
1023 flipY = 1 - flipY;
1024
1025 char *ubufData = ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
1026 for (int subMeshIdx = 0; subMeshIdx != subMeshCount; ++subMeshIdx) {
1027 const SubMeshInfo &subMeshInfo(subMeshInfos[lmIdx][subMeshIdx]);
1028 qint32 hasBaseColorMap = subMeshInfo.baseColorMap ? 1 : 0;
1029 qint32 hasEmissiveMap = subMeshInfo.emissiveMap ? 1 : 0;
1030 qint32 hasNormalMap = subMeshInfo.normalMap && hasTangentAndBinormal ? 1 : 0;
1031 const float minRegionU = minUVRegion.x();
1032 const float minRegionV = minUVRegion.y();
1033 const float maxRegionU = maxUVRegion.x();
1034 const float maxRegionV = maxUVRegion.y();
1035 char *p = ubufData + subMeshIdx * alignedUbufSize;
1036 memcpy(p, &subMeshInfo.baseColor, 4 * sizeof(float));
1037 memcpy(p + 16, &subMeshInfo.emissiveFactor, 3 * sizeof(float));
1038 memcpy(p + 28, &flipY, sizeof(qint32));
1039 memcpy(p + 32, &hasBaseColorMap, sizeof(qint32));
1040 memcpy(p + 36, &hasEmissiveMap, sizeof(qint32));
1041 memcpy(p + 40, &hasNormalMap, sizeof(qint32));
1042 memcpy(p + 44, &subMeshInfo.normalStrength, sizeof(float));
1043 memcpy(p + 48, &minRegionU, sizeof(float));
1044 memcpy(p + 52, &minRegionV, sizeof(float));
1045 memcpy(p + 56, &maxRegionU, sizeof(float));
1046 memcpy(p + 60, &maxRegionV, sizeof(float));
1047 }
1048 ubuf->endFullDynamicBufferUpdateForCurrentFrame();
1049
1050 auto setupPipeline = [rhi, &rpDesc](QSSGRhiShaderPipeline *shaderPipeline,
1051 QRhiShaderResourceBindings *srb,
1052 const QRhiVertexInputLayout &inputLayout)
1053 {
1054 QRhiGraphicsPipeline *ps = rhi->newGraphicsPipeline();
1055 ps->setTopology(QRhiGraphicsPipeline::Triangles);
1056 ps->setDepthTest(true);
1057 ps->setDepthWrite(true);
1058 ps->setDepthOp(QRhiGraphicsPipeline::Less);
1059 ps->setShaderStages(shaderPipeline->cbeginStages(), shaderPipeline->cendStages());
1060 ps->setTargetBlends({ {}, {}, {}, {} });
1061 ps->setRenderPassDescriptor(rpDesc.get());
1062 ps->setVertexInputLayout(inputLayout);
1063 ps->setShaderResourceBindings(srb);
1064 return ps;
1065 };
1066
1067 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
1068 QVector<QRhiGraphicsPipeline *> ps;
1069 // Everything is going to be rendered twice (but note depth testing), first
1070 // with polygon mode fill, then line.
1071 QVector<QRhiGraphicsPipeline *> psLine;
1072
1073 for (int subMeshIdx = 0; subMeshIdx != subMeshCount; ++subMeshIdx) {
1074 const SubMeshInfo &subMeshInfo(subMeshInfos[lmIdx][subMeshIdx]);
1075 QVarLengthArray<QRhiVertexInputAttribute, 6> vertexAttrs;
1076 vertexAttrs << QRhiVertexInputAttribute(0, 0, bakeModelDrawInfo.positionFormat, bakeModelDrawInfo.positionOffset)
1077 << QRhiVertexInputAttribute(0, 1, bakeModelDrawInfo.normalFormat, bakeModelDrawInfo.normalOffset)
1078 << QRhiVertexInputAttribute(0, 2, bakeModelDrawInfo.lightmapUVFormat, bakeModelDrawInfo.lightmapUVOffset);
1079
1080 // Vertex inputs (just like the sampler uniforms) must match exactly on
1081 // the shader and the application side, cannot just leave out or have
1082 // unused inputs.
1083 QSSGBuiltInRhiShaderCache::LightmapUVRasterizationShaderMode shaderVariant = QSSGBuiltInRhiShaderCache::LightmapUVRasterizationShaderMode::Default;
1084 if (hasUV0) {
1085 shaderVariant = QSSGBuiltInRhiShaderCache::LightmapUVRasterizationShaderMode::Uv;
1086 if (hasTangentAndBinormal)
1087 shaderVariant = QSSGBuiltInRhiShaderCache::LightmapUVRasterizationShaderMode::UvTangent;
1088 }
1089
1090 const auto &shaderCache = renderer->contextInterface()->shaderCache();
1091 const auto &lmUvRastShaderPipeline = shaderCache->getBuiltInRhiShaders().getRhiLightmapUVRasterizationShader(shaderVariant);
1092 if (!lmUvRastShaderPipeline) {
1093 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load shaders"));
1094 return result;
1095 }
1096
1097 if (hasUV0) {
1098 vertexAttrs << QRhiVertexInputAttribute(0, 3, bakeModelDrawInfo.uvFormat, bakeModelDrawInfo.uvOffset);
1099 if (hasTangentAndBinormal) {
1100 vertexAttrs << QRhiVertexInputAttribute(0, 4, bakeModelDrawInfo.tangentFormat, bakeModelDrawInfo.tangentOffset);
1101 vertexAttrs << QRhiVertexInputAttribute(0, 5, bakeModelDrawInfo.binormalFormat, bakeModelDrawInfo.binormalOffset);
1102 }
1103 }
1104
1105 inputLayout.setAttributes(vertexAttrs.cbegin(), vertexAttrs.cend());
1106
1107 QSSGRhiShaderResourceBindingList bindings;
1108 bindings.addUniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, ubuf.get(),
1109 subMeshIdx * alignedUbufSize, UBUF_SIZE);
1110 QRhiSampler *dummySampler = rhiCtx->sampler({ QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
1111 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::Repeat });
1112 if (subMeshInfo.baseColorMap) {
1113 const bool mipmapped = subMeshInfo.baseColorMap->flags().testFlag(QRhiTexture::MipMapped);
1114 QRhiSampler *sampler = rhiCtx->sampler({ QSSGRhiHelpers::toRhi(subMeshInfo.baseColorNode->m_minFilterType),
1115 QSSGRhiHelpers::toRhi(subMeshInfo.baseColorNode->m_magFilterType),
1116 mipmapped ? QSSGRhiHelpers::toRhi(subMeshInfo.baseColorNode->m_mipFilterType) : QRhiSampler::None,
1117 QSSGRhiHelpers::toRhi(subMeshInfo.baseColorNode->m_horizontalTilingMode),
1118 QSSGRhiHelpers::toRhi(subMeshInfo.baseColorNode->m_verticalTilingMode),
1119 QSSGRhiHelpers::toRhi(subMeshInfo.baseColorNode->m_depthTilingMode)
1120 });
1121 bindings.addTexture(1, QRhiShaderResourceBinding::FragmentStage, subMeshInfo.baseColorMap, sampler);
1122 } else {
1123 bindings.addTexture(1, QRhiShaderResourceBinding::FragmentStage, dummyTexture, dummySampler);
1124 }
1125 if (subMeshInfo.emissiveMap) {
1126 const bool mipmapped = subMeshInfo.emissiveMap->flags().testFlag(QRhiTexture::MipMapped);
1127 QRhiSampler *sampler = rhiCtx->sampler({ QSSGRhiHelpers::toRhi(subMeshInfo.emissiveNode->m_minFilterType),
1128 QSSGRhiHelpers::toRhi(subMeshInfo.emissiveNode->m_magFilterType),
1129 mipmapped ? QSSGRhiHelpers::toRhi(subMeshInfo.emissiveNode->m_mipFilterType) : QRhiSampler::None,
1130 QSSGRhiHelpers::toRhi(subMeshInfo.emissiveNode->m_horizontalTilingMode),
1131 QSSGRhiHelpers::toRhi(subMeshInfo.emissiveNode->m_verticalTilingMode),
1132 QSSGRhiHelpers::toRhi(subMeshInfo.emissiveNode->m_depthTilingMode)
1133 });
1134 bindings.addTexture(2, QRhiShaderResourceBinding::FragmentStage, subMeshInfo.emissiveMap, sampler);
1135 } else {
1136 bindings.addTexture(2, QRhiShaderResourceBinding::FragmentStage, dummyTexture, dummySampler);
1137 }
1138 if (subMeshInfo.normalMap) {
1139 const bool mipmapped = subMeshInfo.normalMap->flags().testFlag(QRhiTexture::MipMapped);
1140 QRhiSampler *sampler = rhiCtx->sampler({ QSSGRhiHelpers::toRhi(subMeshInfo.normalMapNode->m_minFilterType),
1141 QSSGRhiHelpers::toRhi(subMeshInfo.normalMapNode->m_magFilterType),
1142 mipmapped ? QSSGRhiHelpers::toRhi(subMeshInfo.normalMapNode->m_mipFilterType) : QRhiSampler::None,
1143 QSSGRhiHelpers::toRhi(subMeshInfo.normalMapNode->m_horizontalTilingMode),
1144 QSSGRhiHelpers::toRhi(subMeshInfo.normalMapNode->m_verticalTilingMode),
1145 QSSGRhiHelpers::toRhi(subMeshInfo.normalMapNode->m_depthTilingMode)
1146 });
1147 bindings.addTexture(3, QRhiShaderResourceBinding::FragmentStage, subMeshInfo.normalMap, sampler);
1148 } else {
1149 bindings.addTexture(3, QRhiShaderResourceBinding::FragmentStage, dummyTexture, dummySampler);
1150 }
1151 QRhiShaderResourceBindings *srb = rhiCtxD->srb(bindings);
1152
1153 QRhiGraphicsPipeline *pipeline = setupPipeline(lmUvRastShaderPipeline.get(), srb, inputLayout);
1154 if (!pipeline->create()) {
1155 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create graphics pipeline (mesh %1 submesh %2)").
1156 arg(lmIdx).
1157 arg(subMeshIdx));
1158 qDeleteAll(ps);
1159 qDeleteAll(psLine);
1160 return result;
1161 }
1162 ps.append(pipeline);
1163 pipeline = setupPipeline(lmUvRastShaderPipeline.get(), srb, inputLayout);
1164 pipeline->setPolygonMode(QRhiGraphicsPipeline::Line);
1165 if (!pipeline->create()) {
1166 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create graphics pipeline with line fill mode (mesh %1 submesh %2)").
1167 arg(lmIdx).
1168 arg(subMeshIdx));
1169 qDeleteAll(ps);
1170 qDeleteAll(psLine);
1171 return result;
1172 }
1173 psLine.append(pipeline);
1174 }
1175
1176 QRhiCommandBuffer::VertexInput vertexBuffers = { vbuf.get(), 0 };
1177 const QRhiViewport viewport(0, 0, float(outputSize.width()), float(outputSize.height()));
1178 bool hadViewport = false;
1179
1180 cb->beginPass(rt.get(), Qt::black, { 1.0f, 0 });
1181 for (int subMeshIdx = 0; subMeshIdx != subMeshCount; ++subMeshIdx) {
1182 const SubMeshInfo &subMeshInfo(subMeshInfos[lmIdx][subMeshIdx]);
1183 cb->setGraphicsPipeline(ps[subMeshIdx]);
1184 if (!hadViewport) {
1185 cb->setViewport(viewport);
1186 hadViewport = true;
1187 }
1188 cb->setShaderResources();
1189 cb->setVertexInput(0, 1, &vertexBuffers, ibuf.get(), 0, QRhiCommandBuffer::IndexUInt32);
1190 cb->drawIndexed(subMeshInfo.count, 1, subMeshInfo.offset);
1191 cb->setGraphicsPipeline(psLine[subMeshIdx]);
1192 cb->setShaderResources();
1193 cb->drawIndexed(subMeshInfo.count, 1, subMeshInfo.offset);
1194 }
1195
1196 resUpd = rhi->nextResourceUpdateBatch();
1197 QRhiReadbackResult posReadResult;
1198 QRhiReadbackResult normalReadResult;
1199 QRhiReadbackResult baseColorReadResult;
1200 QRhiReadbackResult emissionReadResult;
1201 resUpd->readBackTexture({ positionData.get() }, &posReadResult);
1202 resUpd->readBackTexture({ normalData.get() }, &normalReadResult);
1203 resUpd->readBackTexture({ baseColorData.get() }, &baseColorReadResult);
1204 resUpd->readBackTexture({ emissionData.get() }, &emissionReadResult);
1205 cb->endPass(resUpd);
1206
1207 // Submit and wait for completion.
1208 rhi->finish();
1209
1210 qDeleteAll(ps);
1211 qDeleteAll(psLine);
1212
1213 const int numPixels = outputSize.width() * outputSize.height();
1214
1215 result.worldPositions.resize(numPixels);
1216 result.normals.resize(numPixels);
1217 result.baseColors.resize(numPixels);
1218 result.emissions.resize(numPixels);
1219
1220 // The readback results are tightly packed (which is supposed to be ensured
1221 // by each rhi backend), so one line is 16 * width bytes.
1222 if (posReadResult.data.size() < numPixels * 16) {
1223 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Position data is smaller than expected"));
1224 return result;
1225 }
1226 if (normalReadResult.data.size() < numPixels * 16) {
1227 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Normal data is smaller than expected"));
1228 return result;
1229 }
1230 if (baseColorReadResult.data.size() < numPixels * 16) {
1231 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Base color data is smaller than expected"));
1232 return result;
1233 }
1234 if (emissionReadResult.data.size() < numPixels * 16) {
1235 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Emission data is smaller than expected"));
1236 return result;
1237 }
1238
1239 result.success = true;
1240 result.width = outputSize.width();
1241 result.height = outputSize.height();
1242 result.worldPositions = posReadResult.data;
1243 result.normals = normalReadResult.data;
1244 result.baseColors = baseColorReadResult.data;
1245 result.emissions = emissionReadResult.data;
1246
1247 return result;
1248}
1249
1250bool QSSGLightmapperPrivate::prepareLightmaps()
1251{
1252 QRhi *rhi = rhiCtxInterface->rhiContext()->rhi();
1253 Q_ASSERT(rhi);
1254 if (!rhi->isTextureFormatSupported(QRhiTexture::RGBA32F)) {
1255 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("FP32 textures not supported, cannot bake"));
1256 return false;
1257 }
1258 if (rhi->resourceLimit(QRhi::MaxColorAttachments) < 4) {
1259 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Multiple render targets not supported, cannot bake"));
1260 return false;
1261 }
1262 if (!rhi->isFeatureSupported(QRhi::NonFillPolygonMode)) {
1263 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Line polygon mode not supported, cannot bake"));
1264 return false;
1265 }
1266
1267 sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Preparing lightmaps..."));
1268 QElapsedTimer lightmapPrepTimer;
1269 lightmapPrepTimer.start();
1270 const int bakedLightingModelCount = bakedLightingModels.size();
1271 Q_ASSERT(drawInfos.size() == bakedLightingModelCount);
1272 Q_ASSERT(subMeshInfos.size() == bakedLightingModelCount);
1273
1274 numValidTexels.resize(bakedLightingModelCount);
1275
1276 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
1277 QElapsedTimer rasterizeTimer;
1278 rasterizeTimer.start();
1279
1280 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
1281 const QSize lightmapSize = drawInfos[lmIdx].lightmapSize;
1282
1283 const int w = lightmapSize.width();
1284 const int h = lightmapSize.height();
1285 const int numPixels = w * h;
1286
1287 int unusedEntries = 0;
1288 QVector<ModelTexel> &texels = modelTexels[lmIdx];
1289 texels.resize(numPixels);
1290
1291 // Dynamically compute number of tiles so that each tile is <= MAX_TILE_SIZE
1292 constexpr int maxTileSize = MAX_TILE_SIZE;
1293 const int numTilesX = (w + maxTileSize - 1) / maxTileSize;
1294 const int numTilesY = (h + maxTileSize - 1) / maxTileSize;
1295
1296 bool isEmissive = false;
1297
1298 // Render tiled to make sure enough GPU memory is available
1299 for (int tileY = 0; tileY < numTilesY; ++tileY) {
1300 for (int tileX = 0; tileX < numTilesX; ++tileX) {
1301 // Compute actual tile size (may be less than maxTileSize on edges)
1302 const int startX = tileX * maxTileSize;
1303 const int startY = tileY * maxTileSize;
1304
1305 const int tileWidth = qMin(maxTileSize, w - startX);
1306 const int tileHeight = qMin(maxTileSize, h - startY);
1307
1308 const int endX = startX + tileWidth;
1309 const int endY = startY + tileHeight;
1310
1311 const float minU = startX / double(w);
1312 const float maxV = 1.0 - startY / double(h);
1313 const float maxU = endX / double(w);
1314 const float minV = 1.0 - endY / double(h);
1315
1316 QSSGLightmapperPrivate::RasterResult raster = rasterizeLightmap(lmIdx,
1317 QSize(tileWidth, tileHeight),
1318 QVector2D(minU, minV),
1319 QVector2D(maxU, maxV));
1320 if (!raster.success)
1321 return false;
1322
1323 QVector4D *worldPositions = reinterpret_cast<QVector4D *>(raster.worldPositions.data());
1324 QVector4D *normals = reinterpret_cast<QVector4D *>(raster.normals.data());
1325 QVector4D *baseColors = reinterpret_cast<QVector4D *>(raster.baseColors.data());
1326 QVector4D *emissions = reinterpret_cast<QVector4D *>(raster.emissions.data());
1327
1328 for (int y = startY; y < endY; ++y) {
1329 const int ySrc = y - startY;
1330 Q_ASSERT(ySrc < tileHeight);
1331 for (int x = startX; x < endX; ++x) {
1332 const int xSrc = x - startX;
1333 Q_ASSERT(xSrc < tileWidth);
1334
1335 const int dstPixelI = y * w + x;
1336 const int srcPixelI = ySrc * tileWidth + xSrc;
1337
1338 ModelTexel &lmPix(texels[dstPixelI]);
1339
1340 lmPix.worldPos = worldPositions[srcPixelI].toVector3D();
1341 lmPix.normal = normals[srcPixelI].toVector3D();
1342 if (lmPix.isValid())
1343 ++numValidTexels[lmIdx];
1344
1345 lmPix.baseColor = baseColors[srcPixelI];
1346 if (lmPix.baseColor[3] < 1.0f)
1347 modelHasBaseColorTransparency[lmIdx] = true;
1348
1349 lmPix.emission = emissions[srcPixelI].toVector3D();
1350 if (!isEmissive && !qFuzzyIsNull(lmPix.emission.length()))
1351 isEmissive = true;
1352 }
1353 }
1354 }
1355 }
1356
1357 if (isEmissive)
1358 ++emissiveModelCount;
1359
1360 sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
1361 QStringLiteral(
1362 "Successfully rasterized %1/%2 lightmap texels for model %3, lightmap size %4 in %5")
1363 .arg(QString::number(texels.size() - unusedEntries),
1364 QString::number(texels.size()),
1365 lm.model->lightmapKey,
1366 QStringLiteral("(%1, %2)").arg(QString::number(w), QString::number(h)),
1367 formatDuration(rasterizeTimer.elapsed())));
1368 for (const SubMeshInfo &subMeshInfo : std::as_const(subMeshInfos[lmIdx])) {
1369 if (!lm.model->castsShadows) // only matters if it's in the raytracer scene
1370 continue;
1371 geomLightmapMap[subMeshInfo.geomId] = lmIdx;
1372 }
1373 }
1374
1375 sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
1376 QStringLiteral("Lightmaps ready. Time taken: %1")
1377 .arg(formatDuration(lightmapPrepTimer.elapsed())));
1378 return true;
1379}
1380
1381bool QSSGLightmapperPrivate::verifyLights() const {
1382
1383 return !lights.empty() || emissiveModelCount > 0;
1384}
1385
1386bool QSSGLightmapper::setupLights(const QSSGRenderer &renderer)
1387{
1388 QSSGLayerRenderData *renderData = QSSGRendererPrivate::getCurrentRenderData(renderer);
1389 if (!renderData) {
1390 qWarning() << "lm: No render data, cannot bake lightmaps";
1391 return false;
1392 }
1393
1394 if (d->bakedLightingModels.isEmpty()) {
1395 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
1396 QStringLiteral("No models provided, cannot bake lightmaps"));
1397 return false;
1398 }
1399
1400 // All subsets for a model reference the same QSSGShaderLight list,
1401 // take the first one, but filter it based on the bake flag.
1402 // also tracks seenLights, as multiple models might reference the same lights.
1403 auto lights = static_cast<QSSGSubsetRenderable *>(d->bakedLightingModels.first().renderables.first().obj)->lights;
1404 for (const QSSGShaderLight &sl : lights) {
1405 if (!sl.light->m_bakingEnabled)
1406 continue;
1407
1408 QSSGLightmapperPrivate::Light light;
1409 light.indirectOnly = !sl.light->m_fullyBaked;
1410 light.direction = sl.direction;
1411
1412 const float brightness = sl.light->m_brightness;
1413 light.color = QVector3D(sl.light->m_diffuseColor.x() * brightness,
1414 sl.light->m_diffuseColor.y() * brightness,
1415 sl.light->m_diffuseColor.z() * brightness);
1416
1417 if (sl.light->type == QSSGRenderLight::Type::PointLight
1418 || sl.light->type == QSSGRenderLight::Type::SpotLight) {
1419 const QMatrix4x4 lightGlobalTransform = renderData->getGlobalTransform(*sl.light);
1420 light.worldPos = QSSGRenderNode::getGlobalPos(lightGlobalTransform);
1421 if (sl.light->type == QSSGRenderLight::Type::SpotLight) {
1422 light.type = QSSGLightmapperPrivate::Light::Spot;
1423 light.cosConeAngle = qCos(qDegreesToRadians(sl.light->m_coneAngle));
1424 light.cosInnerConeAngle = qCos(
1425 qDegreesToRadians(qMin(sl.light->m_innerConeAngle, sl.light->m_coneAngle)));
1426 } else {
1427 light.type = QSSGLightmapperPrivate::Light::Point;
1428 }
1429 light.constantAttenuation = QSSGUtils::aux::translateConstantAttenuation(
1430 sl.light->m_constantFade);
1431 light.linearAttenuation = QSSGUtils::aux::translateLinearAttenuation(
1432 sl.light->m_linearFade);
1433 light.quadraticAttenuation = QSSGUtils::aux::translateQuadraticAttenuation(
1434 sl.light->m_quadraticFade);
1435 } else {
1436 light.type = QSSGLightmapperPrivate::Light::Directional;
1437 }
1438
1439 d->lights.append(light);
1440 }
1441
1442 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
1443 QStringLiteral("Total lights registered: %1").arg(d->lights.size()));
1444
1445 return true;
1446}
1447
1448
1449struct RayHit
1450{
1451 RayHit(const QVector3D &org, const QVector3D &dir, float tnear = 0.0f, float tfar = std::numeric_limits<float>::infinity()) {
1452 rayhit.ray.org_x = org.x();
1453 rayhit.ray.org_y = org.y();
1454 rayhit.ray.org_z = org.z();
1455 rayhit.ray.dir_x = dir.x();
1456 rayhit.ray.dir_y = dir.y();
1457 rayhit.ray.dir_z = dir.z();
1458 rayhit.ray.tnear = tnear;
1459 rayhit.ray.tfar = tfar;
1460 rayhit.hit.u = 0.0f;
1461 rayhit.hit.v = 0.0f;
1462 rayhit.hit.geomID = RTC_INVALID_GEOMETRY_ID;
1463 }
1464
1465 RTCRayHit rayhit;
1466
1467 bool intersect(RTCScene scene)
1468 {
1469 RTCIntersectContext ctx;
1470 rtcInitIntersectContext(&ctx);
1471 rtcIntersect1(scene, &ctx, &rayhit);
1472 return rayhit.hit.geomID != RTC_INVALID_GEOMETRY_ID;
1473 }
1474};
1475
1476static inline QVector3D vectorSign(const QVector3D &v)
1477{
1478 return QVector3D(v.x() < 1.0f ? -1.0f : 1.0f,
1479 v.y() < 1.0f ? -1.0f : 1.0f,
1480 v.z() < 1.0f ? -1.0f : 1.0f);
1481}
1482
1483static inline QVector3D vectorAbs(const QVector3D &v)
1484{
1485 return QVector3D(std::abs(v.x()),
1486 std::abs(v.y()),
1487 std::abs(v.z()));
1488}
1489
1490// Function to apply a Gaussian blur to an image
1491QList<QVector3D> applyGaussianBlur(const QList<QVector3D>& image, const QList<quint32>& mask, int width, int height, float sigma) {
1492 // Create a Gaussian kernel
1493 constexpr int halfKernelSize = GAUSS_HALF_KERNEL_SIZE;
1494 constexpr int kernelSize = halfKernelSize * 2 + 1;
1495
1496 double sum = 0.0;
1497 double kernel[kernelSize][kernelSize];
1498 double mean = halfKernelSize;
1499 for (int y = 0; y < kernelSize; ++y) {
1500 for (int x = 0; x < kernelSize; ++x) {
1501 kernel[y][x] = exp(-0.5 * (pow((x - mean) / sigma, 2.0) + pow((y - mean) / sigma, 2.0))) / (2 * M_PI * sigma * sigma);
1502
1503 // Accumulate the kernel values
1504 sum += kernel[y][x];
1505 }
1506 }
1507
1508 // Normalize the kernel
1509 for (int x = 0; x < kernelSize; ++x)
1510 for (int y = 0; y < kernelSize; ++y)
1511 kernel[y][x] /= sum;
1512
1513 // Create a copy of the image for the output
1514 QList<QVector3D> output(image.size(), QVector3D(0, 0, 0));
1515
1516 // Apply the kernel to each pixel
1517 for (int y = 0; y < height; ++y) {
1518 for (int x = 0; x < width; ++x) {
1519 const int centerIdx = y * width + x;
1520 const quint32 maskID = mask[centerIdx];
1521 if (maskID == PIXEL_VOID)
1522 continue;
1523
1524 QVector3D blurredPixel(0, 0, 0);
1525 float weightSum = 0.0f;
1526
1527 // Convolve the kernel with the image
1528 for (int ky = -halfKernelSize; ky <= halfKernelSize; ++ky) {
1529 for (int kx = -halfKernelSize; kx <= halfKernelSize; ++kx) {
1530 int px = x + kx;
1531 int py = y + ky;
1532 if (px < 0 || px >= width || py < 0 || py >= height)
1533 continue;
1534
1535 int idx = py * width + px;
1536 if (mask[idx] != maskID)
1537 continue;
1538
1539 double weight = kernel[ky + halfKernelSize][kx + halfKernelSize];
1540 blurredPixel += image[idx] * weight;
1541 weightSum += weight;
1542 }
1543 }
1544
1545 // Normalize if needed to avoid darkening near edges
1546 if (weightSum > 0.0f)
1547 blurredPixel /= weightSum;
1548
1549 output[centerIdx] = blurredPixel;
1550 }
1551 }
1552
1553 return output;
1554}
1555
1556struct Edge
1557{
1558 std::array<QVector3D, 2> pos;
1559 std::array<QVector3D, 2> normal;
1560};
1561
1562inline bool operator==(const Edge &a, const Edge &b)
1563{
1564 return qFuzzyCompare(a.pos[0], b.pos[0]) && qFuzzyCompare(a.pos[1], b.pos[1])
1565 && qFuzzyCompare(a.normal[0], b.normal[0]) && qFuzzyCompare(a.normal[1], b.normal[1]);
1566}
1567
1568inline size_t qHash(const Edge &e, size_t seed) Q_DECL_NOTHROW
1569{
1570 return qHash(e.pos[0].x(), seed) ^ qHash(e.pos[0].y()) ^ qHash(e.pos[0].z()) ^ qHash(e.pos[1].x())
1571 ^ qHash(e.pos[1].y()) ^ qHash(e.pos[1].z());
1572}
1573
1574struct EdgeUV
1575{
1576 std::array<QVector2D, 2> uv;
1577 bool seam = false;
1578};
1579
1580struct SeamUV
1581{
1582 std::array<std::array<QVector2D, 2>, 2> uv;
1583};
1584
1585static inline bool vectorLessThan(const QVector3D &a, const QVector3D &b)
1586{
1587 if (a.x() == b.x()) {
1588 if (a.y() == b.y())
1589 return a.z() < b.z();
1590 else
1591 return a.y() < b.y();
1592 }
1593 return a.x() < b.x();
1594}
1595
1596static inline float floatSign(float f)
1597{
1598 return f > 0.0f ? 1.0f : (f < 0.0f ? -1.0f : 0.0f);
1599}
1600
1601static inline QVector2D flooredVec(const QVector2D &v)
1602{
1603 return QVector2D(std::floor(v.x()), std::floor(v.y()));
1604}
1605
1606static inline QVector2D projectPointToLine(const QVector2D &point, const std::array<QVector2D, 2> &line)
1607{
1608 const QVector2D p = point - line[0];
1609 const QVector2D n = line[1] - line[0];
1610 const float lengthSquared = n.lengthSquared();
1611 if (!qFuzzyIsNull(lengthSquared)) {
1612 const float d = (n.x() * p.x() + n.y() * p.y()) / lengthSquared;
1613 return d <= 0.0f ? line[0] : (d >= 1.0f ? line[1] : line[0] + n * d);
1614 }
1615 return line[0];
1616}
1617
1618static void blendLine(const QVector2D &from,
1619 const QVector2D &to,
1620 const QVector2D &uvFrom,
1621 const QVector2D &uvTo,
1622 const float *readBuf,
1623 float *writeBuf,
1624 const QSize &lightmapPixelSize,
1625 const int stride = 4)
1626{
1627 const QVector2D size(lightmapPixelSize.width(), lightmapPixelSize.height());
1628 const std::array<QVector2D, 2> line = { QVector2D(from.x(), 1.0f - from.y()) * size, QVector2D(to.x(), 1.0f - to.y()) * size };
1629 const float lineLength = line[0].distanceToPoint(line[1]);
1630 if (qFuzzyIsNull(lineLength))
1631 return;
1632
1633 const QVector2D startPixel = flooredVec(line[0]);
1634 const QVector2D endPixel = flooredVec(line[1]);
1635
1636 const QVector2D dir = (line[1] - line[0]).normalized();
1637 const QVector2D tStep(1.0f / std::abs(dir.x()), 1.0f / std::abs(dir.y()));
1638 const QVector2D pixelStep(floatSign(dir.x()), floatSign(dir.y()));
1639
1640 QVector2D nextT(std::fmod(line[0].x(), 1.0f), std::fmod(line[0].y(), 1.0f));
1641 if (pixelStep.x() == 1.0f)
1642 nextT.setX(1.0f - nextT.x());
1643 if (pixelStep.y() == 1.0f)
1644 nextT.setY(1.0f - nextT.y());
1645
1646 if (!qFuzzyIsNull(dir.x()))
1647 nextT.setX(nextT.x() / std::abs(dir.x()));
1648 else
1649 nextT.setX(std::numeric_limits<float>::max());
1650
1651 if (!qFuzzyIsNull(dir.y()))
1652 nextT.setY(nextT.y() / std::abs(dir.y()));
1653 else
1654 nextT.setY(std::numeric_limits<float>::max());
1655
1656 QVector2D pixel = startPixel;
1657
1658 const auto clampedXY = [s = lightmapPixelSize](QVector2D xy) -> std::array<int, 2> {
1659 return { qBound(0, int(xy.x()), s.width() - 1), qBound(0, int(xy.y()), s.height() - 1) };
1660 };
1661
1662 while (startPixel.distanceToPoint(pixel) < lineLength + 1.0f) {
1663 const QVector2D point = projectPointToLine(pixel + QVector2D(0.5f, 0.5f), line);
1664 const float t = line[0].distanceToPoint(point) / lineLength;
1665 const QVector2D uvInterp = uvFrom * (1.0 - t) + uvTo * t;
1666 const auto sampledPixelXY = clampedXY(flooredVec(QVector2D(uvInterp.x(), 1.0f - uvInterp.y()) * size));
1667 const int sampOfs = (sampledPixelXY[0] + sampledPixelXY[1] * lightmapPixelSize.width()) * stride;
1668 const QVector3D sampledColor(readBuf[sampOfs], readBuf[sampOfs + 1], readBuf[sampOfs + 2]);
1669 const auto pixelXY = clampedXY(pixel);
1670 const int pixOfs = (pixelXY[0] + pixelXY[1] * lightmapPixelSize.width()) * stride;
1671 QVector3D currentColor(writeBuf[pixOfs], writeBuf[pixOfs + 1], writeBuf[pixOfs + 2]);
1672 currentColor = currentColor * 0.6f + sampledColor * 0.4f;
1673 writeBuf[pixOfs] = currentColor.x();
1674 writeBuf[pixOfs + 1] = currentColor.y();
1675 writeBuf[pixOfs + 2] = currentColor.z();
1676
1677 if (pixel != endPixel) {
1678 if (nextT.x() < nextT.y()) {
1679 pixel.setX(pixel.x() + pixelStep.x());
1680 nextT.setX(nextT.x() + tStep.x());
1681 } else {
1682 pixel.setY(pixel.y() + pixelStep.y());
1683 nextT.setY(nextT.y() + tStep.y());
1684 }
1685 } else {
1686 break;
1687 }
1688 }
1689}
1690
1691QVector3D QSSGLightmapperPrivate::sampleDirectLight(QVector3D worldPos, QVector3D normal, bool allLight) const
1692{
1693 QVector3D directLight = QVector3D(0.f, 0.f, 0.f);
1694
1695 if (options.useAdaptiveBias)
1696 worldPos += vectorSign(normal) * vectorAbs(worldPos * 0.0000002f);
1697
1698 // 'lights' should have all lights that are either BakeModeIndirect or BakeModeAll
1699 for (const Light &light : lights) {
1700 if (light.indirectOnly && !allLight)
1701 continue;
1702
1703 QVector3D lightWorldPos;
1704 float dist = std::numeric_limits<float>::infinity();
1705 float attenuation = 1.0f;
1706 if (light.type == Light::Directional) {
1707 lightWorldPos = worldPos - light.direction;
1708 } else {
1709 lightWorldPos = light.worldPos;
1710 dist = (worldPos - lightWorldPos).length();
1711 attenuation = 1.0f
1712 / (light.constantAttenuation + light.linearAttenuation * dist + light.quadraticAttenuation * dist * dist);
1713 if (light.type == Light::Spot) {
1714 const float spotAngle = QVector3D::dotProduct((worldPos - lightWorldPos).normalized(), light.direction.normalized());
1715 if (spotAngle > light.cosConeAngle) {
1716 // spotFactor = smoothstep(light.cosConeAngle, light.cosInnerConeAngle, spotAngle);
1717 const float edge0 = light.cosConeAngle;
1718 const float edge1 = light.cosInnerConeAngle;
1719 const float x = spotAngle;
1720 const float t = qBound(0.0f, (x - edge0) / (edge1 - edge0), 1.0f);
1721 const float spotFactor = t * t * (3.0f - 2.0f * t);
1722 attenuation *= spotFactor;
1723 } else {
1724 attenuation = 0.0f;
1725 }
1726 }
1727 }
1728
1729 const QVector3D L = (lightWorldPos - worldPos).normalized();
1730 const float energy = qMax(0.0f, QVector3D::dotProduct(normal, L)) * attenuation;
1731 if (qFuzzyIsNull(energy))
1732 continue;
1733
1734 // trace a ray from this point towards the light, and see if something is hit on the way
1735 RayHit ray(worldPos, L, options.bias, dist);
1736 const bool lightReachable = !ray.intersect(rscene);
1737 if (lightReachable) {
1738 directLight += light.color * energy;
1739 }
1740 }
1741
1742 return directLight;
1743}
1744
1745QByteArray QSSGLightmapperPrivate::dilate(const QSize &pixelSize, const QByteArray &image)
1746{
1747 QSSGRhiContext *rhiCtx = rhiCtxInterface->rhiContext().get();
1748 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
1749 QRhi *rhi = rhiCtx->rhi();
1750 QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
1751
1752 const QRhiViewport viewport(0, 0, float(pixelSize.width()), float(pixelSize.height()));
1753
1754 std::unique_ptr<QRhiTexture> lightmapTex(rhi->newTexture(QRhiTexture::RGBA32F, pixelSize));
1755 if (!lightmapTex->create()) {
1756 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for postprocessing"));
1757 return {};
1758 }
1759 std::unique_ptr<QRhiTexture> dilatedLightmapTex(
1760 rhi->newTexture(QRhiTexture::RGBA32F, pixelSize, 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
1761 if (!dilatedLightmapTex->create()) {
1762 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning,
1763 QStringLiteral("Failed to create FP32 dest. texture for postprocessing"));
1764 return {};
1765 }
1766 QRhiTextureRenderTargetDescription rtDescDilate(dilatedLightmapTex.get());
1767 std::unique_ptr<QRhiTextureRenderTarget> rtDilate(rhi->newTextureRenderTarget(rtDescDilate));
1768 std::unique_ptr<QRhiRenderPassDescriptor> rpDescDilate(rtDilate->newCompatibleRenderPassDescriptor());
1769 rtDilate->setRenderPassDescriptor(rpDescDilate.get());
1770 if (!rtDilate->create()) {
1771 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning,
1772 QStringLiteral("Failed to create postprocessing texture render target"));
1773 return {};
1774 }
1775 QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
1776 QRhiTextureSubresourceUploadDescription lightmapTexUpload(image.constData(), image.size());
1777 resUpd->uploadTexture(lightmapTex.get(), QRhiTextureUploadDescription({ 0, 0, lightmapTexUpload }));
1778 QSSGRhiShaderResourceBindingList bindings;
1779 QRhiSampler *nearestSampler = rhiCtx->sampler(
1780 { QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::Repeat });
1781 bindings.addTexture(0, QRhiShaderResourceBinding::FragmentStage, lightmapTex.get(), nearestSampler);
1782 renderer->rhiQuadRenderer()->prepareQuad(rhiCtx, resUpd);
1783 const auto &shaderCache = renderer->contextInterface()->shaderCache();
1784 const auto &lmDilatePipeline = shaderCache->getBuiltInRhiShaders().getRhiLightmapDilateShader();
1785 if (!lmDilatePipeline) {
1786 sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load shaders"));
1787 return {};
1788 }
1789 QSSGRhiGraphicsPipelineState dilatePs;
1790 dilatePs.viewport = viewport;
1791 QSSGRhiGraphicsPipelineStatePrivate::setShaderPipeline(dilatePs, lmDilatePipeline.get());
1792 renderer->rhiQuadRenderer()->recordRenderQuadPass(rhiCtx, &dilatePs, rhiCtxD->srb(bindings), rtDilate.get(), QSSGRhiQuadRenderer::UvCoords);
1793 resUpd = rhi->nextResourceUpdateBatch();
1794 QRhiReadbackResult dilateReadResult;
1795 resUpd->readBackTexture({ dilatedLightmapTex.get() }, &dilateReadResult);
1796 cb->resourceUpdate(resUpd);
1797
1798 // Submit and wait for completion.
1799 rhi->finish();
1800
1801 return dilateReadResult.data;
1802}
1803
1804QVector<QVector3D> QSSGLightmapperPrivate::computeDirectLight(int lmIdx)
1805{
1806 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
1807
1808 // While Light.castsShadow and Model.receivesShadows are irrelevant for
1809 // baked lighting (they are effectively ignored, shadows are always
1810 // there with baked direct lighting), Model.castsShadows is something
1811 // we can and should take into account.
1812 if (!lm.model->castsShadows)
1813 return {};
1814
1815 const DrawInfo &drawInfo(drawInfos[lmIdx]);
1816 const char *vbase = drawInfo.vertexData.constData();
1817 const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
1818
1819 const QSize sz = drawInfo.lightmapSize;
1820 const int w = sz.width();
1821 const int h = sz.height();
1822 constexpr int padding = GAUSS_HALF_KERNEL_SIZE;
1823 const int numPixelsFinal = w * h;
1824
1825 QVector<QVector3D> grid(numPixelsFinal);
1826 QVector<quint32> mask(numPixelsFinal, PIXEL_VOID);
1827
1828 // Setup grid and mask
1829 const QVector<ModelTexel>& texels = modelTexels[lmIdx];
1830 for (int pixelI = 0; pixelI < numPixelsFinal; ++pixelI) {
1831 const auto &entry = texels[pixelI];
1832 if (!entry.isValid())
1833 continue;
1834 mask[pixelI] = PIXEL_UNSET;
1835 grid[pixelI] = sampleDirectLight(entry.worldPos, entry.normal, false);
1836 }
1837
1838 if (std::all_of(grid.begin(), grid.end(), [](const QVector3D &v) { return v.isNull(); })) {
1839 return grid; // All black, meaning no lights hit or all are indirectOnly.
1840 }
1841
1842 floodFill(reinterpret_cast<quint32 *>(mask.data()), h, w);
1843
1844 // Dynamically compute number of tiles so that each tile is <= MAX_TILE_SIZE
1845 constexpr int maxTileSize = MAX_TILE_SIZE / DIRECT_MAP_UPSCALE_FACTOR;
1846 const int numTilesX = (w + maxTileSize - 1) / maxTileSize;
1847 const int numTilesY = (h + maxTileSize - 1) / maxTileSize;
1848
1849 // Render upscaled tiles then blur and downscale to remove jaggies in output
1850 for (int tileY = 0; tileY < numTilesY; ++tileY) {
1851 for (int tileX = 0; tileX < numTilesX; ++tileX) {
1852 // Compute actual tile size (may be less than maxTileSize on edges)
1853 const int startX = tileX * maxTileSize;
1854 const int startY = tileY * maxTileSize;
1855
1856 const int tileWidth = qMin(maxTileSize, w - startX);
1857 const int tileHeight = qMin(maxTileSize, h - startY);
1858
1859 const int currentTileWidth = tileWidth + 2 * padding;
1860 const int currentTileHeight = tileHeight + 2 * padding;
1861
1862 const int wExp = currentTileWidth * DIRECT_MAP_UPSCALE_FACTOR;
1863 const int hExp = currentTileHeight * DIRECT_MAP_UPSCALE_FACTOR;
1864 const int numPixelsExpanded = wExp * hExp;
1865
1866 QVector<quint32> maskTile(numPixelsExpanded, PIXEL_VOID);
1867 QVector<QVector3D> gridTile(numPixelsExpanded);
1868
1869 // Compute full-padded pixel bounds (including kernel padding)
1870 const int pixelStartX = startX - padding;
1871 const int pixelStartY = startY - padding;
1872 const int pixelEndX = startX + tileWidth + padding;
1873 const int pixelEndY = startY + tileHeight + padding;
1874
1875 const float minU = pixelStartX / double(w);
1876 const float maxV = 1.0 - pixelStartY / double(h);
1877 const float maxU = pixelEndX / double(w);
1878 const float minV = 1.0 - pixelEndY / double(h);
1879
1880 // Temporary storage for rasterized, avoids copy
1881 QByteArray worldPositionsBuffer;
1882 QByteArray normalsBuffer;
1883 {
1884 QSSGLightmapperPrivate::RasterResult raster = rasterizeLightmap(lmIdx,
1885 QSize(wExp, hExp),
1886 QVector2D(minU, minV),
1887 QVector2D(maxU, maxV));
1888 if (!raster.success)
1889 return {};
1890 Q_ASSERT(raster.width * raster.height == numPixelsExpanded);
1891 worldPositionsBuffer = raster.worldPositions;
1892 normalsBuffer = raster.normals;
1893 }
1894
1895 QVector4D *worldPositions = reinterpret_cast<QVector4D *>(worldPositionsBuffer.data());
1896 QVector4D *normals = reinterpret_cast<QVector4D *>(normalsBuffer.data());
1897
1898 for (int pixelI = 0; pixelI < numPixelsExpanded; ++pixelI) {
1899 QVector3D position = worldPositions[pixelI].toVector3D();
1900 QVector3D normal = normals[pixelI].toVector3D();
1901 if (normal.isNull()) {
1902 maskTile[pixelI] = PIXEL_VOID;
1903 continue;
1904 }
1905
1906 maskTile[pixelI] = PIXEL_UNSET;
1907 gridTile[pixelI] += sampleDirectLight(position, normal, false);
1908 }
1909
1910 floodFill(reinterpret_cast<quint32 *>(maskTile.data()), hExp, wExp); // Flood fill mask in place
1911 gridTile = applyGaussianBlur(gridTile, maskTile, wExp, hExp, 3.f);
1912
1913 const int endX = qMin(w, startX + tileWidth);
1914 const int endY = qMin(h, startY + tileHeight);
1915
1916 // Downscale and put in the finished grid
1917 // Loop through each pixel in the output image
1918 for (int y = startY; y < endY; ++y) {
1919 const int ySrc = (padding + y - startY) * DIRECT_MAP_UPSCALE_FACTOR;
1920 Q_ASSERT(ySrc < hExp);
1921 for (int x = startX; x < endX; ++x) {
1922 const int xSrc = (padding + x - startX) * DIRECT_MAP_UPSCALE_FACTOR;
1923 Q_ASSERT(xSrc < wExp);
1924
1925 if (mask[y * w + x] == PIXEL_VOID)
1926 continue;
1927
1928 const int dstPixelI = y * w + x;
1929 QVector3D average;
1930 int hits = 0;
1931 for (int sY = 0; sY < DIRECT_MAP_UPSCALE_FACTOR; ++sY) {
1932 for (int sX = 0; sX < DIRECT_MAP_UPSCALE_FACTOR; ++sX) {
1933 int srcPixelI = (ySrc + sY) * wExp + (xSrc + sX);
1934 Q_ASSERT(srcPixelI < numPixelsExpanded);
1935 if (maskTile[srcPixelI] == PIXEL_VOID)
1936 continue;
1937 average += gridTile[srcPixelI];
1938 ++hits;
1939 }
1940 }
1941
1942 // Write value only if we have any hits. Due to sampling and precision differences it is
1943 // technically possible to miss hits. In this case we fallback to the original sampled value.
1944 if (hits > 0)
1945 grid[dstPixelI] = average / hits;
1946 }
1947 }
1948
1949 // Update progress tracker
1950 progressTracker.directTileDone();
1951 }
1952 }
1953
1954 QHash<Edge, EdgeUV> edgeUVMap;
1955 QVector<SeamUV> seams;
1956
1957 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
1958 QVector<std::array<quint32, 3>> triangles;
1959 QVector<QVector3D> positions;
1960 QVector<QVector3D> normals;
1961 QVector<QVector2D> uvs;
1962
1963 triangles.reserve(subMeshInfo.count / 3);
1964 positions.reserve(subMeshInfo.count);
1965 normals.reserve(subMeshInfo.count);
1966 uvs.reserve(subMeshInfo.count);
1967
1968 for (quint32 i = 0; i < subMeshInfo.count / 3; ++i)
1969 triangles.push_back({ i * 3, i * 3 + 1, i * 3 + 2 });
1970
1971 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
1972 const quint32 idx = *(ibase + subMeshInfo.offset + i);
1973 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
1974 float x = *src++;
1975 float y = *src++;
1976 float z = *src++;
1977 positions.push_back(QVector3D(x, y, z));
1978 }
1979
1980 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
1981 const quint32 idx = *(ibase + subMeshInfo.offset + i);
1982 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
1983 float x = *src++;
1984 float y = *src++;
1985 float z = *src++;
1986 normals.push_back(QVector3D(x, y, z));
1987 }
1988
1989 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
1990 const quint32 idx = *(ibase + subMeshInfo.offset + i);
1991 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
1992 float x = *src++;
1993 float y = *src++;
1994 uvs.push_back(QVector2D(x, 1.0f - y)); // NOTE: Flip y
1995 }
1996
1997 for (const auto [i0, i1, i2] : std::as_const(triangles)) {
1998 const QVector3D triVert[3] = { positions[i0], positions[i1], positions[i2] };
1999 const QVector3D triNorm[3] = { normals[i0], normals[i1], normals[i2] };
2000 const QVector2D triUV[3] = { uvs[i0], uvs[i1], uvs[i2] };
2001
2002 for (int i = 0; i < 3; ++i) {
2003 int i0 = i;
2004 int i1 = (i + 1) % 3;
2005 if (vectorLessThan(triVert[i1], triVert[i0]))
2006 std::swap(i0, i1);
2007
2008 const Edge e = { { triVert[i0], triVert[i1] }, { triNorm[i0], triNorm[i1] } };
2009 const EdgeUV edgeUV = { { triUV[i0], triUV[i1] } };
2010 auto it = edgeUVMap.find(e);
2011 if (it == edgeUVMap.end()) {
2012 edgeUVMap.insert(e, edgeUV);
2013 } else if (!qFuzzyCompare(it->uv[0], edgeUV.uv[0]) || !qFuzzyCompare(it->uv[1], edgeUV.uv[1])) {
2014 if (!it->seam) {
2015 std::array<QVector2D, 2> eUV = {QVector2D(edgeUV.uv[0][0], 1.0f - edgeUV.uv[0][1]), QVector2D(edgeUV.uv[1][0], 1.0f - edgeUV.uv[1][1])};
2016 std::array<QVector2D, 2> itUV = {QVector2D(it->uv[0][0], 1.0f - it->uv[0][1]), QVector2D(it->uv[1][0], 1.0f - it->uv[1][1])};
2017
2018 seams.append(SeamUV({ { eUV, itUV } }));
2019 it->seam = true;
2020 }
2021 }
2022 }
2023 }
2024 }
2025
2026 // Blend edges
2027 // NOTE: We only need to blend grid since that is the resulting lightmap for direct light
2028 {
2029 QByteArray workBuf(grid.size() * sizeof(QVector3D), Qt::Uninitialized);
2030 for (int blendIter = 0; blendIter < LM_SEAM_BLEND_ITER_COUNT; ++blendIter) {
2031 memcpy(workBuf.data(), grid.constData(), grid.size() * sizeof(QVector3D));
2032 for (int seamIdx = 0, end = seams.size(); seamIdx != end; ++seamIdx) {
2033 const SeamUV &seam(seams[seamIdx]);
2034 blendLine(seam.uv[0][0],
2035 seam.uv[0][1],
2036 seam.uv[1][0],
2037 seam.uv[1][1],
2038 reinterpret_cast<const float *>(workBuf.data()),
2039 reinterpret_cast<float *>(grid.data()),
2040 QSize(w, h),
2041 3);
2042 blendLine(seam.uv[1][0],
2043 seam.uv[1][1],
2044 seam.uv[0][0],
2045 seam.uv[0][1],
2046 reinterpret_cast<const float *>(workBuf.data()),
2047 reinterpret_cast<float *>(grid.data()),
2048 QSize(w, h),
2049 3);
2050 }
2051 }
2052 }
2053
2054 return grid;
2055}
2056
2057// xorshift rng. this is called a lot -> rand/QRandomGenerator is out of question (way too slow)
2058static inline float uniformRand(quint32 &state)
2059{
2060 state ^= state << 13;
2061 state ^= state >> 17;
2062 state ^= state << 5;
2063 return float(state) / float(UINT32_MAX);
2064}
2065
2066static inline QVector3D cosWeightedHemisphereSample(quint32 &state)
2067{
2068 const float r1 = uniformRand(state);
2069 const float r2 = uniformRand(state) * 2.0f * float(M_PI);
2070 const float sqr1 = std::sqrt(r1);
2071 const float sqr1m = std::sqrt(1.0f - r1);
2072 return QVector3D(sqr1 * std::cos(r2), sqr1 * std::sin(r2), sqr1m);
2073}
2074
2075QVector<QVector3D> QSSGLightmapperPrivate::computeIndirectLight(int lmIdx, int wgCount, int wgSizePerGroup)
2076{
2077 const QVector<ModelTexel>& texels = modelTexels[lmIdx];
2078 QVector<QVector3D> result;
2079 result.resize(texels.size());
2080
2081 QVector<QFuture<QVector3D>> wg(wgCount);
2082
2083 for (int i = 0; i < texels.size(); ++i) {
2084 const ModelTexel& lmPix = texels[i];
2085 if (!lmPix.isValid())
2086 continue;
2087
2088 ++indirectTexelsDone;
2089 for (int wgIdx = 0; wgIdx < wgCount; ++wgIdx) {
2090 const int beginIdx = wgIdx * wgSizePerGroup;
2091 const int endIdx = qMin(beginIdx + wgSizePerGroup, options.indirectLightSamples);
2092
2093 wg[wgIdx] = QtConcurrent::run([this, wgIdx, beginIdx, endIdx, &lmPix] {
2094 QVector3D wgResult;
2095 quint32 state = QRandomGenerator(wgIdx).generate();
2096 for (int sampleIdx = beginIdx; sampleIdx < endIdx; ++sampleIdx) {
2097 QVector3D position = lmPix.worldPos;
2098 QVector3D normal = lmPix.normal;
2099 QVector3D throughput(1.0f, 1.0f, 1.0f);
2100 QVector3D sampleResult;
2101
2102 for (int bounce = 0; bounce < options.indirectLightBounces; ++bounce) {
2103 if (options.useAdaptiveBias)
2104 position += vectorSign(normal) * vectorAbs(position * 0.0000002f);
2105
2106 // get a sample using a cosine-weighted hemisphere sampler
2107 const QVector3D sample = cosWeightedHemisphereSample(state);
2108
2109 // transform to the point's local coordinate system
2110 const QVector3D v0 = qFuzzyCompare(qAbs(normal.z()), 1.0f)
2111 ? QVector3D(0.0f, 1.0f, 0.0f)
2112 : QVector3D(0.0f, 0.0f, 1.0f);
2113 const QVector3D tangent = QVector3D::crossProduct(v0, normal).normalized();
2114 const QVector3D bitangent = QVector3D::crossProduct(tangent, normal).normalized();
2115 QVector3D direction(
2116 tangent.x() * sample.x() + bitangent.x() * sample.y() + normal.x() * sample.z(),
2117 tangent.y() * sample.x() + bitangent.y() * sample.y() + normal.y() * sample.z(),
2118 tangent.z() * sample.x() + bitangent.z() * sample.y() + normal.z() * sample.z());
2119 direction.normalize();
2120
2121 // probability distribution function
2122 const float NdotL = qMax(0.0f, QVector3D::dotProduct(normal, direction));
2123 const float pdf = NdotL / float(M_PI);
2124 if (qFuzzyIsNull(pdf))
2125 break;
2126
2127 // shoot ray, stop if no hit
2128 RayHit ray(position, direction, options.bias);
2129 if (!ray.intersect(rscene))
2130 break;
2131
2132 // see what (sub)mesh and which texel it intersected with
2133 const ModelTexel &hitEntry = texelForLightmapUV(ray.rayhit.hit.geomID,
2134 ray.rayhit.hit.u,
2135 ray.rayhit.hit.v);
2136
2137 // won't bounce further from a back face
2138 const bool hitBackFace = QVector3D::dotProduct(hitEntry.normal, direction) > 0.0f;
2139 if (hitBackFace)
2140 break;
2141
2142 // the BRDF of a diffuse surface is albedo / PI
2143 const QVector3D brdf = hitEntry.baseColor.toVector3D() / float(M_PI);
2144
2145 // calculate result for this bounce
2146 sampleResult += throughput * hitEntry.emission;
2147 throughput *= brdf * NdotL / pdf;
2148 QVector3D directLight = sampleDirectLight(hitEntry.worldPos, hitEntry.normal, true);
2149 sampleResult += throughput * directLight;
2150
2151 // stop if we guess there's no point in bouncing further
2152 // (low throughput path wouldn't contribute much)
2153 const float p = qMax(qMax(throughput.x(), throughput.y()), throughput.z());
2154 if (p < uniformRand(state))
2155 break;
2156
2157 // was not terminated: boost the energy by the probability to be terminated
2158 throughput /= p;
2159
2160 // next bounce starts from the hit's position
2161 position = hitEntry.worldPos;
2162 normal = hitEntry.normal;
2163 }
2164
2165 wgResult += sampleResult;
2166 }
2167 return wgResult;
2168 });
2169 }
2170
2171 QVector3D totalIndirect;
2172 for (const auto &future : wg)
2173 totalIndirect += future.result();
2174
2175 result[i] += totalIndirect * options.indirectLightFactor / options.indirectLightSamples;
2176
2177 if (bakingControl.cancelled)
2178 return {};
2179
2180 progressTracker.indirectTexelDone(indirectTexelsDone, indirectTexelsTotal);
2181 }
2182
2183 return result;
2184}
2185
2186static QString stripQrcPrefix(const QString &path)
2187{
2188 QString result = path;
2189 if (result.startsWith(QStringLiteral(":/")))
2190 result.remove(0, 2);
2191 return result;
2192}
2193
2194// Creates all parent directories needed for the given file path.
2195// Returns true on success, false if creation fails.
2196static bool createDirectory(const QString &filePath)
2197{
2198 QFileInfo fileInfo(filePath);
2199 QString dirPath = fileInfo.path();
2200 QDir dir;
2201
2202 if (dir.exists(dirPath))
2203 return true;
2204
2205 if (!dir.mkpath(dirPath))
2206 return false;
2207
2208 return true;
2209}
2210
2211static bool isValidSavePath(const QString &path) {
2212 const QFileInfo info = QFileInfo(path);
2213 if (!info.exists()) {
2214 return QFileInfo(info.dir().path()).isWritable();
2215 }
2216 return info.isWritable() && !info.isDir();
2217}
2218
2219static inline QString indexToMeshKey(int index)
2220{
2221 return QStringLiteral("_mesh_%1").arg(index);
2222}
2223
2224bool QSSGLightmapperPrivate::storeMeshes(QSharedPointer<QSSGLightmapWriter> writer)
2225{
2226 if (!isValidSavePath(outputPath)) {
2227 sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
2228 QStringLiteral("Source path %1 is not a writable location").arg(outputPath));
2229 return false;
2230 }
2231
2232 for (int i = 0; i < meshes.size(); ++i) {
2233 if (!writer->writeData(indexToMeshKey(i), QSSGLightmapIODataTag::Mesh, meshes[i]))
2234 return false;
2235 }
2236
2237 return true;
2238}
2239
2240bool QSSGLightmapperPrivate::storeSceneMetadata(QSharedPointer<QSSGLightmapWriter> writer)
2241{
2242 QVariantMap metadata;
2243
2244 metadata[QStringLiteral("qt_version")] = QString::fromUtf8(QT_VERSION_STR);
2245 metadata[QStringLiteral("bake_start_time")] = bakeStartTime;
2246 metadata[QStringLiteral("bake_end_time")] = QDateTime::currentMSecsSinceEpoch();
2247
2248 QVariantMap metadata2;
2249 metadata2[QStringLiteral("opacityThreshold")] = options.opacityThreshold;
2250 metadata2[QStringLiteral("bias")] = options.bias;
2251 metadata2[QStringLiteral("useAdaptiveBias")] = options.useAdaptiveBias;
2252 metadata2[QStringLiteral("indirectLightEnabled")] = options.indirectLightEnabled;
2253 metadata2[QStringLiteral("indirectLightSamples")] = options.indirectLightSamples;
2254 metadata2[QStringLiteral("indirectLightWorkgroupSize")] = options.indirectLightWorkgroupSize;
2255 metadata2[QStringLiteral("indirectLightBounces")] = options.indirectLightBounces;
2256 metadata2[QStringLiteral("indirectLightFactor")] = options.indirectLightFactor;
2257 metadata2[QStringLiteral("denoiseSigma")] = options.sigma;
2258 metadata2[QStringLiteral("texelsPerUnit")] = options.texelsPerUnit;
2259
2260 metadata[QStringLiteral("options")] = metadata2;
2261 return writer->writeMap(QString::fromUtf8(KEY_SCENE_METADATA), QSSGLightmapIODataTag::SceneMetadata, metadata);
2262}
2263
2264bool QSSGLightmapperPrivate::storeMetadata(int lmIdx, QSharedPointer<QSSGLightmapWriter> writer)
2265{
2266 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
2267 const DrawInfo &drawInfo(drawInfos[lmIdx]);
2268
2269 QVariantMap metadata;
2270 metadata[QStringLiteral("width")] = drawInfos[lmIdx].lightmapSize.width();
2271 metadata[QStringLiteral("height")] = drawInfos[lmIdx].lightmapSize.height();
2272 metadata[QStringLiteral("mesh_key")] = indexToMeshKey(drawInfo.meshIndex);
2273
2274 return writer->writeMap(lm.model->lightmapKey, QSSGLightmapIODataTag::Metadata, metadata);
2275}
2276
2277bool QSSGLightmapperPrivate::storeScale(int lmIdx, QSharedPointer<QSSGLightmapWriter> writer)
2278{
2279 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
2280 const DrawInfo &drawInfo(drawInfos[lmIdx]);
2281
2282 QByteArray buffer;
2283 QDataStream stream(&buffer, QIODevice::WriteOnly);
2284 stream << drawInfo.scale;
2285 return writer->writeData(lm.model->lightmapKey, QSSGLightmapIODataTag::OriginalScale, buffer);
2286}
2287
2288bool QSSGLightmapperPrivate::storeDirectLightData(int lmIdx, const QVector<QVector3D> &directLight, QSharedPointer<QSSGLightmapWriter> writer)
2289{
2290 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
2291 const int numTexels = modelTexels[lmIdx].size();
2292
2293 QByteArray directFP32(numTexels * 4 * sizeof(float), Qt::Uninitialized);
2294 float *directFloatPtr = reinterpret_cast<float *>(directFP32.data());
2295
2296 for (int i = 0; i < numTexels; ++i) {
2297 const auto &lmPix = modelTexels[lmIdx][i];
2298 if (lmPix.isValid()) {
2299 *directFloatPtr++ = directLight[i].x();
2300 *directFloatPtr++ = directLight[i].y();
2301 *directFloatPtr++ = directLight[i].z();
2302 *directFloatPtr++ = 1.0f;
2303 } else {
2304 *directFloatPtr++ = 0.0f;
2305 *directFloatPtr++ = 0.0f;
2306 *directFloatPtr++ = 0.0f;
2307 *directFloatPtr++ = 0.0f;
2308 }
2309 }
2310
2311 const QByteArray dilated = dilate(drawInfos[lmIdx].lightmapSize, directFP32);
2312
2313 if (dilated.isEmpty())
2314 return false;
2315
2316 writer->writeF32Image(lm.model->lightmapKey, QSSGLightmapIODataTag::Texture_Direct, dilated);
2317
2318 return true;
2319}
2320
2321bool QSSGLightmapperPrivate::storeIndirectLightData(int lmIdx, const QVector<QVector3D> &indirectLight, QSharedPointer<QSSGLightmapWriter> writer)
2322{
2323 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
2324 const int numTexels = modelTexels[lmIdx].size();
2325
2326 QByteArray lightmapFP32(numTexels * 4 * sizeof(float), Qt::Uninitialized);
2327 float *lightmapFloatPtr = reinterpret_cast<float *>(lightmapFP32.data());
2328
2329 for (int i = 0; i < numTexels; ++i) {
2330 const auto &lmPix = modelTexels[lmIdx][i];
2331 if (lmPix.isValid()) {
2332 *lightmapFloatPtr++ = indirectLight[i].x();
2333 *lightmapFloatPtr++ = indirectLight[i].y();
2334 *lightmapFloatPtr++ = indirectLight[i].z();
2335 *lightmapFloatPtr++ = 1.0f;
2336 } else {
2337 *lightmapFloatPtr++ = 0.0f;
2338 *lightmapFloatPtr++ = 0.0f;
2339 *lightmapFloatPtr++ = 0.0f;
2340 *lightmapFloatPtr++ = 0.0f;
2341 }
2342 }
2343
2344 QByteArray dilated = dilate(drawInfos[lmIdx].lightmapSize, lightmapFP32);
2345
2346 if (dilated.isEmpty())
2347 return false;
2348
2349 // Reduce UV seams by collecting all edges (going through all
2350 // triangles), looking for (fuzzy)matching ones, then drawing lines
2351 // with blending on top.
2352 const DrawInfo &drawInfo(drawInfos[lmIdx]);
2353 const char *vbase = drawInfo.vertexData.constData();
2354 const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
2355
2356 // topology is Triangles, would be indexed draw - get rid of the index
2357 // buffer, need nothing but triangles afterwards
2358 qsizetype assembledVertexCount = 0;
2359 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx])
2360 assembledVertexCount += subMeshInfo.count;
2361 QVector<QVector3D> smPos(assembledVertexCount);
2362 QVector<QVector3D> smNormal(assembledVertexCount);
2363 QVector<QVector2D> smCoord(assembledVertexCount);
2364 qsizetype vertexIdx = 0;
2365 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
2366 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
2367 const quint32 idx = *(ibase + subMeshInfo.offset + i);
2368 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
2369 float x = *src++;
2370 float y = *src++;
2371 float z = *src++;
2372 smPos[vertexIdx] = QVector3D(x, y, z);
2373 src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
2374 x = *src++;
2375 y = *src++;
2376 z = *src++;
2377 smNormal[vertexIdx] = QVector3D(x, y, z);
2378 src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
2379 x = *src++;
2380 y = *src++;
2381 smCoord[vertexIdx] = QVector2D(x, y);
2382 ++vertexIdx;
2383 }
2384 }
2385
2386 QHash<Edge, EdgeUV> edgeUVMap;
2387 QVector<SeamUV> seams;
2388 for (vertexIdx = 0; vertexIdx < assembledVertexCount; vertexIdx += 3) {
2389 QVector3D triVert[3] = { smPos[vertexIdx], smPos[vertexIdx + 1], smPos[vertexIdx + 2] };
2390 QVector3D triNorm[3] = { smNormal[vertexIdx], smNormal[vertexIdx + 1], smNormal[vertexIdx + 2] };
2391 QVector2D triUV[3] = { smCoord[vertexIdx], smCoord[vertexIdx + 1], smCoord[vertexIdx + 2] };
2392
2393 for (int i = 0; i < 3; ++i) {
2394 int i0 = i;
2395 int i1 = (i + 1) % 3;
2396 if (vectorLessThan(triVert[i1], triVert[i0]))
2397 std::swap(i0, i1);
2398
2399 const Edge e = {
2400 { triVert[i0], triVert[i1] },
2401 { triNorm[i0], triNorm[i1] }
2402 };
2403 const EdgeUV edgeUV = { { triUV[i0], triUV[i1] } };
2404 auto it = edgeUVMap.find(e);
2405 if (it == edgeUVMap.end()) {
2406 edgeUVMap.insert(e, edgeUV);
2407 } else if (!qFuzzyCompare(it->uv[0], edgeUV.uv[0]) || !qFuzzyCompare(it->uv[1], edgeUV.uv[1])) {
2408 if (!it->seam) {
2409 seams.append(SeamUV({ { edgeUV.uv, it->uv } }));
2410 it->seam = true;
2411 }
2412 }
2413 }
2414 }
2415 //qDebug() << "lm:" << seams.size() << "UV seams in" << lm.model;
2416
2417 QByteArray workBuf(dilated.size(), Qt::Uninitialized);
2418 for (int blendIter = 0; blendIter < LM_SEAM_BLEND_ITER_COUNT; ++blendIter) {
2419 memcpy(workBuf.data(), dilated.constData(), dilated.size());
2420 for (int seamIdx = 0, end = seams.size(); seamIdx != end; ++seamIdx) {
2421 const SeamUV &seam(seams[seamIdx]);
2422 blendLine(seam.uv[0][0], seam.uv[0][1],
2423 seam.uv[1][0], seam.uv[1][1],
2424 reinterpret_cast<const float *>(workBuf.data()),
2425 reinterpret_cast<float *>(dilated.data()),
2426 drawInfos[lmIdx].lightmapSize);
2427 blendLine(seam.uv[1][0], seam.uv[1][1],
2428 seam.uv[0][0], seam.uv[0][1],
2429 reinterpret_cast<const float *>(workBuf.data()),
2430 reinterpret_cast<float *>(dilated.data()),
2431 drawInfos[lmIdx].lightmapSize);
2432 }
2433 }
2434
2435 writer->writeF32Image(lm.model->lightmapKey, QSSGLightmapIODataTag::Texture_Indirect, dilated);
2436
2437 return true;
2438}
2439
2440bool QSSGLightmapperPrivate::storeMaskImage(int lmIdx, QSharedPointer<QSSGLightmapWriter> writer)
2441{
2442 constexpr quint32 PIXEL_VOID = 0;
2443 constexpr quint32 PIXEL_UNSET = -1;
2444
2445 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
2446 const int numTexels = modelTexels[lmIdx].size();
2447
2448 QByteArray mask(numTexels * sizeof(quint32), Qt::Uninitialized);
2449 quint32 *maskUIntPtr = reinterpret_cast<quint32 *>(mask.data());
2450
2451 for (int i = 0; i < numTexels; ++i) {
2452 *maskUIntPtr++ = modelTexels[lmIdx][i].isValid() ? PIXEL_UNSET : PIXEL_VOID;
2453 }
2454
2455 const int rows = drawInfos[lmIdx].lightmapSize.height();
2456 const int cols = drawInfos[lmIdx].lightmapSize.width();
2457
2458 // Use flood fill so each chart has its own "color" which
2459 // can then be used in the denoise shader to only take into account
2460 // pixels in the same chart.
2461 floodFill(reinterpret_cast<quint32 *>(mask.data()), rows, cols);
2462
2463 writer->writeF32Image(lm.model->lightmapKey, QSSGLightmapIODataTag::Mask, mask);
2464
2465 return true;
2466}
2467
2468bool QSSGLightmapperPrivate::denoiseLightmaps()
2469{
2470 QElapsedTimer denoiseTimer;
2471 denoiseTimer.start();
2472
2473 // Tmp file
2474 const QString inPath = QFileInfo(outputPath + QStringLiteral(".raw")).absoluteFilePath();
2475 QSharedPointer<QSSGLightmapLoader> tmpFile = QSSGLightmapLoader::open(inPath);
2476 if (!tmpFile) {
2477 sendOutputInfo(QSSGLightmapper::BakingStatus::Error, QStringLiteral("Could not read file '%1'").arg(inPath));
2478 return false;
2479 }
2480
2481 // Final file
2482 const QString outPath = QFileInfo(outputPath).absoluteFilePath();
2483 QSharedPointer<QSSGLightmapWriter> finalFile = QSSGLightmapWriter::open(outPath);
2484 if (!finalFile) {
2485 sendOutputInfo(QSSGLightmapper::BakingStatus::Error, QStringLiteral("Could not read file '%1'").arg(outPath));
2486 return false;
2487 }
2488
2489 QSet<QString> lightmapKeys;
2490 const auto keys = tmpFile->getKeys();
2491 for (const auto &[key, tag] : keys) {
2492 if (tag == QSSGLightmapIODataTag::SceneMetadata) continue; // Will write at end
2493
2494 if (tag != QSSGLightmapIODataTag::Texture_Direct && tag != QSSGLightmapIODataTag::Texture_Indirect
2495 && tag != QSSGLightmapIODataTag::Mask) {
2496 // Clone meshes and metadata for final file
2497 finalFile->writeData(key, tag, tmpFile->readData(key, tag));
2498 } else if (tag == QSSGLightmapIODataTag::Texture_Direct) {
2499 lightmapKeys.insert(key);
2500 }
2501 }
2502
2503 QRhi *rhi = rhiCtxInterface->rhiContext()->rhi();
2504 Q_ASSERT(rhi);
2505 if (!rhi->isFeatureSupported(QRhi::Compute)) {
2506 qFatal("Compute is not supported, denoising disabled");
2507 return false;
2508 }
2509
2510 const int bakedLightingModelCount = lightmapKeys.size();
2511 if (bakedLightingModelCount == 0)
2512 return true;
2513
2514 QShader shader;
2515 if (QFile f(QStringLiteral(":/res/rhishaders/nlm_denoise.comp.qsb")); f.open(QIODevice::ReadOnly)) {
2516 shader = QShader::fromSerialized(f.readAll());
2517 } else {
2518 qFatal() << "Could not find denoise shader";
2519 return false;
2520 }
2521 Q_ASSERT(shader.isValid());
2522
2523 QVariantMap sceneMetadata = tmpFile->readMap(QString::fromUtf8(KEY_SCENE_METADATA), QSSGLightmapIODataTag::SceneMetadata);
2524 sceneMetadata[QStringLiteral("denoise_start_time")] = QDateTime::currentMSecsSinceEpoch();
2525
2526 int lmIdx = -1;
2527 for (const QString &key : std::as_const(lightmapKeys)) {
2528 ++lmIdx;
2529 auto incrementTracker = QScopeGuard([this, lmIdx, bakedLightingModelCount]() {
2530 progressTracker.denoisedModelDone(lmIdx + 1, bakedLightingModelCount);
2531 });
2532
2533
2534 sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
2535 QStringLiteral("[%2/%3] denoising '%1'").arg(key).arg(lmIdx + 1).arg(bakedLightingModelCount));
2536
2537 QVariantMap metadata = tmpFile->readMap(key, QSSGLightmapIODataTag::Metadata);
2538 QByteArray indirect = tmpFile->readF32Image(key, QSSGLightmapIODataTag::Texture_Indirect);
2539 QByteArray direct = tmpFile->readF32Image(key, QSSGLightmapIODataTag::Texture_Direct);
2540 QByteArray mask = tmpFile->readU32Image(key, QSSGLightmapIODataTag::Mask);
2541
2542 if (!metadata.contains(QStringLiteral("width")) || !metadata.contains(QStringLiteral("height"))
2543 || indirect.isEmpty() || direct.isEmpty() || mask.isEmpty()) {
2544 sendOutputInfo(QSSGLightmapper::BakingStatus::Error,
2545 QStringLiteral("[%2/%3] Failed to denoise '%1'").arg(key).arg(lmIdx + 1).arg(bakedLightingModelCount));
2546 continue;
2547 }
2548
2549 QRhiCommandBuffer *cb = nullptr;
2550 cb = rhiCtxInterface->rhiContext()->commandBuffer();
2551 Q_ASSERT(cb);
2552
2553 QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
2554 Q_ASSERT(u);
2555
2556 const int w = metadata[QStringLiteral("width")].toInt();
2557 const int h = metadata[QStringLiteral("height")].toInt();
2558 const QSize size(w, h);
2559 const int numPixels = w * h;
2560
2561 Q_ASSERT(qsizetype(numPixels * sizeof(float) * 4) == indirect.size());
2562 Q_ASSERT(qsizetype(numPixels * sizeof(float) * 4) == direct.size());
2563 Q_ASSERT(qsizetype(numPixels * sizeof(quint32)) == mask.size());
2564
2565 QScopedPointer<QRhiBuffer> buffIn(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, 3 * numPixels * sizeof(float)));
2566 QScopedPointer<QRhiBuffer> buffCount(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, numPixels * sizeof(quint32)));
2567 QScopedPointer<QRhiBuffer> buffOut(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, 3 * numPixels * sizeof(quint32)));
2568 QScopedPointer<QRhiTexture> texMask(rhi->newTexture(QRhiTexture::RGBA8, size, 1, QRhiTexture::UsedWithLoadStore));
2569
2570 buffIn->create();
2571 buffCount->create();
2572 buffOut->create();
2573 texMask->create();
2574
2575 u->uploadTexture(texMask.data(), QImage(reinterpret_cast<const uchar *>(mask.constData()), w, h, QImage::Format_RGBA8888));
2576
2577 // fill and upload input and count buffers
2578 {
2579 QByteArray inArray(3 * numPixels * sizeof(float), 0);
2580 QByteArray count(numPixels * sizeof(quint32), 0);
2581 QByteArray outArray(3 * numPixels * sizeof(float), 0);
2582
2583 QVector3D* inDst = reinterpret_cast<QVector3D*>(inArray.data());
2584 const QVector4D* indirectSrc = reinterpret_cast<const QVector4D*>(indirect.data());
2585 for (int i = 0; i < numPixels; ++i) {
2586 inDst[i][0] = indirectSrc[i][0] * 256.f;
2587 inDst[i][1] = indirectSrc[i][1] * 256.f;
2588 inDst[i][2] = indirectSrc[i][2] * 256.f;
2589 }
2590 u->uploadStaticBuffer(buffIn.data(), inArray);
2591 u->uploadStaticBuffer(buffCount.data(), count);
2592 u->uploadStaticBuffer(buffOut.data(), outArray);
2593 }
2594
2595 struct Settings
2596 {
2597 float sigma;
2598 float width; // int
2599 float height; // int
2600 } settings;
2601
2602 settings.sigma = options.sigma;
2603 settings.width = w;
2604 settings.height = h;
2605
2606 QScopedPointer<QRhiBuffer> settingsBuffer(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(settings)));
2607 settingsBuffer->create();
2608
2609 u->updateDynamicBuffer(settingsBuffer.data(), 0, sizeof(settings), &settings);
2610
2611 QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
2612 srb->setBindings(
2613 {
2614 QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::ComputeStage, settingsBuffer.data()),
2615 QRhiShaderResourceBinding::bufferLoad(1, QRhiShaderResourceBinding::ComputeStage, buffIn.data()),
2616 QRhiShaderResourceBinding::imageLoad(2, QRhiShaderResourceBinding::ComputeStage, texMask.data(), 0),
2617 QRhiShaderResourceBinding::bufferLoadStore(3, QRhiShaderResourceBinding::ComputeStage, buffOut.data()),
2618 QRhiShaderResourceBinding::bufferLoadStore(4, QRhiShaderResourceBinding::ComputeStage, buffCount.data())
2619 });
2620 srb->create();
2621
2622 QScopedPointer<QRhiComputePipeline> pipeline(rhi->newComputePipeline());
2623 pipeline->setShaderStage({ QRhiShaderStage::Compute, shader });
2624 pipeline->setShaderResourceBindings(srb.data());
2625 pipeline->create();
2626
2627 cb->beginComputePass(u);
2628 cb->setComputePipeline(pipeline.data());
2629 cb->setShaderResources();
2630 constexpr int local_size_x = 8;
2631 constexpr int local_size_y = 8;
2632 constexpr int local_size_z = 1;
2633 cb->dispatch((w + local_size_x - 1) / local_size_x, (h + local_size_y - 1) / local_size_y, local_size_z);
2634
2635 u = rhi->nextResourceUpdateBatch();
2636 Q_ASSERT(u);
2637
2638 QByteArray final;
2639 QByteArray outOut;
2640 QByteArray outCount;
2641
2642 QRhiReadbackResult readResultOut;
2643 readResultOut.completed = [&] {
2644 outOut = readResultOut.data;
2645 Q_ASSERT(outOut.size() == qsizetype(numPixels * sizeof(quint32) * 3));
2646 };
2647 QRhiReadbackResult readResultCount;
2648 readResultCount.completed = [&] {
2649 outCount = readResultCount.data;
2650 Q_ASSERT(outCount.size() == qsizetype(numPixels * sizeof(quint32)));
2651 };
2652
2653 u->readBackBuffer(buffOut.get(), 0, 3 * numPixels *sizeof(quint32), &readResultOut);
2654 u->readBackBuffer(buffCount.get(), 0, numPixels * sizeof(quint32), &readResultCount);
2655
2656 cb->endComputePass(u);
2657 rhi->finish();
2658
2659 // Write back to image.data variable
2660 final.resize(indirect.size());
2661 memcpy(final.data(), indirect.data(), indirect.size());
2662
2663 QVector4D* res = reinterpret_cast<QVector4D*>(final.data());
2664 quint32* ptrRGB = reinterpret_cast<quint32*>(outOut.data());
2665 quint32* ptrCount = reinterpret_cast<quint32*>(outCount.data());
2666 for (int y = 0; y < h; ++y) {
2667 for (int x = 0; x < w; ++x) {
2668 const int idxDst = y * w + x;
2669 const int idxDst1 = 3 * idxDst;
2670 Q_ASSERT(idxDst1 < numPixels * 3);
2671 quint32 cnt = ptrCount[idxDst];
2672 //Q_ASSERT(cnt);
2673 float r = (ptrRGB[idxDst1] / 256.f) / 1000.f;
2674 float g = (ptrRGB[idxDst1 + 1] / 256.f) / 1000.f;
2675 float b = (ptrRGB[idxDst1 + 2] / 256.f) / 1000.f;
2676 if (cnt > 0) {
2677 res[idxDst][0] = r / cnt;
2678 res[idxDst][1] = g / cnt;
2679 res[idxDst][2] = b / cnt;
2680 }
2681 }
2682 }
2683
2684 std::array<float, 4> *imagePtr = reinterpret_cast<std::array<float, 4>*>(const_cast<char*>(final.data()));
2685 std::array<float, 4> *directPtr = reinterpret_cast<std::array<float, 4>*>(const_cast<char*>(direct.data()));
2686 for (int i = 0; i < numPixels; ++i) {
2687 imagePtr[i][0] += directPtr[i][0];
2688 imagePtr[i][1] += directPtr[i][1];
2689 imagePtr[i][2] += directPtr[i][2];
2690 // skip alpha, always 0 or 1
2691 Q_ASSERT(imagePtr[i][3] == directPtr[i][3]);
2692 Q_ASSERT(imagePtr[i][3] == 1.f || imagePtr[i][3] == 0.f);
2693 }
2694
2695 finalFile->writeF32Image(key, QSSGLightmapIODataTag::Texture_Final, final);
2696 }
2697
2698 sceneMetadata[QStringLiteral("denoise_end_time")] = QDateTime::currentMSecsSinceEpoch();
2699 auto optionsMap = sceneMetadata[QStringLiteral("options")].toMap();
2700 optionsMap[QStringLiteral("denoiseSigma")] = options.sigma;
2701 sceneMetadata[QStringLiteral("options")] = optionsMap;
2702
2703 finalFile->writeMap(QString::fromUtf8(KEY_SCENE_METADATA), QSSGLightmapIODataTag::SceneMetadata, sceneMetadata);
2704
2705 if (!finalFile->close()) {
2706 sendOutputInfo(QSSGLightmapper::BakingStatus::Error, QStringLiteral("Could not save file '%1'").arg(outPath));
2707 return false;
2708 }
2709
2710 return true;
2711
2712}
2713
2714bool QSSGLightmapperPrivate::userCancelled()
2715{
2716 if (bakingControl.cancelled) {
2717 sendOutputInfo(QSSGLightmapper::BakingStatus::Cancelled,
2718 QStringLiteral("Cancelled by user"));
2719 }
2720 return bakingControl.cancelled;
2721}
2722
2723void QSSGLightmapperPrivate::sendOutputInfo(QSSGLightmapper::BakingStatus type, std::optional<QString> msg, bool outputToConsole, bool outputConsoleTimeRemanining)
2724{
2725 if (outputToConsole) {
2726 QString consoleMessage;
2727
2728 switch (type)
2729 {
2730 case QSSGLightmapper::BakingStatus::None:
2731 return;
2732 case QSSGLightmapper::BakingStatus::Info:
2733 consoleMessage = QStringLiteral("[lm] Info");
2734 break;
2735 case QSSGLightmapper::BakingStatus::Error:
2736 consoleMessage = QStringLiteral("[lm] Error");
2737 break;
2738 case QSSGLightmapper::BakingStatus::Warning:
2739 consoleMessage = QStringLiteral("[lm] Warning");
2740 break;
2741 case QSSGLightmapper::BakingStatus::Cancelled:
2742 consoleMessage = QStringLiteral("[lm] Cancelled");
2743 break;
2744 case QSSGLightmapper::BakingStatus::Failed:
2745 consoleMessage = QStringLiteral("[lm] Failed");
2746 break;
2747 case QSSGLightmapper::BakingStatus::Complete:
2748 consoleMessage = QStringLiteral("[lm] Complete");
2749 break;
2750 }
2751
2752 if (msg.has_value())
2753 consoleMessage.append(QStringLiteral(": ") + msg.value());
2754 else if (outputConsoleTimeRemanining) {
2755 const QString timeRemaining = estimatedTimeRemaining >= 0 ? formatDuration(estimatedTimeRemaining, false)
2756 : QStringLiteral("Estimating...");
2757 consoleMessage.append(QStringLiteral(": Time remaining: ") + timeRemaining);
2758 }
2759
2760 if (type == QSSGLightmapper::BakingStatus::Error || type == QSSGLightmapper::BakingStatus::Warning)
2761 qWarning() << consoleMessage;
2762 else
2763 qInfo() << consoleMessage;
2764 }
2765
2766 if (outputCallback) {
2767 QVariantMap payload;
2768 payload[QStringLiteral("status")] = (int)type;
2769 payload[QStringLiteral("stage")] = stage;
2770 payload[QStringLiteral("message")] = msg.value_or(QString());
2771 payload[QStringLiteral("totalTimeRemaining")] = estimatedTimeRemaining;
2772 payload[QStringLiteral("totalProgress")] = totalProgress;
2773 outputCallback(payload, &bakingControl);
2774 }
2775}
2776
2777void QSSGLightmapperPrivate::updateStage(const QString &newStage)
2778{
2779 if (newStage == stage)
2780 return;
2781
2782 stage = newStage;
2783 if (outputCallback) {
2784 QVariantMap payload;
2785 payload[QStringLiteral("stage")] = stage;
2786 outputCallback(payload, &bakingControl);
2787 }
2788}
2789
2790bool QSSGLightmapper::bake()
2791{
2792 d->totalTimer.start();
2793 d->bakeStartTime = QDateTime::currentMSecsSinceEpoch();
2794
2795 d->updateStage(QStringLiteral("Preparing"));
2796 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Preparing for bake..."));
2797
2798 if (!isValidSavePath(d->outputPath)) {
2799 d->updateStage(QStringLiteral("Failed"));
2800 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
2801 QStringLiteral("Source path %1 is not a writable location").arg(d->outputPath));
2802 return false;
2803 }
2804
2805 if (d->bakedLightingModels.isEmpty()) {
2806 d->updateStage(QStringLiteral("Failed"));
2807 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("No Models to bake"));
2808 return false;
2809 }
2810
2811 // ------------- Commit geometry -------------
2812
2813 if (!d->commitGeometry()) {
2814 d->updateStage(QStringLiteral("Failed"));
2815 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Baking failed"));
2816 return false;
2817 }
2818
2819 // Main thread can continue
2820 d->initMutex.lock();
2821 d->initCondition.wakeAll();
2822 d->initMutex.unlock();
2823
2824 if (d->userCancelled()) {
2825 d->updateStage(QStringLiteral("Cancelled"));
2826 return false;
2827 }
2828
2829 // ------------- Init Progress Tracker ---------
2830 const int bakedLightingModelCount = d->bakedLightingModels.size();
2831
2832 // Precompute the number of direct light tiles for progress tracking
2833 quint32 numDirectTiles = 0;
2834 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
2835 QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
2836 if (d->denoiseOnly)
2837 break;
2838 if (!lm.model->hasLightmap())
2839 continue;
2840 if (!lm.model->castsShadows)
2841 continue;
2842
2843 const auto &drawInfo = d->drawInfos[lmIdx];
2844 const QSize sz = drawInfo.lightmapSize;
2845 const int w = sz.width();
2846 const int h = sz.height();
2847 constexpr int maxTileSize = MAX_TILE_SIZE / DIRECT_MAP_UPSCALE_FACTOR;
2848 const int numTilesX = (w + maxTileSize - 1) / maxTileSize;
2849 const int numTilesY = (h + maxTileSize - 1) / maxTileSize;
2850
2851 numDirectTiles += numTilesX * numTilesY;
2852 }
2853
2854 d->progressTracker.initBake(d->options.indirectLightSamples, d->options.indirectLightBounces);
2855 d->progressTracker.setTotalDirectTiles(numDirectTiles);
2856
2857 // ------------- Prepare lightmaps -------------
2858
2859 if (!d->prepareLightmaps()) {
2860 d->updateStage(QStringLiteral("Failed"));
2861 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Baking failed"));
2862 return false;
2863 }
2864
2865 if (d->userCancelled()) {
2866 d->updateStage(QStringLiteral("Cancelled"));
2867 return false;
2868 }
2869
2870 if (!d->verifyLights()) {
2871 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
2872 QStringLiteral("Did not find any lights with baking enabled or any "
2873 "emissive models in the scene."));
2874 return false;
2875 }
2876
2877 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
2878 QStringLiteral("Total emissive models registered: %1")
2879 .arg(d->emissiveModelCount));
2880
2881 // indirect lighting is slow, so parallelize per groups of samples,
2882 // e.g. if sample count is 256 and workgroup size is 32, then do up to
2883 // 8 sets in parallel, each calculating 32 samples (how many of the 8
2884 // are really done concurrently that's up to the thread pool to manage)
2885 const int wgSizePerGroup = qMax(1, d->options.indirectLightWorkgroupSize);
2886 const int wgCount = (d->options.indirectLightSamples / wgSizePerGroup) + (d->options.indirectLightSamples % wgSizePerGroup ? 1: 0);
2887
2888 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Sample count: %1, Workgroup size: %2, Max bounces: %3, Multiplier: %4").
2889 arg(d->options.indirectLightSamples).
2890 arg(wgSizePerGroup).
2891 arg(d->options.indirectLightBounces).
2892 arg(d->options.indirectLightFactor));
2893
2894 // We use a work-file where we store the baked lightmaps accumulatively and when
2895 // the baking process is finished successfully, replace the .raw file with it.
2896 QSharedPointer<QTemporaryFile> workFile = QSharedPointer<QTemporaryFile>::create(QDir::tempPath() + "/qt_lightmapper_work_file_XXXXXX"_L1);
2897
2898 QElapsedTimer timer;
2899 timer.start();
2900
2901 // ------------- Store metadata -------------
2902
2903 d->updateStage(QStringLiteral("Storing Metadata"));
2904 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Storing metadata..."));
2905 auto writer = QSSGLightmapWriter::open(workFile);
2906
2907 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
2908 if (d->userCancelled()) {
2909 d->updateStage(QStringLiteral("Cancelled"));
2910 return false;
2911 }
2912 QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
2913 if (!lm.model->hasLightmap())
2914 continue;
2915
2916 if (!d->storeMetadata(lmIdx, writer)) {
2917 d->updateStage(QStringLiteral("Failed"));
2918 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
2919 QStringLiteral("[%1/%2] Failed to store metadata for '%3'")
2920 .arg(lmIdx + 1)
2921 .arg(bakedLightingModelCount)
2922 .arg(lm.model->lightmapKey));
2923 return false;
2924 }
2925
2926 }
2927
2928 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Storing scale..."));
2929 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
2930 if (d->userCancelled()) {
2931 d->updateStage(QStringLiteral("Cancelled"));
2932 return false;
2933 }
2934 QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
2935 if (!lm.model->hasLightmap())
2936 continue;
2937
2938 if (!d->storeScale(lmIdx, writer)) {
2939 d->updateStage(QStringLiteral("failed"));
2940 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
2941 QStringLiteral("[%1/%2] Failed to store scale for '%3'")
2942 .arg(lmIdx + 1)
2943 .arg(bakedLightingModelCount)
2944 .arg(lm.model->lightmapKey));
2945 }
2946 }
2947
2948 // ------------- Store mask -------------
2949
2950 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Storing mask images..."));
2951 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
2952 if (d->userCancelled()) {
2953 d->updateStage(QStringLiteral("Cancelled"));
2954 return false;
2955 }
2956 QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
2957 if (!lm.model->hasLightmap())
2958 continue;
2959
2960 if (!d->storeMaskImage(lmIdx, writer)) {
2961 d->updateStage(QStringLiteral("Failed"));
2962 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
2963 QStringLiteral("[%1/%2] Failed to store mask for '%3'")
2964 .arg(lmIdx + 1)
2965 .arg(bakedLightingModelCount)
2966 .arg(lm.model->lightmapKey));
2967 return false;
2968 }
2969 }
2970 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
2971 QStringLiteral("Took %1").arg(formatDuration(timer.restart())));
2972
2973 if (d->userCancelled()) {
2974 d->updateStage(QStringLiteral("Cancelled"));
2975 return false;
2976 }
2977
2978 // ------------- Direct compute / store -------------
2979
2980 d->updateStage(QStringLiteral("Computing Direct Light"));
2981 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Computing direct light..."));
2982 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
2983 if (d->userCancelled()) {
2984 d->updateStage(QStringLiteral("Cancelled"));
2985 return false;
2986 }
2987 QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
2988 if (!lm.model->hasLightmap())
2989 continue;
2990
2991 timer.restart();
2992 const QVector<QVector3D> directLight = d->computeDirectLight(lmIdx);
2993 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
2994 QStringLiteral("[%1/%2] '%3' took %4")
2995 .arg(QString::number(lmIdx + 1),
2996 QString::number(bakedLightingModelCount),
2997 lm.model->lightmapKey,
2998 formatDuration(timer.elapsed())));
2999
3000 if (directLight.empty()) {
3001 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
3002 QStringLiteral("[%1/%2] Failed to compute for '%3'")
3003 .arg(lmIdx + 1)
3004 .arg(bakedLightingModelCount)
3005 .arg(lm.model->lightmapKey));
3006 return false;
3007 }
3008
3009 if (!d->storeDirectLightData(lmIdx, directLight, writer)) {
3010 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
3011 QStringLiteral("[%1/%2] Failed to store data for '%3'")
3012 .arg(lmIdx + 1)
3013 .arg(bakedLightingModelCount)
3014 .arg(lm.model->lightmapKey));
3015 return false;
3016 }
3017 }
3018
3019 if (d->userCancelled()) {
3020 d->updateStage(QStringLiteral("Cancelled"));
3021 return false;
3022 }
3023
3024 // ------------- Indirect compute / store -------------
3025
3026 if (d->options.indirectLightEnabled) {
3027 d->indirectTexelsTotal = std::accumulate(d->numValidTexels.begin(), d->numValidTexels.end(), quint64(0));
3028 d->updateStage(QStringLiteral("Computing Indirect Light"));
3029 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
3030 QStringLiteral("Computing indirect light..."));
3031 d->progressTracker.setStage(Stage::Indirect);
3032 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
3033 if (d->userCancelled()) {
3034 d->updateStage(QStringLiteral("Cancelled"));
3035 return false;
3036 }
3037 QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
3038 if (!lm.model->hasLightmap())
3039 continue;
3040
3041 timer.restart();
3042 const QVector<QVector3D> indirectLight = d->computeIndirectLight(lmIdx, wgCount, wgSizePerGroup);
3043 if (indirectLight.empty()) {
3044 d->updateStage(QStringLiteral("Failed"));
3045 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
3046 QStringLiteral("[%1/%2] Failed to compute '%3'")
3047 .arg(lmIdx + 1)
3048 .arg(bakedLightingModelCount)
3049 .arg(lm.model->lightmapKey));
3050 return false;
3051 }
3052
3053 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
3054 QStringLiteral("[%1/%2] '%3' took %4")
3055 .arg(QString::number(lmIdx + 1),
3056 QString::number(bakedLightingModelCount),
3057 lm.model->lightmapKey,
3058 formatDuration(timer.elapsed())));
3059
3060 if (d->userCancelled()) {
3061 d->updateStage(QStringLiteral("Cancelled"));
3062 return false;
3063 }
3064
3065 if (!d->storeIndirectLightData(lmIdx, indirectLight, writer)) {
3066 d->updateStage(QStringLiteral("Failed"));
3067 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
3068 QStringLiteral("[%1/%2] Failed to store data for '%3'")
3069 .arg(lmIdx + 1)
3070 .arg(bakedLightingModelCount)
3071 .arg(lm.model->lightmapKey));
3072 return false;
3073 }
3074 }
3075 }
3076
3077 // ------------- Store meshes -------------
3078
3079 if (!d->storeMeshes(writer)) {
3080 d->updateStage(QStringLiteral("Failed"));
3081 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Failed to store meshes"));
3082 return false;
3083 }
3084
3085 if (d->userCancelled()) {
3086 d->updateStage(QStringLiteral("Cancelled"));
3087 return false;
3088 }
3089
3090 // ------------- Scene Metadata ---------
3091
3092 d->updateStage(QStringLiteral("Storing Scene Metadata"));
3093 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Storing scene metadata..."));
3094 if (!d->storeSceneMetadata(writer)) {
3095 d->updateStage(QStringLiteral("Failed"));
3096 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
3097 QStringLiteral("Failed to store scene metadata"));
3098 }
3099
3100 // ------------- Copy file from tmp -------------
3101
3102 if (!writer->close()) {
3103 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Error,
3104 QStringLiteral("Failed to save temp file to %1").arg(workFile->fileName()));
3105 return false;
3106 }
3107
3108 const QString tmpPath = QFileInfo(d->outputPath).absoluteFilePath() + ".raw"_L1;
3109 QFile::remove(tmpPath);
3110 if (!workFile->copy(tmpPath)) {
3111 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Error,
3112 QStringLiteral("Failed to copy temp file to %1").arg(tmpPath));
3113 return false;
3114 }
3115
3116 if (d->userCancelled()) {
3117 d->updateStage(QStringLiteral("Cancelled"));
3118 return false;
3119 }
3120
3121 // ------------- Denoising -------------
3122
3123 d->progressTracker.setStage(Stage::Denoise);
3124 d->updateStage(QStringLiteral("Denoising"));
3125 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Denoising..."));
3126 timer.restart();
3127 if (!d->denoiseLightmaps()) {
3128 d->updateStage(QStringLiteral("Failed"));
3129 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Denoising failed"));
3130 return false;
3131 }
3132 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Took %1").arg(formatDuration(timer.elapsed())));
3133
3134 if (d->userCancelled()) {
3135 d->updateStage(QStringLiteral("Cancelled"));
3136 return false;
3137 }
3138
3139 // -------------------------------------
3140
3141 d->totalProgress = 1.0;
3142 d->estimatedTimeRemaining = -1;
3143 d->updateStage(QStringLiteral("Done"));
3144 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
3145 QStringLiteral("Baking took %1").arg(formatDuration(d->totalTimer.elapsed())));
3146 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Complete, std::nullopt);
3147 return true;
3148}
3149
3150bool QSSGLightmapper::denoise() {
3151
3152 // Main thread can continue
3153 d->initMutex.lock();
3154 d->initCondition.wakeAll();
3155 d->initMutex.unlock();
3156
3157 QElapsedTimer totalTimer;
3158 totalTimer.start();
3159
3160 d->progressTracker.initDenoise();
3161 d->progressTracker.setStage(Stage::Denoise);
3162 d->updateStage("Denoising"_L1);
3163 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Denoise starting..."));
3164
3165 if (!d->denoiseLightmaps()) {
3166 d->updateStage("Failed"_L1);
3167 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Denoising failed"));
3168 return false;
3169 }
3170
3171 d->totalProgress = 1;
3172 d->updateStage("Done"_L1);
3173 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Denoising took %1 ms").arg(totalTimer.elapsed()));
3174 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Complete, std::nullopt);
3175 return true;
3176}
3177
3178void QSSGLightmapper::run(QOffscreenSurface *fallbackSurface)
3179{
3180 auto releaseMainThread = qScopeGuard([&] {
3181 d->initMutex.lock();
3182 d->initCondition.wakeAll();
3183 d->initMutex.unlock();
3184 });
3185
3186 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
3187 QStringLiteral("Total models registered: %1").arg(d->bakedLightingModels.size()));
3188
3189 if (d->bakedLightingModels.isEmpty()) {
3190 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("No Models to bake"));
3191 return;
3192 }
3193
3194 d->outputPath = stripQrcPrefix(d->options.source);
3195
3196 if (!createDirectory(d->outputPath)) {
3197 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Failed to create output directory"));
3198 return;
3199 }
3200
3201 if (!isValidSavePath(d->outputPath)) {
3202 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
3203 QStringLiteral("Source path %1 is not a writable location").arg(d->outputPath));
3204 return;
3205 }
3206
3207 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Source path: %1").arg(d->outputPath));
3208 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Output path: %1").arg(d->outputPath));
3209
3210 const QRhi::Flags flags = QRhi::EnableTimestamps | QRhi::EnableDebugMarkers;
3211#if QT_CONFIG(vulkan)
3212 std::unique_ptr<QVulkanInstance> vulkanInstance; // Needs to live until rhi goes out of scope
3213#endif
3214 std::unique_ptr<QRhi> rhi;
3215
3216 switch (d->rhiBackend) {
3217 case QRhi::Vulkan: {
3218#if QT_CONFIG(vulkan)
3219 vulkanInstance = std::make_unique<QVulkanInstance>();
3220 vulkanInstance->create();
3221 QRhiVulkanInitParams params;
3222 params.inst = vulkanInstance.get();
3223 rhi = std::unique_ptr<QRhi>(QRhi::create(d->rhiBackend, &params, flags));
3224#endif
3225 break;
3226 }
3227 case QRhi::OpenGLES2: {
3228#if QT_CONFIG(opengl)
3229 QRhiGles2InitParams params;
3230 if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) {
3231 // OpenGL 4.3 or higher
3232 params.format.setProfile(QSurfaceFormat::CoreProfile);
3233 params.format.setVersion(4, 3);
3234 } else {
3235 // OpenGL ES 3.1 or higher
3236 params.format.setVersion(3, 1);
3237 }
3238 params.fallbackSurface = fallbackSurface;
3239 rhi = std::unique_ptr<QRhi>(QRhi::create(d->rhiBackend, &params, flags));
3240#endif
3241 break;
3242 }
3243 case QRhi::D3D11: {
3244#if defined(Q_OS_WIN)
3245 QRhiD3D11InitParams params;
3246 rhi = std::unique_ptr<QRhi>(QRhi::create(d->rhiBackend, &params, flags));
3247#endif
3248 break;
3249 }
3250 case QRhi::D3D12: {
3251#if defined(Q_OS_WIN)
3252 QRhiD3D12InitParams params;
3253 rhi = std::unique_ptr<QRhi>(QRhi::create(d->rhiBackend, &params, flags));
3254#endif
3255 break;
3256 }
3257 case QRhi::Metal: {
3258#if QT_CONFIG(metal)
3259 QRhiMetalInitParams params;
3260 rhi = std::unique_ptr<QRhi>(QRhi::create(d->rhiBackend, &params, flags));
3261#endif
3262 break;
3263 }
3264 case QRhi::Null:
3265 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("QRhi backend is null"));
3266 return;
3267 default:
3268 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Failed to initialize QRhi"));
3269 return;
3270 }
3271
3272 if (!rhi) {
3273 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create QRhi, cannot bake"));
3274 return;
3275 }
3276
3277 if (!rhi->isTextureFormatSupported(QRhiTexture::RGBA32F)) {
3278 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("FP32 textures not supported, cannot bake"));
3279 return;
3280 }
3281 if (rhi->resourceLimit(QRhi::MaxColorAttachments) < 4) {
3282 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Multiple render targets not supported, cannot bake"));
3283 return;
3284 }
3285 if (!rhi->isFeatureSupported(QRhi::NonFillPolygonMode)) {
3286 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Line polygon mode not supported, cannot bake"));
3287 return;
3288 }
3289
3290 if (!rhi->isFeatureSupported(QRhi::Compute)) {
3291 qFatal("Compute is not supported, cannot bake");
3292 return;
3293 }
3294
3295 d->rhiCtxInterface = std::
3296 unique_ptr<QSSGRenderContextInterface>(new QSSGRenderContextInterface(rhi.get()));
3297 d->renderer = std::unique_ptr<QSSGRenderer>(new QSSGRenderer());
3298
3299 QSSGRendererPrivate::setRenderContextInterface(*d->renderer, d->rhiCtxInterface.get());
3300
3301 QRhiCommandBuffer *cb;
3302 rhi->beginOffscreenFrame(&cb);
3303
3304 QSSGRhiContext *rhiCtx = d->rhiCtxInterface->rhiContext().get();
3305 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
3306 rhiCtxD->setCommandBuffer(cb);
3307
3308 d->rhiCtxInterface->bufferManager()->setRenderContextInterface(d->rhiCtxInterface.get());
3309
3310 constexpr int timerIntervalMs = 100;
3311 TimerThread timerThread;
3312 timerThread.setInterval(timerIntervalMs);
3313 // Log ETA every 5 seconds to console
3314 constexpr int consoleOutputInterval = 5000 / timerIntervalMs;
3315 int timeoutsSinceOutput = consoleOutputInterval - 1;
3316 timerThread.setCallback([&]() {
3317 d->totalProgress = d->progressTracker.getProgress();
3318 d->estimatedTimeRemaining = d->progressTracker.getEstimatedTimeRemaining();
3319 bool outputToConsole = timeoutsSinceOutput == consoleOutputInterval - 1;
3320 d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, std::nullopt, outputToConsole, outputToConsole);
3321 timeoutsSinceOutput = (timeoutsSinceOutput + 1) % consoleOutputInterval;
3322 });
3323 timerThread.start();
3324
3325 if (d->denoiseOnly) {
3326 denoise();
3327 } else {
3328 bake();
3329 }
3330
3331 rhi->endOffscreenFrame();
3332 rhi->finish();
3333
3334 d->renderer.reset();
3335 d->rhiCtxInterface.reset();
3336}
3337
3338void QSSGLightmapper::waitForInit()
3339{
3340 d->initMutex.lock();
3341 d->initCondition.wait(&d->initMutex);
3342 d->initMutex.unlock();
3343}
3344
3345#else
3346
3350
3354
3356{
3357}
3358
3359void QSSGLightmapper::setOptions(const QSSGLightmapperOptions &)
3360{
3361}
3362
3366
3368{
3369 return 0;
3370}
3371
3375
3377{
3378 return false;
3379}
3380
3382{
3383}
3384
3385void QSSGLightmapper::run(QOffscreenSurface *)
3386{
3387 qWarning("Qt Quick 3D was built without the lightmapper; cannot bake lightmaps");
3388}
3389
3391{
3392}
3393
3394bool QSSGLightmapper::bake()
3395{
3396 return false;
3397}
3398
3399bool QSSGLightmapper::denoise()
3400{
3401 return false;
3402}
3403
3404#endif // QT_QUICK3D_HAS_LIGHTMAPPER
3405
3406QT_END_NAMESPACE
3407
3408#include "qssglightmapper.moc" // Included because of TimerThread (QThread sublcass)
void setRhiBackend(QRhi::Implementation backend)
bool setupLights(const QSSGRenderer &renderer)
qsizetype add(const QSSGBakedLightingModel &model)
void setOptions(const QSSGLightmapperOptions &options)
std::function< void(const QVariantMap &payload, BakingControl *)> Callback
void setDenoiseOnly(bool value)
void run(QOffscreenSurface *fallbackSurface)
void setOutputCallback(Callback callback)
Combined button and popup list for selecting options.