1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qssglightmapper_p.h"
5#include <QtQuick3DRuntimeRender/private/qssgrenderer_p.h>
6#include <QtQuick3DRuntimeRender/private/qssgrhiquadrenderer_p.h>
7#include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
8#include "../qssgrendercontextcore.h"
9#include <QtQuick3DUtils/private/qssgutils_p.h>
10
11#ifdef QT_QUICK3D_HAS_LIGHTMAPPER
12#include <QtCore/qfuture.h>
13#include <QtCore/qfileinfo.h>
14#include <QtConcurrent/qtconcurrentrun.h>
15#include <QRandomGenerator>
16#include <qsimd.h>
17#include <embree3/rtcore.h>
18#include <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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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->lightmapKey).
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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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 = QSSGRhiHelpers::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
396 } else if (vbe.name == QSSGMesh::MeshInternal::getNormalAttrName()) {
397 drawInfo.normalOffset = vbe.offset;
398 drawInfo.normalFormat = QSSGRhiHelpers::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
399 } else if (vbe.name == QSSGMesh::MeshInternal::getUV0AttrName()) {
400 drawInfo.uvOffset = vbe.offset;
401 drawInfo.uvFormat = QSSGRhiHelpers::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
402 } else if (vbe.name == QSSGMesh::MeshInternal::getLightmapUVAttrName()) {
403 drawInfo.lightmapUVOffset = vbe.offset;
404 drawInfo.lightmapUVFormat = QSSGRhiHelpers::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
405 } else if (vbe.name == QSSGMesh::MeshInternal::getTexTanAttrName()) {
406 drawInfo.tangentOffset = vbe.offset;
407 drawInfo.tangentFormat = QSSGRhiHelpers::toVertexInputFormat(compType: QSSGRenderComponentType(vbe.componentType), numComps: vbe.componentCount);
408 } else if (vbe.name == QSSGMesh::MeshInternal::getTexBinormalAttrName()) {
409 drawInfo.binormalOffset = vbe.offset;
410 drawInfo.binormalFormat = QSSGRhiHelpers::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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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->lightmapKey));
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 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(q: rhiCtx);
782 QVector<QRhiGraphicsPipeline *> ps;
783 // Everything is going to be rendered twice (but note depth testing), first
784 // with polygon mode fill, then line.
785 QVector<QRhiGraphicsPipeline *> psLine;
786
787 for (int subMeshIdx = 0; subMeshIdx != subMeshCount; ++subMeshIdx) {
788 const SubMeshInfo &subMeshInfo(subMeshInfos[lmIdx][subMeshIdx]);
789 QVarLengthArray<QRhiVertexInputAttribute, 6> vertexAttrs;
790 vertexAttrs << QRhiVertexInputAttribute(0, 0, bakeModelDrawInfo.positionFormat, bakeModelDrawInfo.positionOffset)
791 << QRhiVertexInputAttribute(0, 1, bakeModelDrawInfo.normalFormat, bakeModelDrawInfo.normalOffset)
792 << QRhiVertexInputAttribute(0, 2, bakeModelDrawInfo.lightmapUVFormat, bakeModelDrawInfo.lightmapUVOffset);
793
794 // Vertex inputs (just like the sampler uniforms) must match exactly on
795 // the shader and the application side, cannot just leave out or have
796 // unused inputs.
797 QSSGBuiltInRhiShaderCache::LightmapUVRasterizationShaderMode shaderVariant = QSSGBuiltInRhiShaderCache::LightmapUVRasterizationShaderMode::Default;
798 if (hasUV0) {
799 shaderVariant = QSSGBuiltInRhiShaderCache::LightmapUVRasterizationShaderMode::Uv;
800 if (hasTangentAndBinormal)
801 shaderVariant = QSSGBuiltInRhiShaderCache::LightmapUVRasterizationShaderMode::UvTangent;
802 }
803
804 const auto &shaderCache = renderer->contextInterface()->shaderCache();
805 const auto &lmUvRastShaderPipeline = shaderCache->getBuiltInRhiShaders().getRhiLightmapUVRasterizationShader(mode: shaderVariant);
806 if (!lmUvRastShaderPipeline) {
807 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load shaders"));
808 return false;
809 }
810
811 if (hasUV0) {
812 vertexAttrs << QRhiVertexInputAttribute(0, 3, bakeModelDrawInfo.uvFormat, bakeModelDrawInfo.uvOffset);
813 if (hasTangentAndBinormal) {
814 vertexAttrs << QRhiVertexInputAttribute(0, 4, bakeModelDrawInfo.tangentFormat, bakeModelDrawInfo.tangentOffset);
815 vertexAttrs << QRhiVertexInputAttribute(0, 5, bakeModelDrawInfo.binormalFormat, bakeModelDrawInfo.binormalOffset);
816 }
817 }
818
819 inputLayout.setAttributes(first: vertexAttrs.cbegin(), last: vertexAttrs.cend());
820
821 QSSGRhiShaderResourceBindingList bindings;
822 bindings.addUniformBuffer(binding: 0, stage: QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, buf: ubuf.get(),
823 offset: subMeshIdx * alignedUbufSize, size: UBUF_SIZE);
824 QRhiSampler *dummySampler = rhiCtx->sampler(samplerDescription: { .minFilter: QRhiSampler::Nearest, .magFilter: QRhiSampler::Nearest, .mipmap: QRhiSampler::None,
825 .hTiling: QRhiSampler::ClampToEdge, .vTiling: QRhiSampler::ClampToEdge, .zTiling: QRhiSampler::Repeat });
826 if (subMeshInfo.baseColorMap) {
827 const bool mipmapped = subMeshInfo.baseColorMap->flags().testFlag(flag: QRhiTexture::MipMapped);
828 QRhiSampler *sampler = rhiCtx->sampler(samplerDescription: { .minFilter: QSSGRhiHelpers::toRhi(op: subMeshInfo.baseColorNode->m_minFilterType),
829 .magFilter: QSSGRhiHelpers::toRhi(op: subMeshInfo.baseColorNode->m_magFilterType),
830 .mipmap: mipmapped ? QSSGRhiHelpers::toRhi(op: subMeshInfo.baseColorNode->m_mipFilterType) : QRhiSampler::None,
831 .hTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.baseColorNode->m_horizontalTilingMode),
832 .vTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.baseColorNode->m_verticalTilingMode),
833 .zTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.baseColorNode->m_depthTilingMode)
834 });
835 bindings.addTexture(binding: 1, stage: QRhiShaderResourceBinding::FragmentStage, tex: subMeshInfo.baseColorMap, sampler);
836 } else {
837 bindings.addTexture(binding: 1, stage: QRhiShaderResourceBinding::FragmentStage, tex: dummyTexture, sampler: dummySampler);
838 }
839 if (subMeshInfo.emissiveMap) {
840 const bool mipmapped = subMeshInfo.emissiveMap->flags().testFlag(flag: QRhiTexture::MipMapped);
841 QRhiSampler *sampler = rhiCtx->sampler(samplerDescription: { .minFilter: QSSGRhiHelpers::toRhi(op: subMeshInfo.emissiveNode->m_minFilterType),
842 .magFilter: QSSGRhiHelpers::toRhi(op: subMeshInfo.emissiveNode->m_magFilterType),
843 .mipmap: mipmapped ? QSSGRhiHelpers::toRhi(op: subMeshInfo.emissiveNode->m_mipFilterType) : QRhiSampler::None,
844 .hTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.emissiveNode->m_horizontalTilingMode),
845 .vTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.emissiveNode->m_verticalTilingMode),
846 .zTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.emissiveNode->m_depthTilingMode)
847 });
848 bindings.addTexture(binding: 2, stage: QRhiShaderResourceBinding::FragmentStage, tex: subMeshInfo.emissiveMap, sampler);
849 } else {
850 bindings.addTexture(binding: 2, stage: QRhiShaderResourceBinding::FragmentStage, tex: dummyTexture, sampler: dummySampler);
851 }
852 if (subMeshInfo.normalMap) {
853 const bool mipmapped = subMeshInfo.normalMap->flags().testFlag(flag: QRhiTexture::MipMapped);
854 QRhiSampler *sampler = rhiCtx->sampler(samplerDescription: { .minFilter: QSSGRhiHelpers::toRhi(op: subMeshInfo.normalMapNode->m_minFilterType),
855 .magFilter: QSSGRhiHelpers::toRhi(op: subMeshInfo.normalMapNode->m_magFilterType),
856 .mipmap: mipmapped ? QSSGRhiHelpers::toRhi(op: subMeshInfo.normalMapNode->m_mipFilterType) : QRhiSampler::None,
857 .hTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.normalMapNode->m_horizontalTilingMode),
858 .vTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.normalMapNode->m_verticalTilingMode),
859 .zTiling: QSSGRhiHelpers::toRhi(tiling: subMeshInfo.normalMapNode->m_depthTilingMode)
860 });
861 bindings.addTexture(binding: 3, stage: QRhiShaderResourceBinding::FragmentStage, tex: subMeshInfo.normalMap, sampler);
862 } else {
863 bindings.addTexture(binding: 3, stage: QRhiShaderResourceBinding::FragmentStage, tex: dummyTexture, sampler: dummySampler);
864 }
865 QRhiShaderResourceBindings *srb = rhiCtxD->srb(bindings);
866
867 QRhiGraphicsPipeline *pipeline = setupPipeline(lmUvRastShaderPipeline.get(), srb, inputLayout);
868 if (!pipeline->create()) {
869 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create graphics pipeline (mesh %1 submesh %2)").
870 arg(a: lmIdx).
871 arg(a: subMeshIdx));
872 qDeleteAll(c: ps);
873 qDeleteAll(c: psLine);
874 return false;
875 }
876 ps.append(t: pipeline);
877 pipeline = setupPipeline(lmUvRastShaderPipeline.get(), srb, inputLayout);
878 pipeline->setPolygonMode(QRhiGraphicsPipeline::Line);
879 if (!pipeline->create()) {
880 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create graphics pipeline with line fill mode (mesh %1 submesh %2)").
881 arg(a: lmIdx).
882 arg(a: subMeshIdx));
883 qDeleteAll(c: ps);
884 qDeleteAll(c: psLine);
885 return false;
886 }
887 psLine.append(t: pipeline);
888 }
889
890 QRhiCommandBuffer::VertexInput vertexBuffers = { vbuf.get(), 0 };
891 const QRhiViewport viewport(0, 0, float(outputSize.width()), float(outputSize.height()));
892 bool hadViewport = false;
893
894 cb->beginPass(rt: rt.get(), colorClearValue: Qt::black, depthStencilClearValue: { 1.0f, 0 });
895 for (int subMeshIdx = 0; subMeshIdx != subMeshCount; ++subMeshIdx) {
896 const SubMeshInfo &subMeshInfo(subMeshInfos[lmIdx][subMeshIdx]);
897 cb->setGraphicsPipeline(ps[subMeshIdx]);
898 if (!hadViewport) {
899 cb->setViewport(viewport);
900 hadViewport = true;
901 }
902 cb->setShaderResources();
903 cb->setVertexInput(startBinding: 0, bindingCount: 1, bindings: &vertexBuffers, indexBuf: ibuf.get(), indexOffset: 0, indexFormat: QRhiCommandBuffer::IndexUInt32);
904 cb->drawIndexed(indexCount: subMeshInfo.count, instanceCount: 1, firstIndex: subMeshInfo.offset);
905 cb->setGraphicsPipeline(psLine[subMeshIdx]);
906 cb->setShaderResources();
907 cb->drawIndexed(indexCount: subMeshInfo.count, instanceCount: 1, firstIndex: subMeshInfo.offset);
908 }
909
910 resUpd = rhi->nextResourceUpdateBatch();
911 QRhiReadbackResult posReadResult;
912 QRhiReadbackResult normalReadResult;
913 QRhiReadbackResult baseColorReadResult;
914 QRhiReadbackResult emissionReadResult;
915 resUpd->readBackTexture(rb: { positionData.get() }, result: &posReadResult);
916 resUpd->readBackTexture(rb: { normalData.get() }, result: &normalReadResult);
917 resUpd->readBackTexture(rb: { baseColorData.get() }, result: &baseColorReadResult);
918 resUpd->readBackTexture(rb: { emissionData.get() }, result: &emissionReadResult);
919 cb->endPass(resourceUpdates: resUpd);
920
921 // Submit and wait for completion.
922 rhi->finish();
923
924 qDeleteAll(c: ps);
925 qDeleteAll(c: psLine);
926
927 Lightmap lightmap(outputSize);
928
929 // The readback results are tightly packed (which is supposed to be ensured
930 // by each rhi backend), so one line is 16 * width bytes.
931 if (posReadResult.data.size() < lightmap.entries.size() * 16) {
932 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Position data is smaller than expected"));
933 return false;
934 }
935 if (normalReadResult.data.size() < lightmap.entries.size() * 16) {
936 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Normal data is smaller than expected"));
937 return false;
938 }
939 if (baseColorReadResult.data.size() < lightmap.entries.size() * 16) {
940 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Base color data is smaller than expected"));
941 return false;
942 }
943 if (emissionReadResult.data.size() < lightmap.entries.size() * 16) {
944 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Emission data is smaller than expected"));
945 return false;
946 }
947 const float *lmPosPtr = reinterpret_cast<const float *>(posReadResult.data.constData());
948 const float *lmNormPtr = reinterpret_cast<const float *>(normalReadResult.data.constData());
949 const float *lmBaseColorPtr = reinterpret_cast<const float *>(baseColorReadResult.data.constData());
950 const float *lmEmissionPtr = reinterpret_cast<const float *>(emissionReadResult.data.constData());
951 int unusedEntries = 0;
952 for (qsizetype i = 0, ie = lightmap.entries.size(); i != ie; ++i) {
953 LightmapEntry &lmPix(lightmap.entries[i]);
954
955 float x = *lmPosPtr++;
956 float y = *lmPosPtr++;
957 float z = *lmPosPtr++;
958 lmPosPtr++;
959 lmPix.worldPos = QVector3D(x, y, z);
960
961 x = *lmNormPtr++;
962 y = *lmNormPtr++;
963 z = *lmNormPtr++;
964 lmNormPtr++;
965 lmPix.normal = QVector3D(x, y, z);
966
967 float r = *lmBaseColorPtr++;
968 float g = *lmBaseColorPtr++;
969 float b = *lmBaseColorPtr++;
970 float a = *lmBaseColorPtr++;
971 lmPix.baseColor = QVector4D(r, g, b, a);
972 if (a < 1.0f)
973 lightmap.hasBaseColorTransparency = true;
974
975 r = *lmEmissionPtr++;
976 g = *lmEmissionPtr++;
977 b = *lmEmissionPtr++;
978 lmEmissionPtr++;
979 lmPix.emission = QVector3D(r, g, b);
980
981 if (!lmPix.isValid())
982 ++unusedEntries;
983 }
984
985 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Successfully rasterized %1/%2 lightmap texels for model %3, lightmap size %4 in %5 ms").
986 arg(a: lightmap.entries.size() - unusedEntries).
987 arg(a: lightmap.entries.size()).
988 arg(a: lm.model->lightmapKey).
989 arg(QStringLiteral("(%1, %2)").arg(a: outputSize.width()).arg(a: outputSize.height())).
990 arg(a: rasterizeTimer.elapsed()));
991 lightmaps.append(t: lightmap);
992
993 for (const SubMeshInfo &subMeshInfo : std::as_const(t&: subMeshInfos[lmIdx])) {
994 if (!lm.model->castsShadows) // only matters if it's in the raytracer scene
995 continue;
996 geomLightmapMap[subMeshInfo.geomId] = lightmaps.size() - 1;
997 }
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->lightmapKey).
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->lightmapKey).
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").
1192 arg(a: lm.model->lightmapKey));
1193 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Sample count: %1, Workgroup size: %2, Max bounces: %3, Multiplier: %4").
1194 arg(a: options.indirectLightSamples).
1195 arg(a: wgSizePerGroup).
1196 arg(a: options.indirectLightBounces).
1197 arg(a: options.indirectLightFactor));
1198 for (LightmapEntry &lmPix : lightmap.entries) {
1199 if (!lmPix.isValid())
1200 continue;
1201
1202 for (int wgIdx = 0; wgIdx < wgCount; ++wgIdx) {
1203 const int beginIdx = wgIdx * wgSizePerGroup;
1204 const int endIdx = qMin(a: beginIdx + wgSizePerGroup, b: options.indirectLightSamples);
1205
1206 wg[wgIdx] = QtConcurrent::run(f: [this, beginIdx, endIdx, &lmPix] {
1207 QVector3D wgResult;
1208 for (int sampleIdx = beginIdx; sampleIdx < endIdx; ++sampleIdx) {
1209 QVector3D position = lmPix.worldPos;
1210 QVector3D normal = lmPix.normal;
1211 QVector3D throughput(1.0f, 1.0f, 1.0f);
1212 QVector3D sampleResult;
1213
1214 for (int bounce = 0; bounce < options.indirectLightBounces; ++bounce) {
1215 if (options.useAdaptiveBias)
1216 position += vectorSign(v: normal) * vectorAbs(v: position * 0.0000002f);
1217
1218 // get a sample using a cosine-weighted hemisphere sampler
1219 const QVector3D sample = cosWeightedHemisphereSample();
1220
1221 // transform to the point's local coordinate system
1222 const QVector3D v0 = qFuzzyCompare(p1: qAbs(t: normal.z()), p2: 1.0f)
1223 ? QVector3D(0.0f, 1.0f, 0.0f)
1224 : QVector3D(0.0f, 0.0f, 1.0f);
1225 const QVector3D tangent = QVector3D::crossProduct(v1: v0, v2: normal).normalized();
1226 const QVector3D bitangent = QVector3D::crossProduct(v1: tangent, v2: normal).normalized();
1227 QVector3D direction(
1228 tangent.x() * sample.x() + bitangent.x() * sample.y() + normal.x() * sample.z(),
1229 tangent.y() * sample.x() + bitangent.y() * sample.y() + normal.y() * sample.z(),
1230 tangent.z() * sample.x() + bitangent.z() * sample.y() + normal.z() * sample.z());
1231 direction.normalize();
1232
1233 // probability distribution function
1234 const float NdotL = qMax(a: 0.0f, b: QVector3D::dotProduct(v1: normal, v2: direction));
1235 const float pdf = NdotL / float(M_PI);
1236 if (qFuzzyIsNull(f: pdf))
1237 break;
1238
1239 // shoot ray, stop if no hit
1240 RayHit ray(position, direction, options.bias);
1241 if (!ray.intersect(scene: rscene))
1242 break;
1243
1244 // see what (sub)mesh and which texel it intersected with
1245 const LightmapEntry &hitEntry = texelForLightmapUV(geomId: ray.rayhit.hit.geomID,
1246 u: ray.rayhit.hit.u,
1247 v: ray.rayhit.hit.v);
1248
1249 // won't bounce further from a back face
1250 const bool hitBackFace = QVector3D::dotProduct(v1: hitEntry.normal, v2: direction) > 0.0f;
1251 if (hitBackFace)
1252 break;
1253
1254 // the BRDF of a diffuse surface is albedo / PI
1255 const QVector3D brdf = hitEntry.baseColor.toVector3D() / float(M_PI);
1256
1257 // calculate result for this bounce
1258 sampleResult += throughput * hitEntry.emission;
1259 throughput *= brdf * NdotL / pdf;
1260 sampleResult += throughput * hitEntry.directLight;
1261
1262 // stop if we guess there's no point in bouncing further
1263 // (low throughput path wouldn't contribute much)
1264 const float p = qMax(a: qMax(a: throughput.x(), b: throughput.y()), b: throughput.z());
1265 if (p < uniformRand())
1266 break;
1267
1268 // was not terminated: boost the energy by the probability to be terminated
1269 throughput /= p;
1270
1271 // next bounce starts from the hit's position
1272 position = hitEntry.worldPos;
1273 normal = hitEntry.normal;
1274 }
1275
1276 wgResult += sampleResult;
1277 }
1278 return wgResult;
1279 });
1280 }
1281
1282 QVector3D totalIndirect;
1283 for (const auto &future : wg)
1284 totalIndirect += future.result();
1285
1286 lmPix.allLight += totalIndirect * options.indirectLightFactor / options.indirectLightSamples;
1287
1288 ++texelsDone;
1289 if (texelsDone % 10000 == 0)
1290 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("%1 texels left").
1291 arg(a: lightmap.entries.size() - texelsDone));
1292
1293 if (bakingControl.cancelled)
1294 return;
1295 }
1296 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Indirect lighting computed for model %1 in %2 ms").
1297 arg(a: lm.model->lightmapKey).
1298 arg(a: indirectLightTimer.elapsed()));
1299 }
1300
1301 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Indirect light computation completed in %1 ms").
1302 arg(a: fullIndirectLightTimer.elapsed()));
1303}
1304
1305struct Edge {
1306 std::array<QVector3D, 2> pos;
1307 std::array<QVector3D, 2> normal;
1308};
1309
1310inline bool operator==(const Edge &a, const Edge &b)
1311{
1312 return qFuzzyCompare(v1: a.pos[0], v2: b.pos[0])
1313 && qFuzzyCompare(v1: a.pos[1], v2: b.pos[1])
1314 && qFuzzyCompare(v1: a.normal[0], v2: b.normal[0])
1315 && qFuzzyCompare(v1: a.normal[1], v2: b.normal[1]);
1316}
1317
1318inline size_t qHash(const Edge &e, size_t seed) Q_DECL_NOTHROW
1319{
1320 return qHash(key: e.pos[0].x(), seed) ^ qHash(key: e.pos[0].y()) ^ qHash(key: e.pos[0].z())
1321 ^ qHash(key: e.pos[1].x()) ^ qHash(key: e.pos[1].y()) ^ qHash(key: e.pos[1].z());
1322}
1323
1324struct EdgeUV {
1325 std::array<QVector2D, 2> uv;
1326 bool seam = false;
1327};
1328
1329struct SeamUV {
1330 std::array<std::array<QVector2D, 2>, 2> uv;
1331};
1332
1333static inline bool vectorLessThan(const QVector3D &a, const QVector3D &b)
1334{
1335 if (a.x() == b.x()) {
1336 if (a.y() == b.y())
1337 return a.z() < b.z();
1338 else
1339 return a.y() < b.y();
1340 }
1341 return a.x() < b.x();
1342}
1343
1344static inline float floatSign(float f)
1345{
1346 return f > 0.0f ? 1.0f : (f < 0.0f ? -1.0f : 0.0f);
1347}
1348
1349static inline QVector2D flooredVec(const QVector2D &v)
1350{
1351 return QVector2D(std::floor(x: v.x()), std::floor(x: v.y()));
1352}
1353
1354static inline QVector2D projectPointToLine(const QVector2D &point, const std::array<QVector2D, 2> &line)
1355{
1356 const QVector2D p = point - line[0];
1357 const QVector2D n = line[1] - line[0];
1358 const float lengthSquared = n.lengthSquared();
1359 if (!qFuzzyIsNull(f: lengthSquared)) {
1360 const float d = (n.x() * p.x() + n.y() * p.y()) / lengthSquared;
1361 return d <= 0.0f ? line[0] : (d >= 1.0f ? line[1] : line[0] + n * d);
1362 }
1363 return line[0];
1364}
1365
1366static void blendLine(const QVector2D &from, const QVector2D &to,
1367 const QVector2D &uvFrom, const QVector2D &uvTo,
1368 const QByteArray &readBuf, QByteArray &writeBuf,
1369 const QSize &lightmapPixelSize)
1370{
1371 const QVector2D size(lightmapPixelSize.width(), lightmapPixelSize.height());
1372 const std::array<QVector2D, 2> line = { QVector2D(from.x(), 1.0f - from.y()) * size,
1373 QVector2D(to.x(), 1.0f - to.y()) * size };
1374 const float lineLength = line[0].distanceToPoint(point: line[1]);
1375 if (qFuzzyIsNull(f: lineLength))
1376 return;
1377
1378 const QVector2D startPixel = flooredVec(v: line[0]);
1379 const QVector2D endPixel = flooredVec(v: line[1]);
1380
1381 const QVector2D dir = (line[1] - line[0]).normalized();
1382 const QVector2D tStep(1.0f / std::abs(x: dir.x()), 1.0f / std::abs(x: dir.y()));
1383 const QVector2D pixelStep(floatSign(f: dir.x()), floatSign(f: dir.y()));
1384
1385 QVector2D nextT(std::fmod(x: line[0].x(), y: 1.0f), std::fmod(x: line[0].y(), y: 1.0f));
1386 if (pixelStep.x() == 1.0f)
1387 nextT.setX(1.0f - nextT.x());
1388 if (pixelStep.y() == 1.0f)
1389 nextT.setY(1.0f - nextT.y());
1390
1391 if (!qFuzzyIsNull(f: dir.x()))
1392 nextT.setX(nextT.x() / std::abs(x: dir.x()));
1393 else
1394 nextT.setX(std::numeric_limits<float>::max());
1395
1396 if (!qFuzzyIsNull(f: dir.y()))
1397 nextT.setY(nextT.y() / std::abs(x: dir.y()));
1398 else
1399 nextT.setY(std::numeric_limits<float>::max());
1400
1401 float *fpW = reinterpret_cast<float *>(writeBuf.data());
1402 const float *fpR = reinterpret_cast<const float *>(readBuf.constData());
1403
1404 QVector2D pixel = startPixel;
1405
1406 while (startPixel.distanceToPoint(point: pixel) < lineLength + 1.0f) {
1407 const QVector2D point = projectPointToLine(point: pixel + QVector2D(0.5f, 0.5f), line);
1408 const float t = line[0].distanceToPoint(point) / lineLength;
1409 const QVector2D uvInterp = uvFrom * (1.0 - t) + uvTo * t;
1410 const QVector2D sampledPixel = flooredVec(v: QVector2D(uvInterp.x(), 1.0f - uvInterp.y()) * size);
1411
1412 const int sampOfs = (int(sampledPixel.x()) + int(sampledPixel.y()) * lightmapPixelSize.width()) * 4;
1413 const QVector3D sampledColor(fpR[sampOfs], fpR[sampOfs + 1], fpR[sampOfs + 2]);
1414 const int pixOfs = (int(pixel.x()) + int(pixel.y()) * lightmapPixelSize.width()) * 4;
1415 QVector3D currentColor(fpW[pixOfs], fpW[pixOfs + 1], fpW[pixOfs + 2]);
1416 currentColor = currentColor * 0.6f + sampledColor * 0.4f;
1417 fpW[pixOfs] = currentColor.x();
1418 fpW[pixOfs + 1] = currentColor.y();
1419 fpW[pixOfs + 2] = currentColor.z();
1420
1421 if (pixel != endPixel) {
1422 if (nextT.x() < nextT.y()) {
1423 pixel.setX(pixel.x() + pixelStep.x());
1424 nextT.setX(nextT.x() + tStep.x());
1425 } else {
1426 pixel.setY(pixel.y() + pixelStep.y());
1427 nextT.setY(nextT.y() + tStep.y());
1428 }
1429 } else {
1430 break;
1431 }
1432 }
1433}
1434
1435bool QSSGLightmapperPrivate::postProcess()
1436{
1437 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(q: rhiCtx);
1438 QRhi *rhi = rhiCtx->rhi();
1439 QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
1440 const int bakedLightingModelCount = bakedLightingModels.size();
1441
1442 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Post-processing..."));
1443 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
1444 QElapsedTimer postProcessTimer;
1445 postProcessTimer.start();
1446
1447 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
1448 // only care about the ones that will store the lightmap image persistently
1449 if (!lm.model->hasLightmap())
1450 continue;
1451
1452 Lightmap &lightmap(lightmaps[lmIdx]);
1453
1454 // Assemble the RGBA32F image from the baker data structures
1455 QByteArray lightmapFP32(lightmap.entries.size() * 4 * sizeof(float), Qt::Uninitialized);
1456 float *lightmapFloatPtr = reinterpret_cast<float *>(lightmapFP32.data());
1457 for (const LightmapEntry &lmPix : std::as_const(t&: lightmap.entries)) {
1458 *lightmapFloatPtr++ = lmPix.allLight.x();
1459 *lightmapFloatPtr++ = lmPix.allLight.y();
1460 *lightmapFloatPtr++ = lmPix.allLight.z();
1461 *lightmapFloatPtr++ = lmPix.isValid() ? 1.0f : 0.0f;
1462 }
1463
1464 // Dilate
1465 const QRhiViewport viewport(0, 0, float(lightmap.pixelSize.width()), float(lightmap.pixelSize.height()));
1466
1467 std::unique_ptr<QRhiTexture> lightmapTex(rhi->newTexture(format: QRhiTexture::RGBA32F, pixelSize: lightmap.pixelSize));
1468 if (!lightmapTex->create()) {
1469 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for postprocessing"));
1470 return false;
1471 }
1472 std::unique_ptr<QRhiTexture> dilatedLightmapTex(rhi->newTexture(format: QRhiTexture::RGBA32F, pixelSize: lightmap.pixelSize, sampleCount: 1,
1473 flags: QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
1474 if (!dilatedLightmapTex->create()) {
1475 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 dest. texture for postprocessing"));
1476 return false;
1477 }
1478 QRhiTextureRenderTargetDescription rtDescDilate(dilatedLightmapTex.get());
1479 std::unique_ptr<QRhiTextureRenderTarget> rtDilate(rhi->newTextureRenderTarget(desc: rtDescDilate));
1480 std::unique_ptr<QRhiRenderPassDescriptor> rpDescDilate(rtDilate->newCompatibleRenderPassDescriptor());
1481 rtDilate->setRenderPassDescriptor(rpDescDilate.get());
1482 if (!rtDilate->create()) {
1483 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create postprocessing texture render target"));
1484 return false;
1485 }
1486 QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
1487 QRhiTextureSubresourceUploadDescription lightmapTexUpload(lightmapFP32.constData(), lightmapFP32.size());
1488 resUpd->uploadTexture(tex: lightmapTex.get(), desc: QRhiTextureUploadDescription({ 0, 0, lightmapTexUpload }));
1489 QSSGRhiShaderResourceBindingList bindings;
1490 QRhiSampler *nearestSampler = rhiCtx->sampler(samplerDescription: { .minFilter: QRhiSampler::Nearest, .magFilter: QRhiSampler::Nearest, .mipmap: QRhiSampler::None,
1491 .hTiling: QRhiSampler::ClampToEdge, .vTiling: QRhiSampler::ClampToEdge, .zTiling: QRhiSampler::Repeat });
1492 bindings.addTexture(binding: 0, stage: QRhiShaderResourceBinding::FragmentStage, tex: lightmapTex.get(), sampler: nearestSampler);
1493 renderer->rhiQuadRenderer()->prepareQuad(rhiCtx, maybeRub: resUpd);
1494 const auto &shaderCache = renderer->contextInterface()->shaderCache();
1495 const auto &lmDilatePipeline = shaderCache->getBuiltInRhiShaders().getRhiLightmapDilateShader();
1496 if (!lmDilatePipeline) {
1497 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load shaders"));
1498 return false;
1499 }
1500 QSSGRhiGraphicsPipelineState dilatePs;
1501 dilatePs.viewport = viewport;
1502 QSSGRhiGraphicsPipelineStatePrivate::setShaderPipeline(ps&: dilatePs, pipeline: lmDilatePipeline.get());
1503 renderer->rhiQuadRenderer()->recordRenderQuadPass(rhiCtx, ps: &dilatePs, srb: rhiCtxD->srb(bindings), rt: rtDilate.get(), flags: QSSGRhiQuadRenderer::UvCoords);
1504 resUpd = rhi->nextResourceUpdateBatch();
1505 QRhiReadbackResult dilateReadResult;
1506 resUpd->readBackTexture(rb: { dilatedLightmapTex.get() }, result: &dilateReadResult);
1507 cb->resourceUpdate(resourceUpdates: resUpd);
1508
1509 // Submit and wait for completion.
1510 rhi->finish();
1511
1512 lightmap.imageFP32 = dilateReadResult.data;
1513
1514 // Reduce UV seams by collecting all edges (going through all
1515 // triangles), looking for (fuzzy)matching ones, then drawing lines
1516 // with blending on top.
1517 const DrawInfo &drawInfo(drawInfos[lmIdx]);
1518 const char *vbase = drawInfo.vertexData.constData();
1519 const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
1520
1521 // topology is Triangles, would be indexed draw - get rid of the index
1522 // buffer, need nothing but triangles afterwards
1523 qsizetype assembledVertexCount = 0;
1524 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx])
1525 assembledVertexCount += subMeshInfo.count;
1526 QVector<QVector3D> smPos(assembledVertexCount);
1527 QVector<QVector3D> smNormal(assembledVertexCount);
1528 QVector<QVector2D> smCoord(assembledVertexCount);
1529 qsizetype vertexIdx = 0;
1530 for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
1531 for (quint32 i = 0; i < subMeshInfo.count; ++i) {
1532 const quint32 idx = *(ibase + subMeshInfo.offset + i);
1533 const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
1534 float x = *src++;
1535 float y = *src++;
1536 float z = *src++;
1537 smPos[vertexIdx] = QVector3D(x, y, z);
1538 src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
1539 x = *src++;
1540 y = *src++;
1541 z = *src++;
1542 smNormal[vertexIdx] = QVector3D(x, y, z);
1543 src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
1544 x = *src++;
1545 y = *src++;
1546 smCoord[vertexIdx] = QVector2D(x, y);
1547 ++vertexIdx;
1548 }
1549 }
1550
1551 QHash<Edge, EdgeUV> edgeUVMap;
1552 QVector<SeamUV> seams;
1553 for (vertexIdx = 0; vertexIdx < assembledVertexCount; vertexIdx += 3) {
1554 QVector3D triVert[3] = { smPos[vertexIdx], smPos[vertexIdx + 1], smPos[vertexIdx + 2] };
1555 QVector3D triNorm[3] = { smNormal[vertexIdx], smNormal[vertexIdx + 1], smNormal[vertexIdx + 2] };
1556 QVector2D triUV[3] = { smCoord[vertexIdx], smCoord[vertexIdx + 1], smCoord[vertexIdx + 2] };
1557
1558 for (int i = 0; i < 3; ++i) {
1559 int i0 = i;
1560 int i1 = (i + 1) % 3;
1561 if (vectorLessThan(a: triVert[i1], b: triVert[i0]))
1562 std::swap(a&: i0, b&: i1);
1563
1564 const Edge e = {
1565 .pos: { triVert[i0], triVert[i1] },
1566 .normal: { triNorm[i0], triNorm[i1] }
1567 };
1568 const EdgeUV edgeUV = { .uv: { triUV[i0], triUV[i1] } };
1569 auto it = edgeUVMap.find(key: e);
1570 if (it == edgeUVMap.end()) {
1571 edgeUVMap.insert(key: e, value: edgeUV);
1572 } else if (!qFuzzyCompare(v1: it->uv[0], v2: edgeUV.uv[0]) || !qFuzzyCompare(v1: it->uv[1], v2: edgeUV.uv[1])) {
1573 if (!it->seam) {
1574 seams.append(t: SeamUV({ .uv: { edgeUV.uv, it->uv } }));
1575 it->seam = true;
1576 }
1577 }
1578 }
1579 }
1580 qDebug() << "lm:" << seams.size() << "UV seams in" << lm.model;
1581
1582 QByteArray workBuf(lightmap.imageFP32.size(), Qt::Uninitialized);
1583 for (int blendIter = 0; blendIter < LM_SEAM_BLEND_ITER_COUNT; ++blendIter) {
1584 memcpy(dest: workBuf.data(), src: lightmap.imageFP32.constData(), n: lightmap.imageFP32.size());
1585 for (int seamIdx = 0, end = seams.size(); seamIdx != end; ++seamIdx) {
1586 const SeamUV &seam(seams[seamIdx]);
1587 blendLine(from: seam.uv[0][0], to: seam.uv[0][1],
1588 uvFrom: seam.uv[1][0], uvTo: seam.uv[1][1],
1589 readBuf: workBuf, writeBuf&: lightmap.imageFP32, lightmapPixelSize: lightmap.pixelSize);
1590 blendLine(from: seam.uv[1][0], to: seam.uv[1][1],
1591 uvFrom: seam.uv[0][0], uvTo: seam.uv[0][1],
1592 readBuf: workBuf, writeBuf&: lightmap.imageFP32, lightmapPixelSize: lightmap.pixelSize);
1593 }
1594 }
1595
1596 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Post-processing for model %1 done in %2").
1597 arg(a: lm.model->lightmapKey).
1598 arg(a: postProcessTimer.elapsed()));
1599 }
1600
1601 return true;
1602}
1603
1604bool QSSGLightmapperPrivate::storeLightmaps()
1605{
1606 const int bakedLightingModelCount = bakedLightingModels.size();
1607 QByteArray listContents;
1608
1609 for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
1610 const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
1611 // only care about the ones that want to store the lightmap image persistently
1612 if (!lm.model->hasLightmap())
1613 continue;
1614
1615 QElapsedTimer writeTimer;
1616 writeTimer.start();
1617
1618 // An empty outputFolder equates to working directory
1619 QString outputFolder;
1620 if (!lm.model->lightmapLoadPath.startsWith(QStringLiteral(":/")))
1621 outputFolder = lm.model->lightmapLoadPath;
1622
1623 const QString fn = QSSGLightmapper::lightmapAssetPathForSave(model: *lm.model, asset: QSSGLightmapper::LightmapAsset::LightmapImage, outputFolder);
1624 const QByteArray fns = fn.toUtf8();
1625
1626 listContents += QFileInfo(fn).absoluteFilePath().toUtf8();
1627 listContents += '\n';
1628
1629 const Lightmap &lightmap(lightmaps[lmIdx]);
1630
1631 if (SaveEXR(data: reinterpret_cast<const float *>(lightmap.imageFP32.constData()),
1632 width: lightmap.pixelSize.width(), height: lightmap.pixelSize.height(),
1633 components: 4, save_as_fp16: false, filename: fns.constData(), err: nullptr) < 0)
1634 {
1635 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to write out lightmap"));
1636 return false;
1637 }
1638
1639 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Lightmap saved for model %1 to %2 in %3 ms").
1640 arg(a: lm.model->lightmapKey).
1641 arg(a: fn).
1642 arg(a: writeTimer.elapsed()));
1643 const DrawInfo &bakeModelDrawInfo(drawInfos[lmIdx]);
1644 if (bakeModelDrawInfo.meshWithLightmapUV.isValid()) {
1645 writeTimer.start();
1646 QFile f(QSSGLightmapper::lightmapAssetPathForSave(model: *lm.model, asset: QSSGLightmapper::LightmapAsset::MeshWithLightmapUV, outputFolder));
1647 if (f.open(flags: QIODevice::WriteOnly | QIODevice::Truncate)) {
1648 bakeModelDrawInfo.meshWithLightmapUV.save(device: &f);
1649 } else {
1650 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to write mesh with lightmap UV data to '%1'").
1651 arg(a: f.fileName()));
1652 return false;
1653 }
1654 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Lightmap-compatible mesh saved for model %1 to %2 in %3 ms").
1655 arg(a: lm.model->lightmapKey).
1656 arg(a: f.fileName()).
1657 arg(a: writeTimer.elapsed()));
1658 } // else the mesh had a lightmap uv channel to begin with, no need to save another version of it
1659 }
1660
1661 QFile listFile(QSSGLightmapper::lightmapAssetPathForSave(asset: QSSGLightmapper::LightmapAsset::LightmapImageList));
1662 if (!listFile.open(flags: QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
1663 sendOutputInfo(type: QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create lightmap list file %1").
1664 arg(a: listFile.fileName()));
1665 return false;
1666 }
1667 listFile.write(data: listContents);
1668
1669 return true;
1670}
1671
1672void QSSGLightmapperPrivate::sendOutputInfo(QSSGLightmapper::BakingStatus type, std::optional<QString> msg)
1673{
1674 QString result;
1675
1676 switch (type)
1677 {
1678 case QSSGLightmapper::BakingStatus::None:
1679 return;
1680 case QSSGLightmapper::BakingStatus::Progress:
1681 result = QStringLiteral("[lm] Progress");
1682 break;
1683 case QSSGLightmapper::BakingStatus::Error:
1684 result = QStringLiteral("[lm] Error");
1685 break;
1686 case QSSGLightmapper::BakingStatus::Warning:
1687 result = QStringLiteral("[lm] Warning");
1688 break;
1689 case QSSGLightmapper::BakingStatus::Cancelled:
1690 result = QStringLiteral("[lm] Cancelled");
1691 break;
1692 case QSSGLightmapper::BakingStatus::Complete:
1693 result = QStringLiteral("[lm] Complete");
1694 break;
1695 }
1696
1697 if (msg.has_value())
1698 result.append(QStringLiteral(": ") + msg.value());
1699
1700 if (type == QSSGLightmapper::BakingStatus::Warning)
1701 qWarning() << result;
1702 else
1703 qDebug() << result;
1704
1705 if (outputCallback)
1706 outputCallback(type, msg, &bakingControl);
1707}
1708
1709bool QSSGLightmapper::bake()
1710{
1711 QElapsedTimer totalTimer;
1712 totalTimer.start();
1713
1714 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Bake starting..."));
1715 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Total models registered: %1").arg(a: d->bakedLightingModels.size()));
1716
1717 if (d->bakedLightingModels.isEmpty()) {
1718 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by LightMapper, No Models to bake"));
1719 return false;
1720 }
1721
1722 if (!d->commitGeometry()) {
1723 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking failed"));
1724 return false;
1725 }
1726
1727 if (!d->prepareLightmaps()) {
1728 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking failed"));
1729 return false;
1730 }
1731
1732 if (d->bakingControl.cancelled) {
1733 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
1734 return false;
1735 }
1736
1737 d->computeDirectLight();
1738
1739 if (d->bakingControl.cancelled) {
1740 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
1741 return false;
1742 }
1743
1744 if (d->options.indirectLightEnabled)
1745 d->computeIndirectLight();
1746
1747 if (d->bakingControl.cancelled) {
1748 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
1749 return false;
1750 }
1751
1752 if (!d->postProcess()) {
1753 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking failed"));
1754 return false;
1755 }
1756
1757 if (d->bakingControl.cancelled) {
1758 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
1759 return false;
1760 }
1761
1762 if (!d->storeLightmaps()) {
1763 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking failed"));
1764 return false;
1765 }
1766
1767 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Progress, QStringLiteral("Baking took %1 ms").arg(a: totalTimer.elapsed()));
1768 d->sendOutputInfo(type: QSSGLightmapper::BakingStatus::Complete, msg: std::nullopt);
1769 return true;
1770}
1771
1772#else
1773
1774QSSGLightmapper::QSSGLightmapper(QSSGRhiContext *, QSSGRenderer *)
1775{
1776}
1777
1778QSSGLightmapper::~QSSGLightmapper()
1779{
1780}
1781
1782void QSSGLightmapper::reset()
1783{
1784}
1785
1786void QSSGLightmapper::setOptions(const QSSGLightmapperOptions &)
1787{
1788}
1789
1790void QSSGLightmapper::setOutputCallback(Callback )
1791{
1792}
1793
1794qsizetype QSSGLightmapper::add(const QSSGBakedLightingModel &)
1795{
1796 return 0;
1797}
1798
1799bool QSSGLightmapper::bake()
1800{
1801 qWarning("Qt Quick 3D was built without the lightmapper; cannot bake lightmaps");
1802 return false;
1803}
1804
1805#endif // QT_QUICK3D_HAS_LIGHTMAPPER
1806
1807QString QSSGLightmapper::lightmapAssetPathForLoad(const QSSGRenderModel &model, LightmapAsset asset)
1808{
1809 QString result;
1810 if (!model.lightmapLoadPath.isEmpty()) {
1811 result += model.lightmapLoadPath;
1812 if (!result.endsWith(c: QLatin1Char('/')))
1813 result += QLatin1Char('/');
1814 }
1815 switch (asset) {
1816 case LightmapAsset::LightmapImage:
1817 result += QStringLiteral("qlm_%1.exr").arg(a: model.lightmapKey);
1818 break;
1819 case LightmapAsset::MeshWithLightmapUV:
1820 result += QStringLiteral("qlm_%1.mesh").arg(a: model.lightmapKey);
1821 break;
1822 default:
1823 return QString();
1824 }
1825 return result;
1826}
1827
1828QString QSSGLightmapper::lightmapAssetPathForSave(const QSSGRenderModel &model, LightmapAsset asset, const QString& outputFolder)
1829{
1830 QString result = outputFolder;
1831 if (!result.isEmpty() && !result.endsWith(c: QLatin1Char('/')))
1832 result += QLatin1Char('/');
1833
1834 switch (asset) {
1835 case LightmapAsset::LightmapImage:
1836 result += QStringLiteral("qlm_%1.exr").arg(a: model.lightmapKey);
1837 break;
1838 case LightmapAsset::MeshWithLightmapUV:
1839 result += QStringLiteral("qlm_%1.mesh").arg(a: model.lightmapKey);
1840 break;
1841 default:
1842 result += lightmapAssetPathForSave(asset, outputFolder);
1843 break;
1844 }
1845 return result;
1846}
1847
1848QString QSSGLightmapper::lightmapAssetPathForSave(LightmapAsset asset, const QString& outputFolder)
1849{
1850 QString result = outputFolder;
1851 if (!result.isEmpty() && !result.endsWith(c: QLatin1Char('/')))
1852 result += QLatin1Char('/');
1853
1854 switch (asset) {
1855 case LightmapAsset::LightmapImageList:
1856 result += QStringLiteral("qlm_list.txt");
1857 break;
1858 default:
1859 break;
1860 }
1861 return result;
1862}
1863
1864QT_END_NAMESPACE
1865

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