1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qssglightmapper_p.h"
5#include <QtQuick3DRuntimeRender/private/qssgrenderer_p.h>
6#include <QtQuick3DRuntimeRender/private/qssgrhiquadrenderer_p.h>
7#include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
8#include <QtQuick3DRuntimeRender/private/qssgrendercontextcore_p.h>
9#include <QtQuick3DUtils/private/qssgutils_p.h>
10
11#ifdef QT_QUICK3D_HAS_LIGHTMAPPER
12#include <QtCore/qfuture.h>
13#include <QtCore/qfileinfo.h>
14#include <QtConcurrent/qtconcurrentrun.h>
15#include <QRandomGenerator>
16#include <qsimd.h>
17#include <embree3/rtcore.h>
18#include <tinyexr.h>
19#endif
20
21QT_BEGIN_NAMESPACE
22
23// References:
24// https://ndotl.wordpress.com/2018/08/29/baking-artifact-free-lightmaps/
25// https://www.scratchapixel.com/lessons/3d-basic-rendering/global-illumination-path-tracing/
26// https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/gdc2018-precomputedgiobalilluminationinfrostbite.pdf
27// https://therealmjp.github.io/posts/new-blog-series-lightmap-baking-and-spherical-gaussians/
28// https://computergraphics.stackexchange.com/questions/2316/is-russian-roulette-really-the-answer
29// https://computergraphics.stackexchange.com/questions/4664/does-cosine-weighted-hemisphere-sampling-still-require-ndotl-when-calculating-co
30// https://www.rorydriscoll.com/2009/01/07/better-sampling/
31// https://github.com/TheRealMJP/BakingLab
32// https://github.com/candycat1992/LightmapperToy
33// https://github.com/godotengine/
34// https://github.com/jpcy/xatlas
35
36#ifdef QT_QUICK3D_HAS_LIGHTMAPPER
37
38struct QSSGLightmapperPrivate
39{
40 QSSGLightmapperOptions options;
41 QSSGRhiContext *rhiCtx;
42 QSSGRenderer *renderer;
43 QVector<QSSGBakedLightingModel> bakedLightingModels;
44 QSSGLightmapper::Callback outputCallback;
45 QSSGLightmapper::BakingControl bakingControl;
46
47 struct SubMeshInfo {
48 quint32 offset = 0;
49 quint32 count = 0;
50 unsigned int geomId = RTC_INVALID_GEOMETRY_ID;
51 QVector4D baseColor;
52 QSSGRenderImage *baseColorNode = nullptr;
53 QRhiTexture *baseColorMap = nullptr;
54 QVector3D emissiveFactor;
55 QSSGRenderImage *emissiveNode = nullptr;
56 QRhiTexture *emissiveMap = nullptr;
57 QSSGRenderImage *normalMapNode = nullptr;
58 QRhiTexture *normalMap = nullptr;
59 float normalStrength = 0.0f;
60 float opacity = 0.0f;
61 };
62 using SubMeshInfoList = QVector<SubMeshInfo>;
63 QVector<SubMeshInfoList> subMeshInfos;
64
65 struct DrawInfo {
66 QSize lightmapSize;
67 QByteArray vertexData;
68 quint32 vertexStride;
69 QByteArray indexData;
70 QRhiCommandBuffer::IndexFormat indexFormat = QRhiCommandBuffer::IndexUInt32;
71 quint32 positionOffset = UINT_MAX;
72 QRhiVertexInputAttribute::Format positionFormat = QRhiVertexInputAttribute::Float;
73 quint32 normalOffset = UINT_MAX;
74 QRhiVertexInputAttribute::Format normalFormat = QRhiVertexInputAttribute::Float;
75 quint32 uvOffset = UINT_MAX;
76 QRhiVertexInputAttribute::Format uvFormat = QRhiVertexInputAttribute::Float;
77 quint32 lightmapUVOffset = UINT_MAX;
78 QRhiVertexInputAttribute::Format lightmapUVFormat = QRhiVertexInputAttribute::Float;
79 quint32 tangentOffset = UINT_MAX;
80 QRhiVertexInputAttribute::Format tangentFormat = QRhiVertexInputAttribute::Float;
81 quint32 binormalOffset = UINT_MAX;
82 QRhiVertexInputAttribute::Format binormalFormat = QRhiVertexInputAttribute::Float;
83 QSSGMesh::Mesh meshWithLightmapUV; // only set when model->hasLightmap() == true
84 };
85 QVector<DrawInfo> drawInfos;
86
87 struct Light {
88 enum {
89 Directional,
90 Point,
91 Spot
92 } type;
93 bool indirectOnly;
94 QVector3D direction;
95 QVector3D color;
96 QVector3D worldPos;
97 float cosConeAngle;
98 float cosInnerConeAngle;
99 float constantAttenuation;
100 float linearAttenuation;
101 float quadraticAttenuation;
102 };
103 QVector<Light> lights;
104
105 RTCDevice rdev = nullptr;
106 RTCScene rscene = nullptr;
107
108 struct LightmapEntry {
109 QSize pixelSize;
110 QVector3D worldPos;
111 QVector3D normal;
112 QVector4D baseColor; // static color * texture map value (both linear)
113 QVector3D emission; // static factor * emission map value
114 bool isValid() const { return !worldPos.isNull() && !normal.isNull(); }
115 QVector3D directLight;
116 QVector3D allLight;
117 };
118 struct Lightmap {
119 Lightmap(const QSize &pixelSize) : pixelSize(pixelSize) {
120 entries.resize(size: pixelSize.width() * pixelSize.height());
121 }
122 QSize pixelSize;
123 QVector<LightmapEntry> entries;
124 QByteArray imageFP32;
125 bool hasBaseColorTransparency = false;
126 };
127 QVector<Lightmap> lightmaps;
128 QVector<int> geomLightmapMap; // [geomId] -> index in lightmaps (NB lightmap is per-model, geomId is per-submesh)
129 QVector<float> subMeshOpacityMap; // [geomId] -> opacity
130
131 inline const LightmapEntry &texelForLightmapUV(unsigned int geomId, float u, float v) const
132 {
133 // find the hit texel in the lightmap for the model to which the submesh with geomId belongs
134 const Lightmap &hitLightmap(lightmaps[geomLightmapMap[geomId]]);
135 u = qBound(min: 0.0f, val: u, max: 1.0f);
136 // flip V, CPU-side data is top-left based
137 v = 1.0f - qBound(min: 0.0f, val: v, max: 1.0f);
138
139 const int w = hitLightmap.pixelSize.width();
140 const int h = hitLightmap.pixelSize.height();
141 const int x = qBound(min: 0, val: int(w * u), max: w - 1);
142 const int y = qBound(min: 0, val: int(h * v), max: h - 1);
143
144 return hitLightmap.entries[x + y * w];
145 }
146
147 bool commitGeometry();
148 bool prepareLightmaps();
149 void computeDirectLight();
150 void computeIndirectLight();
151 bool postProcess();
152 bool storeLightmaps();
153 void sendOutputInfo(QSSGLightmapper::BakingStatus type, std::optional<QString> msg);
154};
155
156static const int LM_SEAM_BLEND_ITER_COUNT = 4;
157
158QSSGLightmapper::QSSGLightmapper(QSSGRhiContext *rhiCtx, QSSGRenderer *renderer)
159 : d(new QSSGLightmapperPrivate)
160{
161 d->rhiCtx = rhiCtx;
162 d->renderer = renderer;
163
164#ifdef __SSE2__
165 _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
166 _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
167#endif
168}
169
170QSSGLightmapper::~QSSGLightmapper()
171{
172 reset();
173 delete d;
174
175#ifdef __SSE2__
176 _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_OFF);
177 _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_OFF);
178#endif
179}
180
181void QSSGLightmapper::reset()
182{
183 d->bakedLightingModels.clear();
184 d->subMeshInfos.clear();
185 d->drawInfos.clear();
186 d->lights.clear();
187 d->lightmaps.clear();
188 d->geomLightmapMap.clear();
189 d->subMeshOpacityMap.clear();
190
191 if (d->rscene) {
192 rtcReleaseScene(scene: d->rscene);
193 d->rscene = nullptr;
194 }
195 if (d->rdev) {
196 rtcReleaseDevice(device: d->rdev);
197 d->rdev = nullptr;
198 }
199
200 d->bakingControl.cancelled = false;
201}
202
203void QSSGLightmapper::setOptions(const QSSGLightmapperOptions &options)
204{
205 d->options = options;
206}
207
208void QSSGLightmapper::setOutputCallback(Callback callback)
209{
210 d->outputCallback = callback;
211}
212
213qsizetype QSSGLightmapper::add(const QSSGBakedLightingModel &model)
214{
215 d->bakedLightingModels.append(t: model);
216 return d->bakedLightingModels.size() - 1;
217}
218
219static void embreeErrFunc(void *, RTCError error, const char *str)
220{
221 qWarning(msg: "lm: Embree error: %d: %s", error, str);
222}
223
224static const unsigned int NORMAL_SLOT = 0;
225static const unsigned int LIGHTMAP_UV_SLOT = 1;
226
227static void embreeFilterFunc(const RTCFilterFunctionNArguments *args)
228{
229 RTCHit *hit = reinterpret_cast<RTCHit *>(args->hit);
230 QSSGLightmapperPrivate *d = static_cast<QSSGLightmapperPrivate *>(args->geometryUserPtr);
231 RTCGeometry geom = rtcGetGeometry(scene: d->rscene, geomID: hit->geomID);
232
233 // convert from barycentric and overwrite u and v in hit with the result
234 rtcInterpolate0(geometry: geom, primID: hit->primID, u: hit->u, v: hit->v, bufferType: RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, bufferSlot: LIGHTMAP_UV_SLOT, P: &hit->u, valueCount: 2);
235
236 const float opacity = d->subMeshOpacityMap[hit->geomID];
237 if (opacity < 1.0f || d->lightmaps[d->geomLightmapMap[hit->geomID]].hasBaseColorTransparency) {
238 const QSSGLightmapperPrivate::LightmapEntry &texel(d->texelForLightmapUV(geomId: hit->geomID, u: hit->u, v: hit->v));
239
240 // In addition to material.opacity, take at least the base color (both
241 // the static color and the value from the base color map, if there is
242 // one) into account. Opacity map, alpha cutoff, etc. are ignored.
243 const float alpha = opacity * texel.baseColor.w();
244
245 // Ignore the hit if the alpha is low enough. This is not exactly perfect,
246 // but better than nothing. An object with an opacity lower than the
247 // threshold will act is if it was not there, as far as the intersection is
248 // concerned. So then the object won't cast shadows for example.
249 if (alpha < d->options.opacityThreshold)
250 args->valid[0] = 0;
251 }
252}
253
254bool QSSGLightmapperPrivate::commitGeometry()
255{
256 if (bakedLightingModels.isEmpty()) {
257 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No models with usedInBakedLighting, cannot bake"));
258 return false;
259 }
260
261 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Geometry setup..."));
262 QElapsedTimer geomPrepTimer;
263 geomPrepTimer.start();
264
265 const auto &bufferManager(renderer->contextInterface()->bufferManager());
266
267 const int bakedLightingModelCount = bakedLightingModels.size();
268 subMeshInfos.resize(size: bakedLightingModelCount);
269 drawInfos.resize(size: bakedLightingModelCount);
270
271 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
272 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
273 if (lm.renderables.isEmpty()) {
274 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No submeshes, model %1 cannot be lightmapped").
275 arg(a: lm.model->debugObjectName));
276 return false;
277 }
278 if (lm.model->skin || lm.model->skeleton) {
279 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Skinned models not supported: %1").
280 arg(a: lm.model->debugObjectName));
281 return false;
282 }
283
284 subMeshInfos[lmIdx].reserve(asize: lm.renderables.size());
285 for (const QSSGRenderableObjectHandle &handle : std::as_const(t: lm.renderables)) {
286 Q_ASSERT(handle.obj->type == QSSGRenderableObject::Type::DefaultMaterialMeshSubset
287 || handle.obj->type == QSSGRenderableObject::Type::CustomMaterialMeshSubset);
288 QSSGSubsetRenderable *renderableObj = static_cast<QSSGSubsetRenderable *>(handle.obj);
289 SubMeshInfo info;
290 info.offset = renderableObj->subset.offset;
291 info.count = renderableObj->subset.count;
292 info.opacity = renderableObj->opacity;
293 if (handle.obj->type == QSSGRenderableObject::Type::DefaultMaterialMeshSubset) {
294 const QSSGRenderDefaultMaterial *defMat = static_cast<const QSSGRenderDefaultMaterial *>(&renderableObj->material);
295 info.baseColor = defMat->color;
296 info.emissiveFactor = defMat->emissiveColor;
297 if (defMat->colorMap) {
298 info.baseColorNode = defMat->colorMap;
299 QSSGRenderImageTexture texture = bufferManager->loadRenderImage(image: defMat->colorMap);
300 info.baseColorMap = texture.m_texture;
301 }
302 if (defMat->emissiveMap) {
303 info.emissiveNode = defMat->emissiveMap;
304 QSSGRenderImageTexture texture = bufferManager->loadRenderImage(image: defMat->emissiveMap);
305 info.emissiveMap = texture.m_texture;
306 }
307 if (defMat->normalMap) {
308 info.normalMapNode = defMat->normalMap;
309 QSSGRenderImageTexture texture = bufferManager->loadRenderImage(image: defMat->normalMap);
310 info.normalMap = texture.m_texture;
311 info.normalStrength = defMat->bumpAmount;
312 }
313 } else {
314 info.baseColor = QVector4D(1.0f, 1.0f, 1.0f, 1.0f);
315 info.emissiveFactor = QVector3D(0.0f, 0.0f, 0.0f);
316 }
317 subMeshInfos[lmIdx].append(t: info);
318 }
319
320 QMatrix4x4 worldTransform;
321 QMatrix3x3 normalMatrix;
322 QSSGSubsetRenderable *renderableObj = static_cast<QSSGSubsetRenderable *>(lm.renderables.first().obj);
323 worldTransform = renderableObj->globalTransform;
324 normalMatrix = renderableObj->modelContext.normalMatrix;
325
326 DrawInfo &drawInfo(drawInfos[lmIdx]);
327 QSSGMesh::Mesh mesh;
328
329 if (lm.model->geometry)
330 mesh = bufferManager->loadMeshData(geometry: lm.model->geometry);
331 else
332 mesh = bufferManager->loadMeshData(inSourcePath: lm.model->meshPath);
333
334 if (!mesh.isValid()) {
335 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load geometry for model %1").
336 arg(a: lm.model->debugObjectName));
337 return false;
338 }
339
340 if (!mesh.hasLightmapUVChannel()) {
341 QElapsedTimer unwrapTimer;
342 unwrapTimer.start();
343 if (!mesh.createLightmapUVChannel(lightmapBaseResolution: lm.model->lightmapBaseResolution)) {
344 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to do lightmap UV unwrapping for model %1").
345 arg(a: lm.model->debugObjectName));
346 return false;
347 }
348 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Lightmap UV unwrap done for model %1 in %2 ms").
349 arg(a: lm.model->debugObjectName).
350 arg(a: unwrapTimer.elapsed()));
351
352 if (lm.model->hasLightmap())
353 drawInfo.meshWithLightmapUV = mesh;
354 } else {
355 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Model %1 already has a lightmap UV channel").arg(a: lm.model->debugObjectName));
356 }
357
358 drawInfo.lightmapSize = mesh.subsets().first().lightmapSizeHint;
359 if (drawInfo.lightmapSize.isEmpty()) {
360 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No lightmap size hint found for model %1, defaulting to 1024x1024").
361 arg(a: lm.model->debugObjectName));
362 drawInfo.lightmapSize = QSize(1024, 1024);
363 }
364
365 drawInfo.vertexData = mesh.vertexBuffer().data;
366 drawInfo.vertexStride = mesh.vertexBuffer().stride;
367 drawInfo.indexData = mesh.indexBuffer().data;
368
369 if (drawInfo.vertexData.isEmpty()) {
370 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No vertex data for model %1").arg(a: lm.model->debugObjectName));
371 return false;
372 }
373 if (drawInfo.indexData.isEmpty()) {
374 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("No index data for model %1").arg(a: lm.model->debugObjectName));
375 return false;
376 }
377
378 switch (mesh.indexBuffer().componentType) {
379 case QSSGMesh::Mesh::ComponentType::UnsignedInt16:
380 drawInfo.indexFormat = QRhiCommandBuffer::IndexUInt16;
381 break;
382 case QSSGMesh::Mesh::ComponentType::UnsignedInt32:
383 drawInfo.indexFormat = QRhiCommandBuffer::IndexUInt32;
384 break;
385 default:
386 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Unknown index component type %1 for model %2").
387 arg(a: int(mesh.indexBuffer().componentType)).
388 arg(a: lm.model->debugObjectName));
389 break;
390 }
391
392 for (const QSSGMesh::Mesh::VertexBufferEntry &vbe : mesh.vertexBuffer().entries) {
393 if (vbe.name == QSSGMesh::MeshInternal::getPositionAttrName()) {
394 drawInfo.positionOffset = vbe.offset;
395 drawInfo.positionFormat = QSSGRhiInputAssemblerState::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
396 } else if (vbe.name == QSSGMesh::MeshInternal::getNormalAttrName()) {
397 drawInfo.normalOffset = vbe.offset;
398 drawInfo.normalFormat = QSSGRhiInputAssemblerState::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
399 } else if (vbe.name == QSSGMesh::MeshInternal::getUV0AttrName()) {
400 drawInfo.uvOffset = vbe.offset;
401 drawInfo.uvFormat = QSSGRhiInputAssemblerState::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
402 } else if (vbe.name == QSSGMesh::MeshInternal::getLightmapUVAttrName()) {
403 drawInfo.lightmapUVOffset = vbe.offset;
404 drawInfo.lightmapUVFormat = QSSGRhiInputAssemblerState::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
405 } else if (vbe.name == QSSGMesh::MeshInternal::getTexTanAttrName()) {
406 drawInfo.tangentOffset = vbe.offset;
407 drawInfo.tangentFormat = QSSGRhiInputAssemblerState::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
408 } else if (vbe.name == QSSGMesh::MeshInternal::getTexBinormalAttrName()) {
409 drawInfo.binormalOffset = vbe.offset;
410 drawInfo.binormalFormat = QSSGRhiInputAssemblerState::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
411 }
412 }
413
414 if (!(drawInfo.positionOffset != UINT_MAX && drawInfo.normalOffset != UINT_MAX)) {
415 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Could not figure out position and normal attribute offsets for model %1").
416 arg(a: lm.model->debugObjectName));
417 return false;
418 }
419
420 // We will manually access and massage the data, so cannot just work with arbitrary formats.
421 if (!(drawInfo.positionFormat == QRhiVertexInputAttribute::Float3
422 && drawInfo.normalFormat == QRhiVertexInputAttribute::Float3))
423 {
424 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Position or normal attribute format is not as expected (float3) for model %1").
425 arg(a: lm.model->debugObjectName));
426 return false;
427 }
428
429 if (drawInfo.lightmapUVOffset == UINT_MAX) {
430 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Could not figure out lightmap UV attribute offset for model %1").
431 arg(a: lm.model->debugObjectName));
432 return false;
433 }
434
435 if (drawInfo.lightmapUVFormat != QRhiVertexInputAttribute::Float2) {
436 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Lightmap UV attribute format is not as expected (float2) for model %1").
437 arg(a: lm.model->debugObjectName));
438 return false;
439 }
440
441 // UV0 is optional
442 if (drawInfo.uvOffset != UINT_MAX) {
443 if (drawInfo.uvFormat != QRhiVertexInputAttribute::Float2) {
444 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("UV0 attribute format is not as expected (float2) for model %1").
445 arg(a: lm.model->debugObjectName));
446 return false;
447 }
448 }
449 // tangent and binormal are optional too
450 if (drawInfo.tangentOffset != UINT_MAX) {
451 if (drawInfo.tangentFormat != QRhiVertexInputAttribute::Float3) {
452 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Tangent attribute format is not as expected (float3) for model %1").
453 arg(a: lm.model->debugObjectName));
454 return false;
455 }
456 }
457 if (drawInfo.binormalOffset != UINT_MAX) {
458 if (drawInfo.binormalFormat != QRhiVertexInputAttribute::Float3) {
459 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Binormal attribute format is not as expected (float3) for model %1").
460 arg(a: lm.model->debugObjectName));
461 return false;
462 }
463 }
464
465 if (drawInfo.indexFormat == QRhiCommandBuffer::IndexUInt16) {
466 drawInfo.indexFormat = QRhiCommandBuffer::IndexUInt32;
467 QByteArray newIndexData(drawInfo.indexData.size() * 2, Qt::Uninitialized);
468 const quint16 *s = reinterpret_cast<const quint16 *>(drawInfo.indexData.constData());
469 size_t sz = drawInfo.indexData.size() / 2;
470 quint32 *p = reinterpret_cast<quint32 *>(newIndexData.data());
471 while (sz--)
472 *p++ = *s++;
473 drawInfo.indexData = newIndexData;
474 }
475
476 // Bake in the world transform.
477 {
478 char *vertexBase = drawInfo.vertexData.data();
479 const qsizetype sz = drawInfo.vertexData.size();
480 for (qsizetype offset = 0; offset < sz; offset += drawInfo.vertexStride) {
481 char *posPtr = vertexBase + offset + drawInfo.positionOffset;
482 float *fPosPtr = reinterpret_cast<float *>(posPtr);
483 QVector3D pos(fPosPtr[0], fPosPtr[1], fPosPtr[2]);
484 char *normalPtr = vertexBase + offset + drawInfo.normalOffset;
485 float *fNormalPtr = reinterpret_cast<float *>(normalPtr);
486 QVector3D normal(fNormalPtr[0], fNormalPtr[1], fNormalPtr[2]);
487 pos = worldTransform.map(point: pos);
488 normal = QSSGUtils::mat33::transform(m: normalMatrix, v: normal).normalized();
489 *fPosPtr++ = pos.x();
490 *fPosPtr++ = pos.y();
491 *fPosPtr++ = pos.z();
492 *fNormalPtr++ = normal.x();
493 *fNormalPtr++ = normal.y();
494 *fNormalPtr++ = normal.z();
495 }
496 }
497 } // end loop over models used in the lightmap
498
499 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Found %1 models for the lightmapped scene").arg(a: bakedLightingModelCount));
500
501 // All subsets for a model reference the same QSSGShaderLight list,
502 // take the first one, but filter it based on the bake flag.
503 for (const QSSGShaderLight &sl : static_cast<QSSGSubsetRenderable *>(bakedLightingModels.first().renderables.first().obj)->lights) {
504 if (!sl.light->m_bakingEnabled)
505 continue;
506
507 Light light;
508 light.indirectOnly = !sl.light->m_fullyBaked;
509 light.direction = sl.direction;
510
511 const float brightness = sl.light->m_brightness;
512 light.color = QVector3D(sl.light->m_diffuseColor.x() * brightness,
513 sl.light->m_diffuseColor.y() * brightness,
514 sl.light->m_diffuseColor.z() * brightness);
515
516 if (sl.light->type == QSSGRenderLight::Type::PointLight
517 || sl.light->type == QSSGRenderLight::Type::SpotLight)
518 {
519 light.worldPos = sl.light->getGlobalPos();
520 if (sl.light->type == QSSGRenderLight::Type::SpotLight) {
521 light.type = Light::Spot;
522 light.cosConeAngle = qCos(v: qDegreesToRadians(degrees: sl.light->m_coneAngle));
523 light.cosInnerConeAngle = qCos(v: qDegreesToRadians(
524 degrees: qMin(a: sl.light->m_innerConeAngle, b: sl.light->m_coneAngle)));
525 } else {
526 light.type = Light::Point;
527 }
528 light.constantAttenuation = QSSGUtils::aux::translateConstantAttenuation(attenuation: sl.light->m_constantFade);
529 light.linearAttenuation = QSSGUtils::aux::translateLinearAttenuation(attenuation: sl.light->m_linearFade);
530 light.quadraticAttenuation = QSSGUtils::aux::translateQuadraticAttenuation(attenuation: sl.light->m_quadraticFade);
531 } else {
532 light.type = Light::Directional;
533 }
534
535 lights.append(t: light);
536 }
537
538 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Found %1 lights enabled for baking").arg(a: lights.size()));
539
540 rdev = rtcNewDevice(config: nullptr);
541 if (!rdev) {
542 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create Embree device"));
543 return false;
544 }
545
546 rtcSetDeviceErrorFunction(device: rdev, error: embreeErrFunc, userPtr: nullptr);
547
548 rscene = rtcNewScene(device: rdev);
549
550 unsigned int geomId = 1;
551
552 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
553 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
554
555 // While Light.castsShadow and Model.receivesShadows are irrelevant for
556 // baked lighting (they are effectively ignored, shadows are always
557 // there with baked direct lighting), Model.castsShadows is something
558 // we can and should take into account.
559 if (!lm.model->castsShadows)
560 continue;
561
562 const DrawInfo &drawInfo(drawInfos[lmIdx]);
563 const char *vbase = drawInfo.vertexData.constData();
564 const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
565
566 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
567 RTCGeometry geom = rtcNewGeometry(device: rdev, type: RTC_GEOMETRY_TYPE_TRIANGLE);
568 rtcSetGeometryVertexAttributeCount(geometry: geom, vertexAttributeCount: 2);
569 quint32 *ip = static_cast<quint32 *>(rtcSetNewGeometryBuffer(geometry: geom, type: RTC_BUFFER_TYPE_INDEX, slot: 0, format: RTC_FORMAT_UINT3, byteStride: 3 * sizeof(uint32_t), itemCount: subMeshInfo.count / 3));
570 for (quint32 i = 0; i < subMeshInfo.count; ++i)
571 *ip++ = i;
572 float *vp = static_cast<float *>(rtcSetNewGeometryBuffer(geometry: geom, type: RTC_BUFFER_TYPE_VERTEX, slot: 0, format: RTC_FORMAT_FLOAT3, byteStride: 3 * sizeof(float), itemCount: subMeshInfo.count));
573 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
574 const quint32 idx = *(ibase + subMeshInfo.offset + i);
575 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
576 *vp++ = *src++;
577 *vp++ = *src++;
578 *vp++ = *src++;
579 }
580 vp = static_cast<float *>(rtcSetNewGeometryBuffer(geometry: geom, type: RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, slot: NORMAL_SLOT, format: RTC_FORMAT_FLOAT3, byteStride: 3 * sizeof(float), itemCount: subMeshInfo.count));
581 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
582 const quint32 idx = *(ibase + subMeshInfo.offset + i);
583 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
584 *vp++ = *src++;
585 *vp++ = *src++;
586 *vp++ = *src++;
587 }
588 vp = static_cast<float *>(rtcSetNewGeometryBuffer(geometry: geom, type: RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, slot: LIGHTMAP_UV_SLOT, format: RTC_FORMAT_FLOAT2, byteStride: 2 * sizeof(float), itemCount: subMeshInfo.count));
589 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
590 const quint32 idx = *(ibase + subMeshInfo.offset + i);
591 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
592 *vp++ = *src++;
593 *vp++ = *src++;
594 }
595 rtcCommitGeometry(geometry: geom);
596 rtcSetGeometryIntersectFilterFunction(geometry: geom, filter: embreeFilterFunc);
597 rtcSetGeometryUserData(geometry: geom, ptr: this);
598 rtcAttachGeometryByID(scene: rscene, geometry: geom, geomID: geomId);
599 subMeshInfo.geomId = geomId++;
600 rtcReleaseGeometry(geometry: geom);
601 }
602 }
603
604 rtcCommitScene(scene: rscene);
605
606 RTCBounds bounds;
607 rtcGetSceneBounds(scene: rscene, bounds_o: &bounds);
608 QVector3D lowerBound(bounds.lower_x, bounds.lower_y, bounds.lower_z);
609 QVector3D upperBound(bounds.upper_x, bounds.upper_y, bounds.upper_z);
610 qDebug() << "[lm] Bounds in world space for raytracing scene:" << lowerBound << upperBound;
611
612 const unsigned int geomIdBasedMapSize = geomId;
613 // Need fast lookup, hence indexing by geomId here. geomId starts from 1,
614 // meaning index 0 will be unused, but that's ok.
615 geomLightmapMap.fill(t: -1, newSize: geomIdBasedMapSize);
616 subMeshOpacityMap.fill(t: 0.0f, newSize: geomIdBasedMapSize);
617
618 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
619 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
620 if (!lm.model->castsShadows) // only matters if it's in the raytracer scene
621 continue;
622 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx])
623 subMeshOpacityMap[subMeshInfo.geomId] = subMeshInfo.opacity;
624 }
625
626 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Geometry setup done. Time taken: %1 ms").arg(a: geomPrepTimer.elapsed()));
627 return true;
628}
629
630bool QSSGLightmapperPrivate::prepareLightmaps()
631{
632 QRhi *rhi = rhiCtx->rhi();
633 if (!rhi->isTextureFormatSupported(format: QRhiTexture::RGBA32F)) {
634 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("FP32 textures not supported, cannot bake"));
635 return false;
636 }
637 if (rhi->resourceLimit(limit: QRhi::MaxColorAttachments) < 4) {
638 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Multiple render targets not supported, cannot bake"));
639 return false;
640 }
641 if (!rhi->isFeatureSupported(feature: QRhi::NonFillPolygonMode)) {
642 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Line polygon mode not supported, cannot bake"));
643 return false;
644 }
645
646 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Preparing lightmaps..."));
647 QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
648 const int bakedLightingModelCount = bakedLightingModels.size();
649 Q_ASSERT(drawInfos.size() == bakedLightingModelCount);
650 Q_ASSERT(subMeshInfos.size() == bakedLightingModelCount);
651
652 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
653 QElapsedTimer rasterizeTimer;
654 rasterizeTimer.start();
655
656 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
657
658 const DrawInfo &bakeModelDrawInfo(drawInfos[lmIdx]);
659 const bool hasUV0 = bakeModelDrawInfo.uvOffset != UINT_MAX;
660 const bool hasTangentAndBinormal = bakeModelDrawInfo.tangentOffset != UINT_MAX
661 && bakeModelDrawInfo.binormalOffset != UINT_MAX;
662 const QSize outputSize = bakeModelDrawInfo.lightmapSize;
663
664 QRhiVertexInputLayout inputLayout;
665 inputLayout.setBindings({ QRhiVertexInputBinding(bakeModelDrawInfo.vertexStride) });
666
667 std::unique_ptr<QRhiBuffer> vbuf(rhi->newBuffer(type: QRhiBuffer::Immutable, usage: QRhiBuffer::VertexBuffer, size: bakeModelDrawInfo.vertexData.size()));
668 if (!vbuf->create()) {
669 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create vertex buffer"));
670 return false;
671 }
672 std::unique_ptr<QRhiBuffer> ibuf(rhi->newBuffer(type: QRhiBuffer::Immutable, usage: QRhiBuffer::IndexBuffer, size: bakeModelDrawInfo.indexData.size()));
673 if (!ibuf->create()) {
674 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create index buffer"));
675 return false;
676 }
677 QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
678 resUpd->uploadStaticBuffer(buf: vbuf.get(), data: bakeModelDrawInfo.vertexData.constData());
679 resUpd->uploadStaticBuffer(buf: ibuf.get(), data: bakeModelDrawInfo.indexData.constData());
680 QRhiTexture *dummyTexture = rhiCtx->dummyTexture(flags: {}, rub: resUpd);
681 cb->resourceUpdate(resourceUpdates: resUpd);
682
683 std::unique_ptr<QRhiTexture> positionData(rhi->newTexture(format: QRhiTexture::RGBA32F, pixelSize: outputSize, sampleCount: 1,
684 flags: QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
685 if (!positionData->create()) {
686 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for positions"));
687 return false;
688 }
689 std::unique_ptr<QRhiTexture> normalData(rhi->newTexture(format: QRhiTexture::RGBA32F, pixelSize: outputSize, sampleCount: 1,
690 flags: QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
691 if (!normalData->create()) {
692 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for normals"));
693 return false;
694 }
695 std::unique_ptr<QRhiTexture> baseColorData(rhi->newTexture(format: QRhiTexture::RGBA32F, pixelSize: outputSize, sampleCount: 1,
696 flags: QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
697 if (!baseColorData->create()) {
698 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for base color"));
699 return false;
700 }
701 std::unique_ptr<QRhiTexture> emissionData(rhi->newTexture(format: QRhiTexture::RGBA32F, pixelSize: outputSize, sampleCount: 1,
702 flags: QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
703 if (!emissionData->create()) {
704 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for emissive color"));
705 return false;
706 }
707
708 std::unique_ptr<QRhiRenderBuffer> ds(rhi->newRenderBuffer(type: QRhiRenderBuffer::DepthStencil, pixelSize: outputSize));
709 if (!ds->create()) {
710 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create depth-stencil buffer"));
711 return false;
712 }
713
714 QRhiColorAttachment posAtt(positionData.get());
715 QRhiColorAttachment normalAtt(normalData.get());
716 QRhiColorAttachment baseColorAtt(baseColorData.get());
717 QRhiColorAttachment emissionAtt(emissionData.get());
718 QRhiTextureRenderTargetDescription rtDesc;
719 rtDesc.setColorAttachments({ posAtt, normalAtt, baseColorAtt, emissionAtt });
720 rtDesc.setDepthStencilBuffer(ds.get());
721
722 std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(desc: rtDesc));
723 std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
724 rt->setRenderPassDescriptor(rpDesc.get());
725 if (!rt->create()) {
726 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create texture render target"));
727 return false;
728 }
729
730 static const int UBUF_SIZE = 48;
731 const int subMeshCount = subMeshInfos[lmIdx].size();
732 const int alignedUbufSize = rhi->ubufAligned(v: UBUF_SIZE);
733 const int totalUbufSize = alignedUbufSize * subMeshCount;
734 std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(type: QRhiBuffer::Dynamic, usage: QRhiBuffer::UniformBuffer, size: totalUbufSize));
735 if (!ubuf->create()) {
736 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create uniform buffer of size %1").arg(a: totalUbufSize));
737 return false;
738 }
739
740 // Must ensure that the final image is identical with all graphics APIs,
741 // regardless of how the Y axis goes in the image and normalized device
742 // coordinate systems.
743 qint32 flipY = rhi->isYUpInFramebuffer() ? 0 : 1;
744 if (rhi->isYUpInNDC())
745 flipY = 1 - flipY;
746
747 char *ubufData = ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
748 for (int subMeshIdx = 0; subMeshIdx != subMeshCount; ++subMeshIdx) {
749 const SubMeshInfo &subMeshInfo(subMeshInfos[lmIdx][subMeshIdx]);
750 qint32 hasBaseColorMap = subMeshInfo.baseColorMap ? 1 : 0;
751 qint32 hasEmissiveMap = subMeshInfo.emissiveMap ? 1 : 0;
752 qint32 hasNormalMap = subMeshInfo.normalMap ? 1 : 0;
753 char *p = ubufData + subMeshIdx * alignedUbufSize;
754 memcpy(dest: p, src: &subMeshInfo.baseColor, n: 4 * sizeof(float));
755 memcpy(dest: p + 16, src: &subMeshInfo.emissiveFactor, n: 3 * sizeof(float));
756 memcpy(dest: p + 28, src: &flipY, n: sizeof(qint32));
757 memcpy(dest: p + 32, src: &hasBaseColorMap, n: sizeof(qint32));
758 memcpy(dest: p + 36, src: &hasEmissiveMap, n: sizeof(qint32));
759 memcpy(dest: p + 40, src: &hasNormalMap, n: sizeof(qint32));
760 memcpy(dest: p + 44, src: &subMeshInfo.normalStrength, n: sizeof(float));
761 }
762 ubuf->endFullDynamicBufferUpdateForCurrentFrame();
763
764 auto setupPipeline = [rhi, &rpDesc](QSSGRhiShaderPipeline *shaderPipeline,
765 QRhiShaderResourceBindings *srb,
766 const QRhiVertexInputLayout &inputLayout)
767 {
768 QRhiGraphicsPipeline *ps = rhi->newGraphicsPipeline();
769 ps->setTopology(QRhiGraphicsPipeline::Triangles);
770 ps->setDepthTest(true);
771 ps->setDepthWrite(true);
772 ps->setDepthOp(QRhiGraphicsPipeline::Less);
773 ps->setShaderStages(first: shaderPipeline->cbeginStages(), last: shaderPipeline->cendStages());
774 ps->setTargetBlends({ {}, {}, {}, {} });
775 ps->setRenderPassDescriptor(rpDesc.get());
776 ps->setVertexInputLayout(inputLayout);
777 ps->setShaderResourceBindings(srb);
778 return ps;
779 };
780
781 QVector<QRhiGraphicsPipeline *> ps;
782 // Everything is going to be rendered twice (but note depth testing), first
783 // with polygon mode fill, then line.
784 QVector<QRhiGraphicsPipeline *> psLine;
785
786 for (int subMeshIdx = 0; subMeshIdx != subMeshCount; ++subMeshIdx) {
787 const SubMeshInfo &subMeshInfo(subMeshInfos[lmIdx][subMeshIdx]);
788 QVarLengthArray<QRhiVertexInputAttribute, 6> vertexAttrs;
789 vertexAttrs << QRhiVertexInputAttribute(0, 0, bakeModelDrawInfo.positionFormat, bakeModelDrawInfo.positionOffset)
790 << QRhiVertexInputAttribute(0, 1, bakeModelDrawInfo.normalFormat, bakeModelDrawInfo.normalOffset)
791 << QRhiVertexInputAttribute(0, 2, bakeModelDrawInfo.lightmapUVFormat, bakeModelDrawInfo.lightmapUVOffset);
792
793 // Vertex inputs (just like the sampler uniforms) must match exactly on
794 // the shader and the application side, cannot just leave out or have
795 // unused inputs.
796 QSSGRenderer::LightmapUVRasterizationShaderMode shaderVariant = QSSGRenderer::LightmapUVRasterizationShaderMode::Default;
797 if (hasUV0) {
798 shaderVariant = QSSGRenderer::LightmapUVRasterizationShaderMode::Uv;
799 if (hasTangentAndBinormal)
800 shaderVariant = QSSGRenderer::LightmapUVRasterizationShaderMode::UvTangent;
801 }
802
803 const auto &lmUvRastShaderPipeline = renderer->getRhiLightmapUVRasterizationShader(mode: shaderVariant);
804 if (!lmUvRastShaderPipeline) {
805 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load shaders"));
806 return false;
807 }
808
809 if (hasUV0) {
810 vertexAttrs << QRhiVertexInputAttribute(0, 3, bakeModelDrawInfo.uvFormat, bakeModelDrawInfo.uvOffset);
811 if (hasTangentAndBinormal) {
812 vertexAttrs << QRhiVertexInputAttribute(0, 4, bakeModelDrawInfo.tangentFormat, bakeModelDrawInfo.tangentOffset);
813 vertexAttrs << QRhiVertexInputAttribute(0, 5, bakeModelDrawInfo.binormalFormat, bakeModelDrawInfo.binormalOffset);
814 }
815 }
816
817 inputLayout.setAttributes(first: vertexAttrs.cbegin(), last: vertexAttrs.cend());
818
819 QSSGRhiShaderResourceBindingList bindings;
820 bindings.addUniformBuffer(binding: 0, stage: QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, buf: ubuf.get(),
821 offset: subMeshIdx * alignedUbufSize, size: UBUF_SIZE);
822 QRhiSampler *dummySampler = rhiCtx->sampler(samplerDescription: { .minFilter: QRhiSampler::Nearest, .magFilter: QRhiSampler::Nearest, .mipmap: QRhiSampler::None,
823 .hTiling: QRhiSampler::ClampToEdge, .vTiling: QRhiSampler::ClampToEdge, .zTiling: QRhiSampler::Repeat });
824 if (subMeshInfo.baseColorMap) {
825 const bool mipmapped = subMeshInfo.baseColorMap->flags().testFlag(flag: QRhiTexture::MipMapped);
826 QRhiSampler *sampler = rhiCtx->sampler(samplerDescription: { .minFilter: toRhi(op: subMeshInfo.baseColorNode->m_minFilterType),
827 .magFilter: toRhi(op: subMeshInfo.baseColorNode->m_magFilterType),
828 .mipmap: mipmapped ? toRhi(op: subMeshInfo.baseColorNode->m_mipFilterType) : QRhiSampler::None,
829 .hTiling: toRhi(tiling: subMeshInfo.baseColorNode->m_horizontalTilingMode),
830 .vTiling: toRhi(tiling: subMeshInfo.baseColorNode->m_verticalTilingMode),
831 .zTiling: QRhiSampler::Repeat
832 });
833 bindings.addTexture(binding: 1, stage: QRhiShaderResourceBinding::FragmentStage, tex: subMeshInfo.baseColorMap, sampler);
834 } else {
835 bindings.addTexture(binding: 1, stage: QRhiShaderResourceBinding::FragmentStage, tex: dummyTexture, sampler: dummySampler);
836 }
837 if (subMeshInfo.emissiveMap) {
838 const bool mipmapped = subMeshInfo.emissiveMap->flags().testFlag(flag: QRhiTexture::MipMapped);
839 QRhiSampler *sampler = rhiCtx->sampler(samplerDescription: { .minFilter: toRhi(op: subMeshInfo.emissiveNode->m_minFilterType),
840 .magFilter: toRhi(op: subMeshInfo.emissiveNode->m_magFilterType),
841 .mipmap: mipmapped ? toRhi(op: subMeshInfo.emissiveNode->m_mipFilterType) : QRhiSampler::None,
842 .hTiling: toRhi(tiling: subMeshInfo.emissiveNode->m_horizontalTilingMode),
843 .vTiling: toRhi(tiling: subMeshInfo.emissiveNode->m_verticalTilingMode),
844 .zTiling: QRhiSampler::Repeat
845 });
846 bindings.addTexture(binding: 2, stage: QRhiShaderResourceBinding::FragmentStage, tex: subMeshInfo.emissiveMap, sampler);
847 } else {
848 bindings.addTexture(binding: 2, stage: QRhiShaderResourceBinding::FragmentStage, tex: dummyTexture, sampler: dummySampler);
849 }
850 if (subMeshInfo.normalMap) {
851 if (!hasUV0 || !hasTangentAndBinormal) {
852 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("submesh %1 has a normal map, "
853 "but the mesh does not provide all three of UV0, tangent, and binormal; "
854 "expect incorrect results").arg(a: subMeshIdx));
855 }
856 const bool mipmapped = subMeshInfo.normalMap->flags().testFlag(flag: QRhiTexture::MipMapped);
857 QRhiSampler *sampler = rhiCtx->sampler(samplerDescription: { .minFilter: toRhi(op: subMeshInfo.normalMapNode->m_minFilterType),
858 .magFilter: toRhi(op: subMeshInfo.normalMapNode->m_magFilterType),
859 .mipmap: mipmapped ? toRhi(op: subMeshInfo.normalMapNode->m_mipFilterType) : QRhiSampler::None,
860 .hTiling: toRhi(tiling: subMeshInfo.normalMapNode->m_horizontalTilingMode),
861 .vTiling: toRhi(tiling: subMeshInfo.normalMapNode->m_verticalTilingMode),
862 .zTiling: QRhiSampler::Repeat
863 });
864 bindings.addTexture(binding: 3, stage: QRhiShaderResourceBinding::FragmentStage, tex: subMeshInfo.normalMap, sampler);
865 } else {
866 bindings.addTexture(binding: 3, stage: QRhiShaderResourceBinding::FragmentStage, tex: dummyTexture, sampler: dummySampler);
867 }
868 QRhiShaderResourceBindings *srb = rhiCtx->srb(bindings);
869
870 QRhiGraphicsPipeline *pipeline = setupPipeline(lmUvRastShaderPipeline.get(), srb, inputLayout);
871 if (!pipeline->create()) {
872 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create graphics pipeline (mesh %1 submesh %2)").
873 arg(a: lmIdx).
874 arg(a: subMeshIdx));
875 qDeleteAll(c: ps);
876 qDeleteAll(c: psLine);
877 return false;
878 }
879 ps.append(t: pipeline);
880 pipeline = setupPipeline(lmUvRastShaderPipeline.get(), srb, inputLayout);
881 pipeline->setPolygonMode(QRhiGraphicsPipeline::Line);
882 if (!pipeline->create()) {
883 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create graphics pipeline with line fill mode (mesh %1 submesh %2)").
884 arg(a: lmIdx).
885 arg(a: subMeshIdx));
886 qDeleteAll(c: ps);
887 qDeleteAll(c: psLine);
888 return false;
889 }
890 psLine.append(t: pipeline);
891 }
892
893 QRhiCommandBuffer::VertexInput vertexBuffers = { vbuf.get(), 0 };
894 const QRhiViewport viewport(0, 0, float(outputSize.width()), float(outputSize.height()));
895 bool hadViewport = false;
896
897 cb->beginPass(rt: rt.get(), colorClearValue: Qt::black, depthStencilClearValue: { 1.0f, 0 });
898 for (int subMeshIdx = 0; subMeshIdx != subMeshCount; ++subMeshIdx) {
899 const SubMeshInfo &subMeshInfo(subMeshInfos[lmIdx][subMeshIdx]);
900 cb->setGraphicsPipeline(ps[subMeshIdx]);
901 if (!hadViewport) {
902 cb->setViewport(viewport);
903 hadViewport = true;
904 }
905 cb->setShaderResources();
906 cb->setVertexInput(startBinding: 0, bindingCount: 1, bindings: &vertexBuffers, indexBuf: ibuf.get(), indexOffset: 0, indexFormat: QRhiCommandBuffer::IndexUInt32);
907 cb->drawIndexed(indexCount: subMeshInfo.count, instanceCount: 1, firstIndex: subMeshInfo.offset);
908 cb->setGraphicsPipeline(psLine[subMeshIdx]);
909 cb->setShaderResources();
910 cb->drawIndexed(indexCount: subMeshInfo.count, instanceCount: 1, firstIndex: subMeshInfo.offset);
911 }
912
913 resUpd = rhi->nextResourceUpdateBatch();
914 QRhiReadbackResult posReadResult;
915 QRhiReadbackResult normalReadResult;
916 QRhiReadbackResult baseColorReadResult;
917 QRhiReadbackResult emissionReadResult;
918 resUpd->readBackTexture(rb: { positionData.get() }, result: &posReadResult);
919 resUpd->readBackTexture(rb: { normalData.get() }, result: &normalReadResult);
920 resUpd->readBackTexture(rb: { baseColorData.get() }, result: &baseColorReadResult);
921 resUpd->readBackTexture(rb: { emissionData.get() }, result: &emissionReadResult);
922 cb->endPass(resourceUpdates: resUpd);
923
924 // Submit and wait for completion.
925 rhi->finish();
926
927 qDeleteAll(c: ps);
928 qDeleteAll(c: psLine);
929
930 Lightmap lightmap(outputSize);
931
932 // The readback results are tightly packed (which is supposed to be ensured
933 // by each rhi backend), so one line is 16 * width bytes.
934 if (posReadResult.data.size() < lightmap.entries.size() * 16) {
935 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Position data is smaller than expected"));
936 return false;
937 }
938 if (normalReadResult.data.size() < lightmap.entries.size() * 16) {
939 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Normal data is smaller than expected"));
940 return false;
941 }
942 if (baseColorReadResult.data.size() < lightmap.entries.size() * 16) {
943 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Base color data is smaller than expected"));
944 return false;
945 }
946 if (emissionReadResult.data.size() < lightmap.entries.size() * 16) {
947 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Emission data is smaller than expected"));
948 return false;
949 }
950 const float *lmPosPtr = reinterpret_cast<const float *>(posReadResult.data.constData());
951 const float *lmNormPtr = reinterpret_cast<const float *>(normalReadResult.data.constData());
952 const float *lmBaseColorPtr = reinterpret_cast<const float *>(baseColorReadResult.data.constData());
953 const float *lmEmissionPtr = reinterpret_cast<const float *>(emissionReadResult.data.constData());
954 int unusedEntries = 0;
955 for (qsizetype i = 0, ie = lightmap.entries.size(); i != ie; ++i) {
956 LightmapEntry &lmPix(lightmap.entries[i]);
957
958 float x = *lmPosPtr++;
959 float y = *lmPosPtr++;
960 float z = *lmPosPtr++;
961 lmPosPtr++;
962 lmPix.worldPos = QVector3D(x, y, z);
963
964 x = *lmNormPtr++;
965 y = *lmNormPtr++;
966 z = *lmNormPtr++;
967 lmNormPtr++;
968 lmPix.normal = QVector3D(x, y, z);
969
970 float r = *lmBaseColorPtr++;
971 float g = *lmBaseColorPtr++;
972 float b = *lmBaseColorPtr++;
973 float a = *lmBaseColorPtr++;
974 lmPix.baseColor = QVector4D(r, g, b, a);
975 if (a < 1.0f)
976 lightmap.hasBaseColorTransparency = true;
977
978 r = *lmEmissionPtr++;
979 g = *lmEmissionPtr++;
980 b = *lmEmissionPtr++;
981 lmEmissionPtr++;
982 lmPix.emission = QVector3D(r, g, b);
983
984 if (!lmPix.isValid())
985 ++unusedEntries;
986 }
987
988 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Successfully rasterized %1/%2 lightmap texels for model %3 with lightmap size %4 in %5 ms").
989 arg(a: lightmap.entries.size() - unusedEntries).
990 arg(a: lightmap.entries.size()).
991 arg(a: lm.model->debugObjectName).
992 arg(QStringLiteral("(%1, %2)").arg(a: outputSize.width()).arg(a: outputSize.height())).
993 arg(a: rasterizeTimer.elapsed()));
994 lightmaps.append(t: lightmap);
995
996 for (const SubMeshInfo &subMeshInfo : std::as_const(t&: subMeshInfos[lmIdx]))
997 geomLightmapMap[subMeshInfo.geomId] = lightmaps.size() - 1;
998 }
999
1000 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Lightmap preparing done"));
1001 return true;
1002}
1003
1004struct RayHit
1005{
1006 RayHit(const QVector3D &org, const QVector3D &dir, float tnear = 0.0f, float tfar = std::numeric_limits<float>::infinity()) {
1007 rayhit.ray.org_x = org.x();
1008 rayhit.ray.org_y = org.y();
1009 rayhit.ray.org_z = org.z();
1010 rayhit.ray.dir_x = dir.x();
1011 rayhit.ray.dir_y = dir.y();
1012 rayhit.ray.dir_z = dir.z();
1013 rayhit.ray.tnear = tnear;
1014 rayhit.ray.tfar = tfar;
1015 rayhit.hit.u = 0.0f;
1016 rayhit.hit.v = 0.0f;
1017 rayhit.hit.geomID = RTC_INVALID_GEOMETRY_ID;
1018 }
1019
1020 RTCRayHit rayhit;
1021
1022 bool intersect(RTCScene scene)
1023 {
1024 RTCIntersectContext ctx;
1025 rtcInitIntersectContext(context: &ctx);
1026 rtcIntersect1(scene, context: &ctx, rayhit: &rayhit);
1027 return rayhit.hit.geomID != RTC_INVALID_GEOMETRY_ID;
1028 }
1029};
1030
1031static inline QVector3D vectorSign(const QVector3D &v)
1032{
1033 return QVector3D(v.x() < 1.0f ? -1.0f : 1.0f,
1034 v.y() < 1.0f ? -1.0f : 1.0f,
1035 v.z() < 1.0f ? -1.0f : 1.0f);
1036}
1037
1038static inline QVector3D vectorAbs(const QVector3D &v)
1039{
1040 return QVector3D(std::abs(x: v.x()),
1041 std::abs(x: v.y()),
1042 std::abs(x: v.z()));
1043}
1044
1045void QSSGLightmapperPrivate::computeDirectLight()
1046{
1047 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Computing direct lighting..."));
1048 QElapsedTimer fullDirectLightTimer;
1049 fullDirectLightTimer.start();
1050
1051 const int bakedLightingModelCount = bakedLightingModels.size();
1052 Q_ASSERT(lightmaps.size() == bakedLightingModelCount);
1053
1054 QVector<QFuture<void>> futures;
1055
1056 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
1057 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
1058 Lightmap &lightmap(lightmaps[lmIdx]);
1059
1060 // direct lighting is relatively fast to calculate, so parallelize per model
1061 futures << QtConcurrent::run(f: [this, &lm, &lightmap] {
1062 QElapsedTimer directLightTimer;
1063 directLightTimer.start();
1064
1065 const int lightCount = lights.size();
1066 for (LightmapEntry &lmPix : lightmap.entries) {
1067 if (!lmPix.isValid())
1068 continue;
1069
1070 QVector3D worldPos = lmPix.worldPos;
1071 if (options.useAdaptiveBias)
1072 worldPos += vectorSign(v: lmPix.normal) * vectorAbs(v: worldPos * 0.0000002f);
1073
1074 // 'lights' should have all lights that are either BakeModeIndirect or BakeModeAll
1075 for (int i = 0; i < lightCount; ++i) {
1076 const Light &light(lights[i]);
1077
1078 QVector3D lightWorldPos;
1079 float dist = std::numeric_limits<float>::infinity();
1080 float attenuation = 1.0f;
1081 if (light.type == Light::Directional) {
1082 lightWorldPos = worldPos - light.direction;
1083 } else {
1084 lightWorldPos = light.worldPos;
1085 dist = (worldPos - lightWorldPos).length();
1086 attenuation = 1.0f / (light.constantAttenuation
1087 + light.linearAttenuation * dist
1088 + light.quadraticAttenuation * dist * dist);
1089 if (light.type == Light::Spot) {
1090 const float spotAngle = QVector3D::dotProduct(v1: (worldPos - lightWorldPos).normalized(),
1091 v2: light.direction.normalized());
1092 if (spotAngle > light.cosConeAngle) {
1093 // spotFactor = smoothstep(light.cosConeAngle, light.cosInnerConeAngle, spotAngle);
1094 const float edge0 = light.cosConeAngle;
1095 const float edge1 = light.cosInnerConeAngle;
1096 const float x = spotAngle;
1097 const float t = qBound(min: 0.0f, val: (x - edge0) / (edge1 - edge0), max: 1.0f);
1098 const float spotFactor = t * t * (3.0f - 2.0f * t);
1099 attenuation *= spotFactor;
1100 } else {
1101 attenuation = 0.0f;
1102 }
1103 }
1104 }
1105
1106 const QVector3D N = lmPix.normal;
1107 const QVector3D L = (lightWorldPos - worldPos).normalized();
1108 const float energy = qMax(a: 0.0f, b: QVector3D::dotProduct(v1: N, v2: L)) * attenuation;
1109 if (qFuzzyIsNull(f: energy))
1110 continue;
1111
1112 // trace a ray from this point towards the light, and see if something is hit on the way
1113 RayHit ray(worldPos, L, options.bias, dist);
1114 const bool lightReachable = !ray.intersect(scene: rscene);
1115 if (lightReachable) {
1116 // direct light must always be stored because indirect computation will need it
1117 lmPix.directLight += light.color * energy;
1118 // but we take it into account in the final result only for lights that have BakeModeAll
1119 if (!light.indirectOnly)
1120 lmPix.allLight += light.color * energy;
1121 }
1122 }
1123 }
1124
1125 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Direct light computed for model %1 in %2 ms").
1126 arg(a: lm.model->debugObjectName).
1127 arg(a: directLightTimer.elapsed()));
1128 });
1129 }
1130
1131 for (QFuture<void> &future : futures)
1132 future.waitForFinished();
1133
1134 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Direct light computation completed in %1 ms").
1135 arg(a: fullDirectLightTimer.elapsed()));
1136}
1137
1138// xorshift rng. this is called a lot -> rand/QRandomGenerator is out of question (way too slow)
1139static inline float uniformRand()
1140{
1141 static thread_local quint32 state = QRandomGenerator::global()->generate();
1142 state ^= state << 13;
1143 state ^= state >> 17;
1144 state ^= state << 5;
1145 return float(state) / float(UINT32_MAX);
1146}
1147
1148static inline QVector3D cosWeightedHemisphereSample()
1149{
1150 const float r1 = uniformRand();
1151 const float r2 = uniformRand() * 2.0f * float(M_PI);
1152 const float sqr1 = std::sqrt(x: r1);
1153 const float sqr1m = std::sqrt(x: 1.0f - r1);
1154 return QVector3D(sqr1 * std::cos(x: r2), sqr1 * std::sin(x: r2), sqr1m);
1155}
1156
1157void QSSGLightmapperPrivate::computeIndirectLight()
1158{
1159 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Computing indirect lighting..."));
1160 QElapsedTimer fullIndirectLightTimer;
1161 fullIndirectLightTimer.start();
1162
1163 const int bakedLightingModelCount = bakedLightingModels.size();
1164
1165 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
1166 // here we only care about the models that will store the lightmap image persistently
1167 if (!bakedLightingModels[lmIdx].model->hasLightmap())
1168 continue;
1169
1170 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
1171 Lightmap &lightmap(lightmaps[lmIdx]);
1172 int texelsDone = 0;
1173 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Total texels to compute for model %1: %2").
1174 arg(a: lm.model->debugObjectName).
1175 arg(a: lightmap.entries.size()));
1176 QElapsedTimer indirectLightTimer;
1177 indirectLightTimer.start();
1178
1179 // indirect lighting is slow, so parallelize per groups of samples,
1180 // e.g. if sample count is 256 and workgroup size is 32, then do up to
1181 // 8 sets in parallel, each calculating 32 samples (how many of the 8
1182 // are really done concurrently that's up to the thread pool to manage)
1183
1184 int wgSizePerGroup = qMax(a: 1, b: options.indirectLightWorkgroupSize);
1185 int wgCount = options.indirectLightSamples / wgSizePerGroup;
1186 if (options.indirectLightSamples % wgSizePerGroup)
1187 ++wgCount;
1188
1189 QVector<QFuture<QVector3D>> wg(wgCount);
1190
1191 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Computing indirect lighting for model %1 with key %2").
1192 arg(a: lm.model->debugObjectName).
1193 arg(a: lm.model->lightmapKey));
1194 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Sample count: %1, Workgroup size: %2, Max bounces: %3, Multiplier: %4").
1195 arg(a: options.indirectLightSamples).
1196 arg(a: wgSizePerGroup).
1197 arg(a: options.indirectLightBounces).
1198 arg(a: options.indirectLightFactor));
1199 for (LightmapEntry &lmPix : lightmap.entries) {
1200 if (!lmPix.isValid())
1201 continue;
1202
1203 for (int wgIdx = 0; wgIdx < wgCount; ++wgIdx) {
1204 const int beginIdx = wgIdx * wgSizePerGroup;
1205 const int endIdx = qMin(a: beginIdx + wgSizePerGroup, b: options.indirectLightSamples);
1206
1207 wg[wgIdx] = QtConcurrent::run(f: [this, beginIdx, endIdx, &lmPix] {
1208 QVector3D wgResult;
1209 for (int sampleIdx = beginIdx; sampleIdx < endIdx; ++sampleIdx) {
1210 QVector3D position = lmPix.worldPos;
1211 QVector3D normal = lmPix.normal;
1212 QVector3D throughput(1.0f, 1.0f, 1.0f);
1213 QVector3D sampleResult;
1214
1215 for (int bounce = 0; bounce < options.indirectLightBounces; ++bounce) {
1216 if (options.useAdaptiveBias)
1217 position += vectorSign(v: normal) * vectorAbs(v: position * 0.0000002f);
1218
1219 // get a sample using a cosine-weighted hemisphere sampler
1220 const QVector3D sample = cosWeightedHemisphereSample();
1221
1222 // transform to the point's local coordinate system
1223 const QVector3D v0 = qFuzzyCompare(p1: qAbs(t: normal.z()), p2: 1.0f)
1224 ? QVector3D(0.0f, 1.0f, 0.0f)
1225 : QVector3D(0.0f, 0.0f, 1.0f);
1226 const QVector3D tangent = QVector3D::crossProduct(v1: v0, v2: normal).normalized();
1227 const QVector3D bitangent = QVector3D::crossProduct(v1: tangent, v2: normal).normalized();
1228 QVector3D direction(
1229 tangent.x() * sample.x() + bitangent.x() * sample.y() + normal.x() * sample.z(),
1230 tangent.y() * sample.x() + bitangent.y() * sample.y() + normal.y() * sample.z(),
1231 tangent.z() * sample.x() + bitangent.z() * sample.y() + normal.z() * sample.z());
1232 direction.normalize();
1233
1234 // probability distribution function
1235 const float NdotL = qMax(a: 0.0f, b: QVector3D::dotProduct(v1: normal, v2: direction));
1236 const float pdf = NdotL / float(M_PI);
1237 if (qFuzzyIsNull(f: pdf))
1238 break;
1239
1240 // shoot ray, stop if no hit
1241 RayHit ray(position, direction, options.bias);
1242 if (!ray.intersect(scene: rscene))
1243 break;
1244
1245 // see what (sub)mesh and which texel it intersected with
1246 const LightmapEntry &hitEntry = texelForLightmapUV(geomId: ray.rayhit.hit.geomID,
1247 u: ray.rayhit.hit.u,
1248 v: ray.rayhit.hit.v);
1249
1250 // won't bounce further from a back face
1251 const bool hitBackFace = QVector3D::dotProduct(v1: hitEntry.normal, v2: direction) > 0.0f;
1252 if (hitBackFace)
1253 break;
1254
1255 // the BRDF of a diffuse surface is albedo / PI
1256 const QVector3D brdf = hitEntry.baseColor.toVector3D() / float(M_PI);
1257
1258 // calculate result for this bounce
1259 sampleResult += throughput * hitEntry.emission;
1260 throughput *= brdf * NdotL / pdf;
1261 sampleResult += throughput * hitEntry.directLight;
1262
1263 // stop if we guess there's no point in bouncing further
1264 // (low throughput path wouldn't contribute much)
1265 const float p = qMax(a: qMax(a: throughput.x(), b: throughput.y()), b: throughput.z());
1266 if (p < uniformRand())
1267 break;
1268
1269 // was not terminated: boost the energy by the probability to be terminated
1270 throughput /= p;
1271
1272 // next bounce starts from the hit's position
1273 position = hitEntry.worldPos;
1274 normal = hitEntry.normal;
1275 }
1276
1277 wgResult += sampleResult;
1278 }
1279 return wgResult;
1280 });
1281 }
1282
1283 QVector3D totalIndirect;
1284 for (const auto &future : wg)
1285 totalIndirect += future.result();
1286
1287 lmPix.allLight += totalIndirect * options.indirectLightFactor / options.indirectLightSamples;
1288
1289 ++texelsDone;
1290 if (texelsDone % 10000 == 0)
1291 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("%1 texels left").
1292 arg(a: lightmap.entries.size() - texelsDone));
1293
1294 if (bakingControl.cancelled)
1295 return;
1296 }
1297 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Indirect lighting computed for model %1 with key %2 in %3 ms").
1298 arg(a: lm.model->debugObjectName).
1299 arg(a: lm.model->lightmapKey).
1300 arg(a: indirectLightTimer.elapsed()));
1301 }
1302
1303 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Indirect light computation completed in %1 ms").
1304 arg(a: fullIndirectLightTimer.elapsed()));
1305}
1306
1307struct Edge {
1308 std::array<QVector3D, 2> pos;
1309 std::array<QVector3D, 2> normal;
1310};
1311
1312inline bool operator==(const Edge &a, const Edge &b)
1313{
1314 return qFuzzyCompare(v1: a.pos[0], v2: b.pos[0])
1315 && qFuzzyCompare(v1: a.pos[1], v2: b.pos[1])
1316 && qFuzzyCompare(v1: a.normal[0], v2: b.normal[0])
1317 && qFuzzyCompare(v1: a.normal[1], v2: b.normal[1]);
1318}
1319
1320inline size_t qHash(const Edge &e, size_t seed) Q_DECL_NOTHROW
1321{
1322 return qHash(key: e.pos[0].x(), seed) ^ qHash(key: e.pos[0].y()) ^ qHash(key: e.pos[0].z())
1323 ^ qHash(key: e.pos[1].x()) ^ qHash(key: e.pos[1].y()) ^ qHash(key: e.pos[1].z());
1324}
1325
1326struct EdgeUV {
1327 std::array<QVector2D, 2> uv;
1328 bool seam = false;
1329};
1330
1331struct SeamUV {
1332 std::array<std::array<QVector2D, 2>, 2> uv;
1333};
1334
1335static inline bool vectorLessThan(const QVector3D &a, const QVector3D &b)
1336{
1337 if (a.x() == b.x()) {
1338 if (a.y() == b.y())
1339 return a.z() < b.z();
1340 else
1341 return a.y() < b.y();
1342 }
1343 return a.x() < b.x();
1344}
1345
1346static inline float floatSign(float f)
1347{
1348 return f > 0.0f ? 1.0f : (f < 0.0f ? -1.0f : 0.0f);
1349}
1350
1351static inline QVector2D flooredVec(const QVector2D &v)
1352{
1353 return QVector2D(std::floor(x: v.x()), std::floor(x: v.y()));
1354}
1355
1356static inline QVector2D projectPointToLine(const QVector2D &point, const std::array<QVector2D, 2> &line)
1357{
1358 const QVector2D p = point - line[0];
1359 const QVector2D n = line[1] - line[0];
1360 const float lengthSquared = n.lengthSquared();
1361 if (!qFuzzyIsNull(f: lengthSquared)) {
1362 const float d = (n.x() * p.x() + n.y() * p.y()) / lengthSquared;
1363 return d <= 0.0f ? line[0] : (d >= 1.0f ? line[1] : line[0] + n * d);
1364 }
1365 return line[0];
1366}
1367
1368static void blendLine(const QVector2D &from, const QVector2D &to,
1369 const QVector2D &uvFrom, const QVector2D &uvTo,
1370 const QByteArray &readBuf, QByteArray &writeBuf,
1371 const QSize &lightmapPixelSize)
1372{
1373 const QVector2D size(lightmapPixelSize.width(), lightmapPixelSize.height());
1374 const std::array<QVector2D, 2> line = { QVector2D(from.x(), 1.0f - from.y()) * size,
1375 QVector2D(to.x(), 1.0f - to.y()) * size };
1376 const float lineLength = line[0].distanceToPoint(point: line[1]);
1377 if (qFuzzyIsNull(f: lineLength))
1378 return;
1379
1380 const QVector2D startPixel = flooredVec(v: line[0]);
1381 const QVector2D endPixel = flooredVec(v: line[1]);
1382
1383 const QVector2D dir = (line[1] - line[0]).normalized();
1384 const QVector2D tStep(1.0f / std::abs(x: dir.x()), 1.0f / std::abs(x: dir.y()));
1385 const QVector2D pixelStep(floatSign(f: dir.x()), floatSign(f: dir.y()));
1386
1387 QVector2D nextT(std::fmod(x: line[0].x(), y: 1.0f), std::fmod(x: line[0].y(), y: 1.0f));
1388 if (pixelStep.x() == 1.0f)
1389 nextT.setX(1.0f - nextT.x());
1390 if (pixelStep.y() == 1.0f)
1391 nextT.setY(1.0f - nextT.y());
1392 nextT /= QVector2D(std::abs(x: dir.x()), std::abs(x: dir.y()));
1393 if (std::isnan(x: nextT.x()))
1394 nextT.setX(std::numeric_limits<float>::max());
1395 if (std::isnan(x: nextT.y()))
1396 nextT.setY(std::numeric_limits<float>::max());
1397
1398 float *fpW = reinterpret_cast<float *>(writeBuf.data());
1399 const float *fpR = reinterpret_cast<const float *>(readBuf.constData());
1400
1401 QVector2D pixel = startPixel;
1402
1403 while (startPixel.distanceToPoint(point: pixel) < lineLength + 1.0f) {
1404 const QVector2D point = projectPointToLine(point: pixel + QVector2D(0.5f, 0.5f), line);
1405 const float t = line[0].distanceToPoint(point) / lineLength;
1406 const QVector2D uvInterp = uvFrom * (1.0 - t) + uvTo * t;
1407 const QVector2D sampledPixel = flooredVec(v: QVector2D(uvInterp.x(), 1.0f - uvInterp.y()) * size);
1408
1409 const int sampOfs = (int(sampledPixel.x()) + int(sampledPixel.y()) * lightmapPixelSize.width()) * 4;
1410 const QVector3D sampledColor(fpR[sampOfs], fpR[sampOfs + 1], fpR[sampOfs + 2]);
1411 const int pixOfs = (int(pixel.x()) + int(pixel.y()) * lightmapPixelSize.width()) * 4;
1412 QVector3D currentColor(fpW[pixOfs], fpW[pixOfs + 1], fpW[pixOfs + 2]);
1413 currentColor = currentColor * 0.6f + sampledColor * 0.4f;
1414 fpW[pixOfs] = currentColor.x();
1415 fpW[pixOfs + 1] = currentColor.y();
1416 fpW[pixOfs + 2] = currentColor.z();
1417
1418 if (pixel != endPixel) {
1419 if (nextT.x() < nextT.y()) {
1420 pixel.setX(pixel.x() + pixelStep.x());
1421 nextT.setX(nextT.x() + tStep.x());
1422 } else {
1423 pixel.setY(pixel.y() + pixelStep.y());
1424 nextT.setY(nextT.y() + tStep.y());
1425 }
1426 } else {
1427 break;
1428 }
1429 }
1430}
1431
1432bool QSSGLightmapperPrivate::postProcess()
1433{
1434 QRhi *rhi = rhiCtx->rhi();
1435 QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
1436 const int bakedLightingModelCount = bakedLightingModels.size();
1437
1438 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Post-processing..."));
1439 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
1440 QElapsedTimer postProcessTimer;
1441 postProcessTimer.start();
1442
1443 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
1444 // only care about the ones that will store the lightmap image persistently
1445 if (!lm.model->hasLightmap())
1446 continue;
1447
1448 Lightmap &lightmap(lightmaps[lmIdx]);
1449
1450 // Assemble the RGBA32F image from the baker data structures
1451 QByteArray lightmapFP32(lightmap.entries.size() * 4 * sizeof(float), Qt::Uninitialized);
1452 float *lightmapFloatPtr = reinterpret_cast<float *>(lightmapFP32.data());
1453 for (const LightmapEntry &lmPix : std::as_const(t&: lightmap.entries)) {
1454 *lightmapFloatPtr++ = lmPix.allLight.x();
1455 *lightmapFloatPtr++ = lmPix.allLight.y();
1456 *lightmapFloatPtr++ = lmPix.allLight.z();
1457 *lightmapFloatPtr++ = lmPix.isValid() ? 1.0f : 0.0f;
1458 }
1459
1460 // Dilate
1461 const QRhiViewport viewport(0, 0, float(lightmap.pixelSize.width()), float(lightmap.pixelSize.height()));
1462
1463 std::unique_ptr<QRhiTexture> lightmapTex(rhi->newTexture(format: QRhiTexture::RGBA32F, pixelSize: lightmap.pixelSize));
1464 if (!lightmapTex->create()) {
1465 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for postprocessing"));
1466 return false;
1467 }
1468 std::unique_ptr<QRhiTexture> dilatedLightmapTex(rhi->newTexture(format: QRhiTexture::RGBA32F, pixelSize: lightmap.pixelSize, sampleCount: 1,
1469 flags: QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
1470 if (!dilatedLightmapTex->create()) {
1471 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 dest. texture for postprocessing"));
1472 return false;
1473 }
1474 QRhiTextureRenderTargetDescription rtDescDilate(dilatedLightmapTex.get());
1475 std::unique_ptr<QRhiTextureRenderTarget> rtDilate(rhi->newTextureRenderTarget(desc: rtDescDilate));
1476 std::unique_ptr<QRhiRenderPassDescriptor> rpDescDilate(rtDilate->newCompatibleRenderPassDescriptor());
1477 rtDilate->setRenderPassDescriptor(rpDescDilate.get());
1478 if (!rtDilate->create()) {
1479 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create postprocessing texture render target"));
1480 return false;
1481 }
1482 QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
1483 QRhiTextureSubresourceUploadDescription lightmapTexUpload(lightmapFP32.constData(), lightmapFP32.size());
1484 resUpd->uploadTexture(tex: lightmapTex.get(), desc: QRhiTextureUploadDescription({ 0, 0, lightmapTexUpload }));
1485 QSSGRhiShaderResourceBindingList bindings;
1486 QRhiSampler *nearestSampler = rhiCtx->sampler(samplerDescription: { .minFilter: QRhiSampler::Nearest, .magFilter: QRhiSampler::Nearest, .mipmap: QRhiSampler::None,
1487 .hTiling: QRhiSampler::ClampToEdge, .vTiling: QRhiSampler::ClampToEdge, .zTiling: QRhiSampler::Repeat });
1488 bindings.addTexture(binding: 0, stage: QRhiShaderResourceBinding::FragmentStage, tex: lightmapTex.get(), sampler: nearestSampler);
1489 renderer->rhiQuadRenderer()->prepareQuad(rhiCtx, maybeRub: resUpd);
1490 const auto &lmDilatePipeline = renderer->getRhiLightmapDilateShader();
1491 if (!lmDilatePipeline) {
1492 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load shaders"));
1493 return false;
1494 }
1495 QSSGRhiGraphicsPipelineState dilatePs;
1496 dilatePs.viewport = viewport;
1497 dilatePs.shaderPipeline = lmDilatePipeline.get();
1498 renderer->rhiQuadRenderer()->recordRenderQuadPass(rhiCtx, ps: &dilatePs, srb: rhiCtx->srb(bindings), rt: rtDilate.get(), flags: QSSGRhiQuadRenderer::UvCoords);
1499 resUpd = rhi->nextResourceUpdateBatch();
1500 QRhiReadbackResult dilateReadResult;
1501 resUpd->readBackTexture(rb: { dilatedLightmapTex.get() }, result: &dilateReadResult);
1502 cb->resourceUpdate(resourceUpdates: resUpd);
1503
1504 // Submit and wait for completion.
1505 rhi->finish();
1506
1507 lightmap.imageFP32 = dilateReadResult.data;
1508
1509 // Reduce UV seams by collecting all edges (going through all
1510 // triangles), looking for (fuzzy)matching ones, then drawing lines
1511 // with blending on top.
1512 const DrawInfo &drawInfo(drawInfos[lmIdx]);
1513 const char *vbase = drawInfo.vertexData.constData();
1514 const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
1515
1516 // topology is Triangles, would be indexed draw - get rid of the index
1517 // buffer, need nothing but triangles afterwards
1518 qsizetype assembledVertexCount = 0;
1519 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx])
1520 assembledVertexCount += subMeshInfo.count;
1521 QVector<QVector3D> smPos(assembledVertexCount);
1522 QVector<QVector3D> smNormal(assembledVertexCount);
1523 QVector<QVector2D> smCoord(assembledVertexCount);
1524 qsizetype vertexIdx = 0;
1525 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
1526 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
1527 const quint32 idx = *(ibase + subMeshInfo.offset + i);
1528 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
1529 float x = *src++;
1530 float y = *src++;
1531 float z = *src++;
1532 smPos[vertexIdx] = QVector3D(x, y, z);
1533 src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
1534 x = *src++;
1535 y = *src++;
1536 z = *src++;
1537 smNormal[vertexIdx] = QVector3D(x, y, z);
1538 src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
1539 x = *src++;
1540 y = *src++;
1541 smCoord[vertexIdx] = QVector2D(x, y);
1542 ++vertexIdx;
1543 }
1544 }
1545
1546 QHash<Edge, EdgeUV> edgeUVMap;
1547 QVector<SeamUV> seams;
1548 for (vertexIdx = 0; vertexIdx < assembledVertexCount; vertexIdx += 3) {
1549 QVector3D triVert[3] = { smPos[vertexIdx], smPos[vertexIdx + 1], smPos[vertexIdx + 2] };
1550 QVector3D triNorm[3] = { smNormal[vertexIdx], smNormal[vertexIdx + 1], smNormal[vertexIdx + 2] };
1551 QVector2D triUV[3] = { smCoord[vertexIdx], smCoord[vertexIdx + 1], smCoord[vertexIdx + 2] };
1552
1553 for (int i = 0; i < 3; ++i) {
1554 int i0 = i;
1555 int i1 = (i + 1) % 3;
1556 if (vectorLessThan(a: triVert[i1], b: triVert[i0]))
1557 std::swap(a&: i0, b&: i1);
1558
1559 const Edge e = {
1560 .pos: { triVert[i0], triVert[i1] },
1561 .normal: { triNorm[i0], triNorm[i1] }
1562 };
1563 const EdgeUV edgeUV = { .uv: { triUV[i0], triUV[i1] } };
1564 auto it = edgeUVMap.find(key: e);
1565 if (it == edgeUVMap.end()) {
1566 edgeUVMap.insert(key: e, value: edgeUV);
1567 } else if (!qFuzzyCompare(v1: it->uv[0], v2: edgeUV.uv[0]) || !qFuzzyCompare(v1: it->uv[1], v2: edgeUV.uv[1])) {
1568 if (!it->seam) {
1569 seams.append(t: SeamUV({ .uv: { edgeUV.uv, it->uv } }));
1570 it->seam = true;
1571 }
1572 }
1573 }
1574 }
1575 qDebug() << "lm:" << seams.size() << "UV seams in" << lm.model;
1576
1577 QByteArray workBuf(lightmap.imageFP32.size(), Qt::Uninitialized);
1578 for (int blendIter = 0; blendIter < LM_SEAM_BLEND_ITER_COUNT; ++blendIter) {
1579 memcpy(dest: workBuf.data(), src: lightmap.imageFP32.constData(), n: lightmap.imageFP32.size());
1580 for (int seamIdx = 0, end = seams.size(); seamIdx != end; ++seamIdx) {
1581 const SeamUV &seam(seams[seamIdx]);
1582 blendLine(from: seam.uv[0][0], to: seam.uv[0][1],
1583 uvFrom: seam.uv[1][0], uvTo: seam.uv[1][1],
1584 readBuf: workBuf, writeBuf&: lightmap.imageFP32, lightmapPixelSize: lightmap.pixelSize);
1585 blendLine(from: seam.uv[1][0], to: seam.uv[1][1],
1586 uvFrom: seam.uv[0][0], uvTo: seam.uv[0][1],
1587 readBuf: workBuf, writeBuf&: lightmap.imageFP32, lightmapPixelSize: lightmap.pixelSize);
1588 }
1589 }
1590
1591 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Post-processing for model %1 with key %2 done in %3").
1592 arg(a: lm.model->debugObjectName).
1593 arg(a: lm.model->lightmapKey).
1594 arg(a: postProcessTimer.elapsed()));
1595 }
1596
1597 return true;
1598}
1599
1600bool QSSGLightmapperPrivate::storeLightmaps()
1601{
1602 const int bakedLightingModelCount = bakedLightingModels.size();
1603 QByteArray listContents;
1604
1605 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
1606 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
1607 // only care about the ones that want to store the lightmap image persistently
1608 if (!lm.model->hasLightmap())
1609 continue;
1610
1611 QElapsedTimer writeTimer;
1612 writeTimer.start();
1613
1614 // An empty outputFolder equates to working directory
1615 QString outputFolder;
1616 if (!lm.model->lightmapLoadPath.startsWith(QStringLiteral(":/")))
1617 outputFolder = lm.model->lightmapLoadPath;
1618
1619 const QString fn = QSSGLightmapper::lightmapAssetPathForSave(model: *lm.model, asset: QSSGLightmapper::LightmapAsset::LightmapImage, outputFolder);
1620 const QByteArray fns = fn.toUtf8();
1621
1622 listContents += QFileInfo(fn).absoluteFilePath().toUtf8();
1623 listContents += '\n';
1624
1625 const Lightmap &lightmap(lightmaps[lmIdx]);
1626
1627 if (SaveEXR(data: reinterpret_cast<const float *>(lightmap.imageFP32.constData()),
1628 width: lightmap.pixelSize.width(), height: lightmap.pixelSize.height(),
1629 components: 4, save_as_fp16: false, filename: fns.constData(), err: nullptr) < 0)
1630 {
1631 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to write out lightmap"));
1632 return false;
1633 }
1634
1635 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Lightmap saved for model %1 to %2 in %3 ms").
1636 arg(a: lm.model->debugObjectName).
1637 arg(a: fn).
1638 arg(a: writeTimer.elapsed()));
1639 const DrawInfo &bakeModelDrawInfo(drawInfos[lmIdx]);
1640 if (bakeModelDrawInfo.meshWithLightmapUV.isValid()) {
1641 writeTimer.start();
1642 QFile f(QSSGLightmapper::lightmapAssetPathForSave(model: *lm.model, asset: QSSGLightmapper::LightmapAsset::MeshWithLightmapUV, outputFolder));
1643 if (f.open(flags: QIODevice::WriteOnly | QIODevice::Truncate)) {
1644 bakeModelDrawInfo.meshWithLightmapUV.save(device: &f);
1645 } else {
1646 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to write mesh with lightmap UV data to '%1'").
1647 arg(a: f.fileName()));
1648 return false;
1649 }
1650 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Lightmap-compatible mesh saved for model %1 to %2 in %3 ms").
1651 arg(a: lm.model->debugObjectName).
1652 arg(a: f.fileName()).
1653 arg(a: writeTimer.elapsed()));
1654 } // else the mesh had a lightmap uv channel to begin with, no need to save another version of it
1655 }
1656
1657 QFile listFile(QSSGLightmapper::lightmapAssetPathForSave(asset: QSSGLightmapper::LightmapAsset::LightmapImageList));
1658 if (!listFile.open(flags: QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
1659 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create lightmap list file %1").
1660 arg(a: listFile.fileName()));
1661 return false;
1662 }
1663 listFile.write(data: listContents);
1664
1665 return true;
1666}
1667
1668void QSSGLightmapperPrivate::sendOutputInfo(QSSGLightmapper::BakingStatus type, std::optional<QString> msg)
1669{
1670 QString result;
1671
1672 switch (type)
1673 {
1674 case QSSGLightmapper::BakingStatus::None:
1675 return;
1676 case QSSGLightmapper::BakingStatus::Progress:
1677 result = QStringLiteral("[lm] Progress");
1678 break;
1679 case QSSGLightmapper::BakingStatus::Error:
1680 result = QStringLiteral("[lm] Error");
1681 break;
1682 case QSSGLightmapper::BakingStatus::Warning:
1683 result = QStringLiteral("[lm] Warning");
1684 break;
1685 case QSSGLightmapper::BakingStatus::Cancelled:
1686 result = QStringLiteral("[lm] Cancelled");
1687 break;
1688 case QSSGLightmapper::BakingStatus::Complete:
1689 result = QStringLiteral("[lm] Complete");
1690 break;
1691 }
1692
1693 if (msg.has_value())
1694 result.append(QStringLiteral(": ") + msg.value());
1695
1696 if (type == QSSGLightmapper::BakingStatus::Warning)
1697 qWarning() << result;
1698 else
1699 qDebug() << result;
1700
1701 if (outputCallback)
1702 outputCallback(type, msg, &bakingControl);
1703}
1704
1705bool QSSGLightmapper::bake()
1706{
1707 QElapsedTimer totalTimer;
1708 totalTimer.start();
1709
1710 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Bake starting..."));
1711 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Total models registered: %1").arg(a: d->bakedLightingModels.size()));
1712
1713 if (d->bakedLightingModels.isEmpty()) {
1714 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by LightMapper, No Models to bake"));
1715 return false;
1716 }
1717
1718 if (!d->commitGeometry()) {
1719 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking failed"));
1720 return false;
1721 }
1722
1723 if (!d->prepareLightmaps()) {
1724 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking failed"));
1725 return false;
1726 }
1727
1728 if (d->bakingControl.cancelled) {
1729 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
1730 return false;
1731 }
1732
1733 d->computeDirectLight();
1734
1735 if (d->bakingControl.cancelled) {
1736 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
1737 return false;
1738 }
1739
1740 if (d->options.indirectLightEnabled)
1741 d->computeIndirectLight();
1742
1743 if (d->bakingControl.cancelled) {
1744 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
1745 return false;
1746 }
1747
1748 if (!d->postProcess()) {
1749 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking failed"));
1750 return false;
1751 }
1752
1753 if (d->bakingControl.cancelled) {
1754 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
1755 return false;
1756 }
1757
1758 if (!d->storeLightmaps()) {
1759 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking failed"));
1760 return false;
1761 }
1762
1763 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking took %1 ms").arg(a: totalTimer.elapsed()));
1764 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Complete, msg: std::nullopt);
1765 return true;
1766}
1767
1768#else
1769
1770QSSGLightmapper::QSSGLightmapper(QSSGRhiContext *, QSSGRenderer *)
1771{
1772}
1773
1774QSSGLightmapper::~QSSGLightmapper()
1775{
1776}
1777
1778void QSSGLightmapper::reset()
1779{
1780}
1781
1782void QSSGLightmapper::setOptions(const QSSGLightmapperOptions &)
1783{
1784}
1785
1786void QSSGLightmapper::setOutputCallback(Callback )
1787{
1788}
1789
1790qsizetype QSSGLightmapper::add(const QSSGBakedLightingModel &)
1791{
1792 return 0;
1793}
1794
1795bool QSSGLightmapper::bake()
1796{
1797 qWarning("Qt Quick 3D was built without the lightmapper; cannot bake lightmaps");
1798 return false;
1799}
1800
1801#endif // QT_QUICK3D_HAS_LIGHTMAPPER
1802
1803QString QSSGLightmapper::lightmapAssetPathForLoad(const QSSGRenderModel &model, LightmapAsset asset)
1804{
1805 QString result;
1806 if (!model.lightmapLoadPath.isEmpty()) {
1807 result += model.lightmapLoadPath;
1808 if (!result.endsWith(c: QLatin1Char('/')))
1809 result += QLatin1Char('/');
1810 }
1811 switch (asset) {
1812 case LightmapAsset::LightmapImage:
1813 result += QStringLiteral("qlm_%1.exr").arg(a: model.lightmapKey);
1814 break;
1815 case LightmapAsset::MeshWithLightmapUV:
1816 result += QStringLiteral("qlm_%1.mesh").arg(a: model.lightmapKey);
1817 break;
1818 default:
1819 return QString();
1820 }
1821 return result;
1822}
1823
1824QString QSSGLightmapper::lightmapAssetPathForSave(const QSSGRenderModel &model, LightmapAsset asset, const QString& outputFolder)
1825{
1826 QString result = outputFolder;
1827 if (!result.isEmpty() && !result.endsWith(c: QLatin1Char('/')))
1828 result += QLatin1Char('/');
1829
1830 switch (asset) {
1831 case LightmapAsset::LightmapImage:
1832 result += QStringLiteral("qlm_%1.exr").arg(a: model.lightmapKey);
1833 break;
1834 case LightmapAsset::MeshWithLightmapUV:
1835 result += QStringLiteral("qlm_%1.mesh").arg(a: model.lightmapKey);
1836 break;
1837 default:
1838 result += lightmapAssetPathForSave(asset, outputFolder);
1839 break;
1840 }
1841 return result;
1842}
1843
1844QString QSSGLightmapper::lightmapAssetPathForSave(LightmapAsset asset, const QString& outputFolder)
1845{
1846 QString result = outputFolder;
1847 if (!result.isEmpty() && !result.endsWith(c: QLatin1Char('/')))
1848 result += QLatin1Char('/');
1849
1850 switch (asset) {
1851 case LightmapAsset::LightmapImageList:
1852 result += QStringLiteral("qlm_list.txt");
1853 default:
1854 break;
1855 }
1856 return result;
1857}
1858
1859QT_END_NAMESPACE
1860

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