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

source code of qtquick3d/src/runtimerender/rendererimpl/qssglightmapper.cpp