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