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