aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKristoffer Skau <[email protected]>2025-06-25 11:03:59 +0200
committerJonas Karlsson <[email protected]>2025-07-03 12:08:56 +0200
commit8d2873fb8ffbb4992f4392fb09d6df3b5b027fed (patch)
treeb345462b3f72d8dddaaf3689e5b1959de529479d
parent1faeac6a18d9398a493ec9b008f46d80005bc2e9 (diff)
Lightmapper: Per model bakingHEADdev
Restructure the baking stages to be per model and continously use a temp lightmap file during the baking process. This allows us to cache way less data to lower the memory usage further. Pick-to: 6.10 Change-Id: I0305f22724befbf5c27664162da6a55f4aa31b1f Reviewed-by: Jonas Karlsson <[email protected]>
-rw-r--r--src/runtimerender/rendererimpl/qssglightmapper.cpp1700
1 files changed, 846 insertions, 854 deletions
diff --git a/src/runtimerender/rendererimpl/qssglightmapper.cpp b/src/runtimerender/rendererimpl/qssglightmapper.cpp
index 5756b040..66e39c48 100644
--- a/src/runtimerender/rendererimpl/qssglightmapper.cpp
+++ b/src/runtimerender/rendererimpl/qssglightmapper.cpp
@@ -178,7 +178,7 @@ struct QSSGLightmapperPrivate
QRhiVertexInputAttribute::Format binormalFormat = QRhiVertexInputAttribute::Float;
int meshIndex = -1; // Maps to an index in meshInfos;
};
- QVector<DrawInfo> drawInfos;
+ QVector<DrawInfo> drawInfos; // per model
QVector<QByteArray> meshes;
struct Light {
@@ -212,103 +212,74 @@ struct QSSGLightmapperPrivate
QByteArray emissions; // vec4, static factor * emission map value
};
- struct LightmapEntry {
+ struct ModelTexel {
QVector3D worldPos;
QVector3D normal;
QVector4D baseColor; // static color * texture map value (both linear)
QVector3D emission; // static factor * emission map value
bool isValid() const { return !worldPos.isNull() && !normal.isNull(); }
- // This contains the direct light of all lights regardless if they are indirect only.
- // It is only used for computation of indirectLight.
- QVector3D directLightAll;
- QVector3D directLight;
- QVector3D indirectLight;
};
- struct Lightmap {
- Lightmap(const QSize &pixelSize) : pixelSize(pixelSize) {
- entries.resize(pixelSize.width() * pixelSize.height());
- }
- QSize pixelSize;
- QVector<LightmapEntry> entries;
- QByteArray indirectFP32;
- QByteArray directFP32;
- QByteArray chartsMask;
- bool hasBaseColorTransparency = false;
- };
- QVector<Lightmap> lightmaps;
+
+ QVector<QVector<ModelTexel>> modelTexels; // commit geom
+ QVector<bool> modelHasBaseColorTransparency;
+ QVector<quint32> numValidTexels;
+
QVector<int> geomLightmapMap; // [geomId] -> index in lightmaps (NB lightmap is per-model, geomId is per-submesh)
QVector<float> subMeshOpacityMap; // [geomId] -> opacity
int totalUnusedEntries = 0;
- int totalProgressPercent = 0;
- qint64 estimatedTimeRemaining = -1;
+ double totalProgress = 0; // [0-1]
+ qint64 estimatedTimeRemaining = -1; // ms
+ qint64 texelsDone = 0;
+
+ qint64 totalIncrementsToBeMade = 0;
+ qint64 incrementsDone = 0;
- inline const LightmapEntry &texelForLightmapUV(unsigned int geomId, float u, float v) const
+ inline const ModelTexel &texelForLightmapUV(unsigned int geomId, float u, float v) const
{
// find the hit texel in the lightmap for the model to which the submesh with geomId belongs
- const Lightmap &hitLightmap(lightmaps[geomLightmapMap[geomId]]);
+ const int modelIdx = geomLightmapMap[geomId];
+ QSize texelSize = drawInfos[modelIdx].lightmapSize;
u = qBound(0.0f, u, 1.0f);
// flip V, CPU-side data is top-left based
v = 1.0f - qBound(0.0f, v, 1.0f);
- const int w = hitLightmap.pixelSize.width();
- const int h = hitLightmap.pixelSize.height();
+ const int w = texelSize.width();
+ const int h = texelSize.height();
const int x = qBound(0, int(w * u), w - 1);
const int y = qBound(0, int(h * v), h - 1);
+ const int texelIdx = x + y * w;
- return hitLightmap.entries[x + y * w];
+ return modelTexels[modelIdx][texelIdx];
}
- struct StageProgressReporter
- {
- StageProgressReporter(int &currentTotal, int to) : actualTotal(currentTotal), from(currentTotal), to(to) { }
-
- int initial() const { return from; }
- int report(double localProgress) const
- {
- actualTotal = from + int(localProgress * (to - from));
- return actualTotal;
- }
-
- private:
- int &actualTotal;
- int from;
- int to;
- };
-
- enum class Stage {
- CommitGeometry = 0,
- PrepareLightmaps,
- ComputeDirectLight,
- ComputeIndirectLight,
- PostProcess,
- StoreLightmaps,
- DenoiseLightmaps,
+ bool userCancelled();
+ void sendOutputInfo(QSSGLightmapper::BakingStatus type,
+ std::optional<QString> msg,
+ bool outputToConsole = true,
+ bool outputConsoleTimeRemanining = false);
+ bool commitGeometry();
+ bool prepareLightmaps();
+ QVector<QVector3D> computeDirectLight(int lmIdx);
+ QVector<QVector3D> computeIndirectLight(int lmIdx,
+ int wgSizePerGroup,
+ int wgCount);
+ bool storeMeshes(QSharedPointer<QSSGLightmapWriter> tempFile);
- Count
- };
- static constexpr std::size_t StageCount = static_cast<std::size_t>(Stage::Count);
- static constexpr std::array<int, StageCount> stageEndProgress { 2, 4, 8, 95, 98, 100 };
+ RasterResult rasterizeLightmap(int lmIdx,
+ QSize outputSize,
+ QVector2D minUVRegion = QVector2D(0, 0),
+ QVector2D maxUVRegion = QVector2D(1, 1));
- StageProgressReporter createReporter(Stage stage)
- {
- return { totalProgressPercent, stageEndProgress[static_cast<size_t>(stage)]};
- }
+ bool storeMetadata(int lmIdx, QSharedPointer<QSSGLightmapWriter> tempFile);
+ bool storeDirectLightData(int lmIdx, const QVector<QVector3D> &directLight, QSharedPointer<QSSGLightmapWriter> tempFile);
+ bool storeIndirectLightData(int lmIdx, const QVector<QVector3D> &indirectLight, QSharedPointer<QSSGLightmapWriter> tempFile);
+ bool storeMaskImage(int lmIdx, QSharedPointer<QSSGLightmapWriter> tempFile);
- void sendOutputInfo(QSSGLightmapper::BakingStatus type, std::optional<QString> msg, bool outputToConsole = true, bool outputConsoleTimeRemanining = false);
- bool commitGeometry(const StageProgressReporter &reporter);
- bool prepareLightmaps(const StageProgressReporter &reporter);
- void computeDirectLight(const StageProgressReporter &reporter);
- void computeIndirectLight(const StageProgressReporter &reporter);
- bool postProcess(const StageProgressReporter &reporter);
- bool storeLightmaps(const StageProgressReporter &reporter);
- bool denoiseLightmaps(const StageProgressReporter &reporter);
+ bool denoiseLightmaps();
- std::pair<QVector3D, QVector3D> sampleDirectLight(QVector3D worldPos, QVector3D normal) const;
- RasterResult rasterizeLightmap(int lmIdx,
- QSize outputSize,
- QVector2D minUVRegion = QVector2D(0, 0),
- QVector2D maxUVRegion = QVector2D(1, 1));
+ QVector3D sampleDirectLight(QVector3D worldPos, QVector3D normal, bool allLight) const;
+ QByteArray dilate(const QSize &pixelSize, const QByteArray &image);
};
// Used to output progress ETA during baking.
@@ -389,7 +360,10 @@ void QSSGLightmapper::reset()
d->subMeshInfos.clear();
d->drawInfos.clear();
d->lights.clear();
- d->lightmaps.clear();
+
+ d->modelHasBaseColorTransparency.clear();
+ d->meshes.clear();
+
d->geomLightmapMap.clear();
d->subMeshOpacityMap.clear();
@@ -404,7 +378,7 @@ void QSSGLightmapper::reset()
d->bakingControl.cancelled = false;
d->totalUnusedEntries = 0;
- d->totalProgressPercent = 0;
+ d->totalProgress = 0.0;
d->estimatedTimeRemaining = -1;
}
@@ -442,8 +416,9 @@ static void embreeFilterFunc(const RTCFilterFunctionNArguments *args)
rtcInterpolate0(geom, hit->primID, hit->u, hit->v, RTC_BUFFER_TYPE_VERTEX_ATTRIBUTE, LIGHTMAP_UV_SLOT, &hit->u, 2);
const float opacity = d->subMeshOpacityMap[hit->geomID];
- if (opacity < 1.0f || d->lightmaps[d->geomLightmapMap[hit->geomID]].hasBaseColorTransparency) {
- const QSSGLightmapperPrivate::LightmapEntry &texel(d->texelForLightmapUV(hit->geomID, hit->u, hit->v));
+ const int modelIdx = d->geomLightmapMap[hit->geomID];
+ if (opacity < 1.0f || d->modelHasBaseColorTransparency[modelIdx]) {
+ const QSSGLightmapperPrivate::ModelTexel &texel(d->texelForLightmapUV(hit->geomID, hit->u, hit->v));
// In addition to material.opacity, take at least the base color (both
// the static color and the value from the base color map, if there is
@@ -491,7 +466,7 @@ static QMatrix4x4 extractScaleMatrix(const QMatrix4x4 &transform)
return scaleMatrix;
}
-bool QSSGLightmapperPrivate::commitGeometry(const StageProgressReporter &reporter)
+bool QSSGLightmapperPrivate::commitGeometry()
{
QSSGLayerRenderData *renderData = QSSGRendererPrivate::getCurrentRenderData(*renderer);
if (!renderData) {
@@ -513,6 +488,8 @@ bool QSSGLightmapperPrivate::commitGeometry(const StageProgressReporter &reporte
const int bakedLightingModelCount = bakedLightingModels.size();
subMeshInfos.resize(bakedLightingModelCount);
drawInfos.resize(bakedLightingModelCount);
+ modelTexels.resize(bakedLightingModelCount);
+ modelHasBaseColorTransparency.resize(bakedLightingModelCount, false);
for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
@@ -855,8 +832,6 @@ bool QSSGLightmapperPrivate::commitGeometry(const StageProgressReporter &reporte
subMeshInfo.geomId = geomId++;
rtcReleaseGeometry(geom);
}
-
- reporter.report(((lmIdx + 1) / (double)bakedLightingModelCount) * 0.5); // First half
}
rtcCommitScene(rscene);
@@ -879,8 +854,6 @@ bool QSSGLightmapperPrivate::commitGeometry(const StageProgressReporter &reporte
continue;
for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx])
subMeshOpacityMap[subMeshInfo.geomId] = subMeshInfo.opacity;
-
- reporter.report(((lmIdx + 1) / (double)bakedLightingModelCount)); // Second half
}
sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Geometry setup done. Time taken: %1").arg(formatDuration(geomPrepTimer.elapsed())));
@@ -1207,7 +1180,7 @@ QSSGLightmapperPrivate::RasterResult QSSGLightmapperPrivate::rasterizeLightmap(i
return result;
}
-bool QSSGLightmapperPrivate::prepareLightmaps(const StageProgressReporter &reporter)
+bool QSSGLightmapperPrivate::prepareLightmaps()
{
QRhi *rhi = rhiCtx->rhi();
if (!rhi->isTextureFormatSupported(QRhiTexture::RGBA32F)) {
@@ -1228,6 +1201,8 @@ bool QSSGLightmapperPrivate::prepareLightmaps(const StageProgressReporter &repor
Q_ASSERT(drawInfos.size() == bakedLightingModelCount);
Q_ASSERT(subMeshInfos.size() == bakedLightingModelCount);
+ numValidTexels.resize(bakedLightingModelCount);
+
for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
QElapsedTimer rasterizeTimer;
rasterizeTimer.start();
@@ -1239,9 +1214,11 @@ bool QSSGLightmapperPrivate::prepareLightmaps(const StageProgressReporter &repor
if (!raster.success)
return false;
Q_ASSERT(lightmapSize == QSize(raster.width, raster.height));
- Lightmap lightmap(QSize(raster.width, raster.height));
const int numPixels = raster.width * raster.height;
+ QVector<ModelTexel>& texels = modelTexels[lmIdx];
+ texels.resize(numPixels);
+
const float *lmPosPtr = reinterpret_cast<const float *>(raster.worldPositions.constData());
const float *lmNormPtr = reinterpret_cast<const float *>(raster.normals.constData());
const float *lmBaseColorPtr = reinterpret_cast<const float *>(raster.baseColors.constData());
@@ -1249,7 +1226,7 @@ bool QSSGLightmapperPrivate::prepareLightmaps(const StageProgressReporter &repor
int unusedEntries = 0;
for (qsizetype i = 0; i < numPixels; ++i) {
- LightmapEntry &lmPix(lightmap.entries[i]);
+ ModelTexel &lmPix(texels[i]);
float x = *lmPosPtr++;
float y = *lmPosPtr++;
@@ -1269,7 +1246,7 @@ bool QSSGLightmapperPrivate::prepareLightmaps(const StageProgressReporter &repor
float a = *lmBaseColorPtr++;
lmPix.baseColor = QVector4D(r, g, b, a);
if (a < 1.0f)
- lightmap.hasBaseColorTransparency = true;
+ modelHasBaseColorTransparency[lmIdx] = true;
r = *lmEmissionPtr++;
g = *lmEmissionPtr++;
@@ -1277,33 +1254,30 @@ bool QSSGLightmapperPrivate::prepareLightmaps(const StageProgressReporter &repor
lmEmissionPtr++;
lmPix.emission = QVector3D(r, g, b);
- if (!lmPix.isValid())
- ++unusedEntries;
+ lmPix.isValid() ? ++numValidTexels[lmIdx] : ++unusedEntries;
}
totalUnusedEntries += unusedEntries;
sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Successfully rasterized %1/%2 lightmap texels for model %3, lightmap size %4 in %5").
- arg(lightmap.entries.size() - unusedEntries).
- arg(lightmap.entries.size()).
+ arg(texels.size() - unusedEntries).
+ arg(texels.size()).
arg(lm.model->lightmapKey).
arg(QStringLiteral("(%1, %2)").arg(raster.width).arg(raster.height)).
arg(formatDuration(rasterizeTimer.elapsed())));
- lightmaps.append(lightmap);
for (const SubMeshInfo &subMeshInfo : std::as_const(subMeshInfos[lmIdx])) {
if (!lm.model->castsShadows) // only matters if it's in the raytracer scene
continue;
- geomLightmapMap[subMeshInfo.geomId] = lightmaps.size() - 1;
+ geomLightmapMap[subMeshInfo.geomId] = lmIdx;
}
-
- reporter.report(((lmIdx + 1) / (double)bakedLightingModelCount));
}
sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Lightmap preparing done"));
return true;
}
+
struct RayHit
{
RayHit(const QVector3D &org, const QVector3D &dir, float tnear = 0.0f, float tfar = std::numeric_limits<float>::infinity()) {
@@ -1546,9 +1520,8 @@ static void blendLine(const QVector2D &from,
}
}
-std::pair<QVector3D, QVector3D> QSSGLightmapperPrivate::sampleDirectLight(QVector3D worldPos, QVector3D normal) const
+QVector3D QSSGLightmapperPrivate::sampleDirectLight(QVector3D worldPos, QVector3D normal, bool allLight) const
{
- QVector3D allDirectLight = QVector3D(0.f, 0.f, 0.f);
QVector3D directLight = QVector3D(0.f, 0.f, 0.f);
if (options.useAdaptiveBias)
@@ -1556,6 +1529,9 @@ std::pair<QVector3D, QVector3D> QSSGLightmapperPrivate::sampleDirectLight(QVecto
// 'lights' should have all lights that are either BakeModeIndirect or BakeModeAll
for (const Light &light : lights) {
+ if (light.indirectOnly && !allLight)
+ continue;
+
QVector3D lightWorldPos;
float dist = std::numeric_limits<float>::infinity();
float attenuation = 1.0f;
@@ -1591,300 +1567,320 @@ std::pair<QVector3D, QVector3D> QSSGLightmapperPrivate::sampleDirectLight(QVecto
RayHit ray(worldPos, L, options.bias, dist);
const bool lightReachable = !ray.intersect(rscene);
if (lightReachable) {
- // direct light must always be stored because indirect computation will need it
- allDirectLight += light.color * energy;
- // but we take it into account in the final result only for lights that have BakeModeAll
- if (!light.indirectOnly)
- directLight += light.color * energy;
+ directLight += light.color * energy;
}
}
- return { directLight, allDirectLight };
+ return directLight;
}
-void QSSGLightmapperPrivate::computeDirectLight(const StageProgressReporter &reporter)
+QByteArray QSSGLightmapperPrivate::dilate(const QSize &pixelSize, const QByteArray &image)
{
- Q_UNUSED(reporter);
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Computing direct lighting..."));
- QElapsedTimer fullDirectLightTimer;
- fullDirectLightTimer.start();
+ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
+ QRhi *rhi = rhiCtx->rhi();
+ QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
- const int bakedLightingModelCount = bakedLightingModels.size();
- Q_ASSERT(lightmaps.size() == bakedLightingModelCount);
+ const QRhiViewport viewport(0, 0, float(pixelSize.width()), float(pixelSize.height()));
+
+ std::unique_ptr<QRhiTexture> lightmapTex(rhi->newTexture(QRhiTexture::RGBA32F, pixelSize));
+ if (!lightmapTex->create()) {
+ sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for postprocessing"));
+ return {};
+ }
+ std::unique_ptr<QRhiTexture> dilatedLightmapTex(
+ rhi->newTexture(QRhiTexture::RGBA32F, pixelSize, 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ if (!dilatedLightmapTex->create()) {
+ sendOutputInfo(QSSGLightmapper::BakingStatus::Warning,
+ QStringLiteral("Failed to create FP32 dest. texture for postprocessing"));
+ return {};
+ }
+ QRhiTextureRenderTargetDescription rtDescDilate(dilatedLightmapTex.get());
+ std::unique_ptr<QRhiTextureRenderTarget> rtDilate(rhi->newTextureRenderTarget(rtDescDilate));
+ std::unique_ptr<QRhiRenderPassDescriptor> rpDescDilate(rtDilate->newCompatibleRenderPassDescriptor());
+ rtDilate->setRenderPassDescriptor(rpDescDilate.get());
+ if (!rtDilate->create()) {
+ sendOutputInfo(QSSGLightmapper::BakingStatus::Warning,
+ QStringLiteral("Failed to create postprocessing texture render target"));
+ return {};
+ }
+ QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
+ QRhiTextureSubresourceUploadDescription lightmapTexUpload(image.constData(), image.size());
+ resUpd->uploadTexture(lightmapTex.get(), QRhiTextureUploadDescription({ 0, 0, lightmapTexUpload }));
+ QSSGRhiShaderResourceBindingList bindings;
+ QRhiSampler *nearestSampler = rhiCtx->sampler(
+ { QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::Repeat });
+ bindings.addTexture(0, QRhiShaderResourceBinding::FragmentStage, lightmapTex.get(), nearestSampler);
+ renderer->rhiQuadRenderer()->prepareQuad(rhiCtx, resUpd);
+ const auto &shaderCache = renderer->contextInterface()->shaderCache();
+ const auto &lmDilatePipeline = shaderCache->getBuiltInRhiShaders().getRhiLightmapDilateShader();
+ if (!lmDilatePipeline) {
+ sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load shaders"));
+ return {};
+ }
+ QSSGRhiGraphicsPipelineState dilatePs;
+ dilatePs.viewport = viewport;
+ QSSGRhiGraphicsPipelineStatePrivate::setShaderPipeline(dilatePs, lmDilatePipeline.get());
+ renderer->rhiQuadRenderer()->recordRenderQuadPass(rhiCtx, &dilatePs, rhiCtxD->srb(bindings), rtDilate.get(), QSSGRhiQuadRenderer::UvCoords);
+ resUpd = rhi->nextResourceUpdateBatch();
+ QRhiReadbackResult dilateReadResult;
+ resUpd->readBackTexture({ dilatedLightmapTex.get() }, &dilateReadResult);
+ cb->resourceUpdate(resUpd);
- for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
- // direct lighting is relatively fast to calculate, so parallelize per model
- const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
+ // Submit and wait for completion.
+ rhi->finish();
- // While Light.castsShadow and Model.receivesShadows are irrelevant for
- // baked lighting (they are effectively ignored, shadows are always
- // there with baked direct lighting), Model.castsShadows is something
- // we can and should take into account.
- if (!lm.model->castsShadows)
+ return dilateReadResult.data;
+}
+
+QVector<QVector3D> QSSGLightmapperPrivate::computeDirectLight(int lmIdx)
+{
+ const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
+
+ // While Light.castsShadow and Model.receivesShadows are irrelevant for
+ // baked lighting (they are effectively ignored, shadows are always
+ // there with baked direct lighting), Model.castsShadows is something
+ // we can and should take into account.
+ if (!lm.model->castsShadows)
+ return {};
+
+ const DrawInfo &drawInfo(drawInfos[lmIdx]);
+ const char *vbase = drawInfo.vertexData.constData();
+ const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
+
+ const QSize sz = drawInfo.lightmapSize;
+ const int w = sz.width();
+ const int h = sz.height();
+ constexpr int padding = GAUSS_HALF_KERNEL_SIZE;
+ const int numPixelsFinal = w * h;
+
+ QVector<QVector3D> grid(numPixelsFinal);
+ QVector<quint32> mask(numPixelsFinal, PIXEL_VOID);
+
+ // Setup grid and mask
+ const QVector<ModelTexel>& texels = modelTexels[lmIdx];
+ for (int pixelI = 0; pixelI < numPixelsFinal; ++pixelI) {
+ const auto &entry = texels[pixelI];
+ if (!entry.isValid())
continue;
+ mask[pixelI] = PIXEL_UNSET;
+ grid[pixelI] = sampleDirectLight(entry.worldPos, entry.normal, false);
+ }
- const auto elapsedStart = fullDirectLightTimer.elapsed();
+ if (std::all_of(grid.begin(), grid.end(), [](const QVector3D &v) { return v.isNull(); })) {
+ return grid; // All black, meaning no lights hit or all are indirectOnly.
+ }
- Lightmap &lightmap(lightmaps[lmIdx]);
+ floodFill(reinterpret_cast<quint32 *>(mask.data()), h, w);
- const DrawInfo &drawInfo(drawInfos[lmIdx]);
- const char *vbase = drawInfo.vertexData.constData();
- const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
+ // Compute ideal tile size
+ const int numTilesX = DIRECT_MAP_UPSCALE_FACTOR;
+ const int numTilesY = DIRECT_MAP_UPSCALE_FACTOR;
- const QSize sz = lightmap.pixelSize;
- const int w = sz.width();
- const int h = sz.height();
- constexpr int padding = GAUSS_HALF_KERNEL_SIZE;
- const int numPixelsFinal = w * h;
+ const int tileWidth = (w + DIRECT_MAP_UPSCALE_FACTOR - 1) / DIRECT_MAP_UPSCALE_FACTOR;
+ const int tileHeight = (h + DIRECT_MAP_UPSCALE_FACTOR - 1) / DIRECT_MAP_UPSCALE_FACTOR;
- QVector<QVector3D> gridAll(numPixelsFinal);
- QVector<QVector3D> gridDirect(numPixelsFinal);
- QVector<quint32> mask(numPixelsFinal, PIXEL_VOID);
+ // Render upscaled tiles then blur and downscale to remove jaggies in output
+ for (int tileY = 0; tileY < numTilesY; ++tileY) {
+ for (int tileX = 0; tileX < numTilesX; ++tileX) {
+ const int contentTileWidth = tileWidth;
+ const int contentTileHeight = tileHeight;
- // Setup gridAll and mask
- for (int pixelI = 0; pixelI < numPixelsFinal; ++pixelI) {
- const auto &entry = lightmap.entries[pixelI];
- if (!entry.isValid())
- continue;
- mask[pixelI] = PIXEL_UNSET;
- auto [directLight, allLight] = sampleDirectLight(entry.worldPos, entry.normal);
- gridAll[pixelI] = allLight;
- // Write direct value here so we can fallback if the tile has no hits.
- gridDirect[pixelI] = directLight;
- }
- floodFill(reinterpret_cast<quint32 *>(mask.data()), h, w);
+ const int currentTileWidth = contentTileWidth + 2 * padding;
+ const int currentTileHeight = contentTileHeight + 2 * padding;
- // Compute ideal tile size
- const int numTilesX = DIRECT_MAP_UPSCALE_FACTOR;
- const int numTilesY = DIRECT_MAP_UPSCALE_FACTOR;
+ const int wExp = currentTileWidth * DIRECT_MAP_UPSCALE_FACTOR;
+ const int hExp = currentTileHeight * DIRECT_MAP_UPSCALE_FACTOR;
+ const int numPixelsExpanded = wExp * hExp;
- const int tileWidth = (w + DIRECT_MAP_UPSCALE_FACTOR - 1) / DIRECT_MAP_UPSCALE_FACTOR;
- const int tileHeight = (h + DIRECT_MAP_UPSCALE_FACTOR - 1) / DIRECT_MAP_UPSCALE_FACTOR;
+ QVector<quint32> maskTile(numPixelsExpanded, PIXEL_VOID);
+ QVector<QVector3D> gridTile(numPixelsExpanded);
- // Render upscaled tiles then blur and downscale to remove jaggies in output
- for (int tileY = 0; tileY < numTilesY; ++tileY) {
- for (int tileX = 0; tileX < numTilesX; ++tileX) {
- const int contentTileWidth = tileWidth;
- const int contentTileHeight = tileHeight;
+ // Compute full-padded pixel bounds (including kernel padding)
+ const int pixelStartX = tileX * tileWidth - padding;
+ const int pixelStartY = tileY * tileHeight - padding;
- const int currentTileWidth = contentTileWidth + 2 * padding;
- const int currentTileHeight = contentTileHeight + 2 * padding;
+ const int pixelEndX = pixelStartX + contentTileWidth + 2 * padding;
+ const int pixelEndY = pixelStartY + contentTileHeight + 2 * padding;
- const int wExp = currentTileWidth * DIRECT_MAP_UPSCALE_FACTOR;
- const int hExp = currentTileHeight * DIRECT_MAP_UPSCALE_FACTOR;
- const int numPixelsExpanded = wExp * hExp;
+ const float minU = pixelStartX / double(w);
+ const float maxV = 1.0 - pixelStartY / double(h);
+ const float maxU = pixelEndX / double(w);
+ const float minV = 1.0f - pixelEndY / double(h);
- QVector<quint32> maskTile(numPixelsExpanded, PIXEL_VOID);
- QVector<QVector3D> gridTile(numPixelsExpanded);
+ // Temporary storage for rasterized, avoids copy
+ QByteArray worldPositionsBuffer;
+ QByteArray normalsBuffer;
+ {
+ QSSGLightmapperPrivate::RasterResult raster = rasterizeLightmap(lmIdx,
+ QSize(wExp, hExp),
+ QVector2D(minU, minV),
+ QVector2D(maxU, maxV));
+ if (!raster.success)
+ return {};
+ Q_ASSERT(raster.width * raster.height == numPixelsExpanded);
+ worldPositionsBuffer = raster.worldPositions;
+ normalsBuffer = raster.normals;
+ }
- // Compute full-padded pixel bounds (including kernel padding)
- const int pixelStartX = tileX * tileWidth - padding;
- const int pixelStartY = tileY * tileHeight - padding;
+ QVector4D *worldPositions = reinterpret_cast<QVector4D *>(worldPositionsBuffer.data());
+ QVector4D *normals = reinterpret_cast<QVector4D *>(normalsBuffer.data());
- const int pixelEndX = pixelStartX + contentTileWidth + 2 * padding;
- const int pixelEndY = pixelStartY + contentTileHeight + 2 * padding;
+ for (int pixelI = 0; pixelI < numPixelsExpanded; ++pixelI) {
+ QVector3D position = worldPositions[pixelI].toVector3D();
+ QVector3D normal = normals[pixelI].toVector3D();
+ if (normal.isNull()) {
+ maskTile[pixelI] = PIXEL_VOID;
+ continue;
+ }
- const float minU = pixelStartX / double(w);
- const float maxV = 1.0 - pixelStartY / double(h);
- const float maxU = pixelEndX / double(w);
- const float minV = 1.0f - pixelEndY / double(h);
+ maskTile[pixelI] = PIXEL_UNSET;
+ gridTile[pixelI] += sampleDirectLight(position, normal, false);
+ }
- // Temporary storage for rasterized, avoids copy
- QByteArray worldPositionsBuffer;
- QByteArray normalsBuffer;
- {
- QSSGLightmapperPrivate::RasterResult raster = rasterizeLightmap(lmIdx,
- QSize(wExp, hExp),
- QVector2D(minU, minV),
- QVector2D(maxU, maxV));
- if (!raster.success)
- return;
- Q_ASSERT(raster.width * raster.height == numPixelsExpanded);
- worldPositionsBuffer = raster.worldPositions;
- normalsBuffer = raster.normals;
- }
+ floodFill(reinterpret_cast<quint32 *>(maskTile.data()), hExp, wExp); // Flood fill mask in place
+ gridTile = applyGaussianBlur(gridTile, maskTile, wExp, hExp, 3.f);
- QVector4D *worldPositions = reinterpret_cast<QVector4D *>(worldPositionsBuffer.data());
- QVector4D *normals = reinterpret_cast<QVector4D *>(normalsBuffer.data());
+ const int startX = tileX * tileWidth;
+ const int endX = qMin(w, startX + tileWidth);
+ const int startY = tileY * tileHeight;
+ const int endY = qMin(h, startY + tileHeight);
- for (int pixelI = 0; pixelI < numPixelsExpanded; ++pixelI) {
- QVector3D position = worldPositions[pixelI].toVector3D();
- QVector3D normal = normals[pixelI].toVector3D();
- if (normal.isNull()) {
- maskTile[pixelI] = PIXEL_VOID;
- continue;
- }
+ // Downscale and put in the finished grid
+ // Loop through each pixel in the output image
+ for (int y = startY; y < endY; ++y) {
+ const int ySrc = (padding + y - startY) * DIRECT_MAP_UPSCALE_FACTOR;
+ Q_ASSERT(ySrc < hExp);
+ for (int x = startX; x < endX; ++x) {
+ const int xSrc = (padding + x - startX) * DIRECT_MAP_UPSCALE_FACTOR;
+ Q_ASSERT(xSrc < wExp);
- maskTile[pixelI] = PIXEL_UNSET;
- auto [directLight, _] = sampleDirectLight(position, normal);
- gridTile[pixelI] += directLight;
- }
+ if (mask[y * w + x] == PIXEL_VOID)
+ continue;
- floodFill(reinterpret_cast<quint32 *>(maskTile.data()), hExp, wExp); // Flood fill mask in place
- gridTile = applyGaussianBlur(gridTile, maskTile, wExp, hExp, 3.f);
-
- const int startX = tileX * tileWidth;
- const int endX = qMin(w, startX + tileWidth);
- const int startY = tileY * tileHeight;
- const int endY = qMin(h, startY + tileHeight);
-
- // Downscale and put in the finished grid
- // Loop through each pixel in the output image
- for (int y = startY; y < endY; ++y) {
- const int ySrc = (padding + y - startY) * DIRECT_MAP_UPSCALE_FACTOR;
- Q_ASSERT(ySrc < hExp);
- for (int x = startX; x < endX; ++x) {
- const int xSrc = (padding + x - startX) * DIRECT_MAP_UPSCALE_FACTOR;
- Q_ASSERT(xSrc < wExp);
-
- if (mask[y * w + x] == PIXEL_VOID)
- continue;
-
- const int dstPixelI = y * w + x;
- QVector3D average;
- int hits = 0;
- for (int sY = 0; sY < DIRECT_MAP_UPSCALE_FACTOR; ++sY) {
- for (int sX = 0; sX < DIRECT_MAP_UPSCALE_FACTOR; ++sX) {
- int srcPixelI = (ySrc + sY) * wExp + (xSrc + sX);
- Q_ASSERT(srcPixelI < numPixelsExpanded);
- if (maskTile[srcPixelI] == PIXEL_VOID)
- continue;
- average += gridTile[srcPixelI];
- ++hits;
- }
+ const int dstPixelI = y * w + x;
+ QVector3D average;
+ int hits = 0;
+ for (int sY = 0; sY < DIRECT_MAP_UPSCALE_FACTOR; ++sY) {
+ for (int sX = 0; sX < DIRECT_MAP_UPSCALE_FACTOR; ++sX) {
+ int srcPixelI = (ySrc + sY) * wExp + (xSrc + sX);
+ Q_ASSERT(srcPixelI < numPixelsExpanded);
+ if (maskTile[srcPixelI] == PIXEL_VOID)
+ continue;
+ average += gridTile[srcPixelI];
+ ++hits;
}
-
- // Write value only if we have any hits. Due to sampling and precision differences it is
- // technically possible to miss hits. In this case we fallback to the original sampled value.
- if (hits > 0)
- gridDirect[dstPixelI] = average / hits;
}
+
+ // Write value only if we have any hits. Due to sampling and precision differences it is
+ // technically possible to miss hits. In this case we fallback to the original sampled value.
+ if (hits > 0)
+ grid[dstPixelI] = average / hits;
}
}
}
+ }
- QHash<Edge, EdgeUV> edgeUVMap;
- QVector<SeamUV> seams;
+ QHash<Edge, EdgeUV> edgeUVMap;
+ QVector<SeamUV> seams;
- for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
- QVector<std::array<quint32, 3>> triangles;
- QVector<QVector3D> positions;
- QVector<QVector3D> normals;
- QVector<QVector2D> uvs;
+ for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
+ QVector<std::array<quint32, 3>> triangles;
+ QVector<QVector3D> positions;
+ QVector<QVector3D> normals;
+ QVector<QVector2D> uvs;
- triangles.reserve(subMeshInfo.count / 3);
- positions.reserve(subMeshInfo.count);
- normals.reserve(subMeshInfo.count);
- uvs.reserve(subMeshInfo.count);
+ triangles.reserve(subMeshInfo.count / 3);
+ positions.reserve(subMeshInfo.count);
+ normals.reserve(subMeshInfo.count);
+ uvs.reserve(subMeshInfo.count);
- for (quint32 i = 0; i < subMeshInfo.count / 3; ++i)
- triangles.push_back({ i * 3, i * 3 + 1, i * 3 + 2 });
+ for (quint32 i = 0; i < subMeshInfo.count / 3; ++i)
+ triangles.push_back({ i * 3, i * 3 + 1, i * 3 + 2 });
- for (quint32 i = 0; i < subMeshInfo.count; ++i) {
- const quint32 idx = *(ibase + subMeshInfo.offset + i);
- const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
- float x = *src++;
- float y = *src++;
- float z = *src++;
- positions.push_back(QVector3D(x, y, z));
- }
+ for (quint32 i = 0; i < subMeshInfo.count; ++i) {
+ const quint32 idx = *(ibase + subMeshInfo.offset + i);
+ const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
+ float x = *src++;
+ float y = *src++;
+ float z = *src++;
+ positions.push_back(QVector3D(x, y, z));
+ }
- for (quint32 i = 0; i < subMeshInfo.count; ++i) {
- const quint32 idx = *(ibase + subMeshInfo.offset + i);
- const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
- float x = *src++;
- float y = *src++;
- float z = *src++;
- normals.push_back(QVector3D(x, y, z));
- }
+ for (quint32 i = 0; i < subMeshInfo.count; ++i) {
+ const quint32 idx = *(ibase + subMeshInfo.offset + i);
+ const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
+ float x = *src++;
+ float y = *src++;
+ float z = *src++;
+ normals.push_back(QVector3D(x, y, z));
+ }
- for (quint32 i = 0; i < subMeshInfo.count; ++i) {
- const quint32 idx = *(ibase + subMeshInfo.offset + i);
- const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
- float x = *src++;
- float y = *src++;
- uvs.push_back(QVector2D(x, 1.0f - y)); // NOTE: Flip y
- }
+ for (quint32 i = 0; i < subMeshInfo.count; ++i) {
+ const quint32 idx = *(ibase + subMeshInfo.offset + i);
+ const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
+ float x = *src++;
+ float y = *src++;
+ uvs.push_back(QVector2D(x, 1.0f - y)); // NOTE: Flip y
+ }
- for (auto [i0, i1, i2] : triangles) {
- const QVector3D triVert[3] = { positions[i0], positions[i1], positions[i2] };
- const QVector3D triNorm[3] = { normals[i0], normals[i1], normals[i2] };
- const QVector2D triUV[3] = { uvs[i0], uvs[i1], uvs[i2] };
-
- for (int i = 0; i < 3; ++i) {
- int i0 = i;
- int i1 = (i + 1) % 3;
- if (vectorLessThan(triVert[i1], triVert[i0]))
- std::swap(i0, i1);
-
- const Edge e = { { triVert[i0], triVert[i1] }, { triNorm[i0], triNorm[i1] } };
- const EdgeUV edgeUV = { { triUV[i0], triUV[i1] } };
- auto it = edgeUVMap.find(e);
- if (it == edgeUVMap.end()) {
- edgeUVMap.insert(e, edgeUV);
- } else if (!qFuzzyCompare(it->uv[0], edgeUV.uv[0]) || !qFuzzyCompare(it->uv[1], edgeUV.uv[1])) {
- if (!it->seam) {
- std::array<QVector2D, 2> eUV = {QVector2D(edgeUV.uv[0][0], 1.0f - edgeUV.uv[0][1]), QVector2D(edgeUV.uv[1][0], 1.0f - edgeUV.uv[1][1])};
- std::array<QVector2D, 2> itUV = {QVector2D(it->uv[0][0], 1.0f - it->uv[0][1]), QVector2D(it->uv[1][0], 1.0f - it->uv[1][1])};
-
- seams.append(SeamUV({ { eUV, itUV } }));
- it->seam = true;
- }
+ for (auto [i0, i1, i2] : triangles) {
+ const QVector3D triVert[3] = { positions[i0], positions[i1], positions[i2] };
+ const QVector3D triNorm[3] = { normals[i0], normals[i1], normals[i2] };
+ const QVector2D triUV[3] = { uvs[i0], uvs[i1], uvs[i2] };
+
+ for (int i = 0; i < 3; ++i) {
+ int i0 = i;
+ int i1 = (i + 1) % 3;
+ if (vectorLessThan(triVert[i1], triVert[i0]))
+ std::swap(i0, i1);
+
+ const Edge e = { { triVert[i0], triVert[i1] }, { triNorm[i0], triNorm[i1] } };
+ const EdgeUV edgeUV = { { triUV[i0], triUV[i1] } };
+ auto it = edgeUVMap.find(e);
+ if (it == edgeUVMap.end()) {
+ edgeUVMap.insert(e, edgeUV);
+ } else if (!qFuzzyCompare(it->uv[0], edgeUV.uv[0]) || !qFuzzyCompare(it->uv[1], edgeUV.uv[1])) {
+ if (!it->seam) {
+ std::array<QVector2D, 2> eUV = {QVector2D(edgeUV.uv[0][0], 1.0f - edgeUV.uv[0][1]), QVector2D(edgeUV.uv[1][0], 1.0f - edgeUV.uv[1][1])};
+ std::array<QVector2D, 2> itUV = {QVector2D(it->uv[0][0], 1.0f - it->uv[0][1]), QVector2D(it->uv[1][0], 1.0f - it->uv[1][1])};
+
+ seams.append(SeamUV({ { eUV, itUV } }));
+ it->seam = true;
}
}
}
}
+ }
- // Blend edges
- // NOTE: We only need to blend 'gridDirect' since that is the resulting lightmap for direct light
- {
- QByteArray workBuf(gridDirect.size() * sizeof(QVector3D), Qt::Uninitialized);
- for (int blendIter = 0; blendIter < LM_SEAM_BLEND_ITER_COUNT; ++blendIter) {
- memcpy(workBuf.data(), gridDirect.constData(), gridDirect.size() * sizeof(QVector3D));
- for (int seamIdx = 0, end = seams.size(); seamIdx != end; ++seamIdx) {
- const SeamUV &seam(seams[seamIdx]);
- blendLine(seam.uv[0][0],
- seam.uv[0][1],
- seam.uv[1][0],
- seam.uv[1][1],
- reinterpret_cast<const float *>(workBuf.data()),
- reinterpret_cast<float *>(gridDirect.data()),
- QSize(w, h),
- 3);
- blendLine(seam.uv[1][0],
- seam.uv[1][1],
- seam.uv[0][0],
- seam.uv[0][1],
- reinterpret_cast<const float *>(workBuf.data()),
- reinterpret_cast<float *>(gridDirect.data()),
- QSize(w, h),
- 3);
- }
+ // Blend edges
+ // NOTE: We only need to blend grid since that is the resulting lightmap for direct light
+ {
+ QByteArray workBuf(grid.size() * sizeof(QVector3D), Qt::Uninitialized);
+ for (int blendIter = 0; blendIter < LM_SEAM_BLEND_ITER_COUNT; ++blendIter) {
+ memcpy(workBuf.data(), grid.constData(), grid.size() * sizeof(QVector3D));
+ for (int seamIdx = 0, end = seams.size(); seamIdx != end; ++seamIdx) {
+ const SeamUV &seam(seams[seamIdx]);
+ blendLine(seam.uv[0][0],
+ seam.uv[0][1],
+ seam.uv[1][0],
+ seam.uv[1][1],
+ reinterpret_cast<const float *>(workBuf.data()),
+ reinterpret_cast<float *>(grid.data()),
+ QSize(w, h),
+ 3);
+ blendLine(seam.uv[1][0],
+ seam.uv[1][1],
+ seam.uv[0][0],
+ seam.uv[0][1],
+ reinterpret_cast<const float *>(workBuf.data()),
+ reinterpret_cast<float *>(grid.data()),
+ QSize(w, h),
+ 3);
}
}
-
- // Copy values to lightmap entries
- for (int i = 0, n = lightmap.entries.size(); i < n; ++i) {
- QVector3D v = gridDirect[i];
- QVector3D v1 = gridAll[i];
- Q_ASSERT(v.x() >= 0.f && !std::isnan(v.x()));
- Q_ASSERT(v.y() >= 0.f && !std::isnan(v.y()));
- Q_ASSERT(v.z() >= 0.f && !std::isnan(v.z()));
- Q_ASSERT(v1.x() >= 0.f && !std::isnan(v1.x()));
- Q_ASSERT(v1.y() >= 0.f && !std::isnan(v1.y()));
- Q_ASSERT(v1.z() >= 0.f && !std::isnan(v1.z()));
- lightmap.entries[i].directLightAll = gridAll[i];
- lightmap.entries[i].directLight = gridDirect[i];
- }
-
- const auto elapsed = fullDirectLightTimer.elapsed() - elapsedStart;
-
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
- QStringLiteral("Direct light computed for model %1 in %2").arg(lm.model->lightmapKey).arg(formatDuration(elapsed)));
}
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Direct light computation completed in %1").
- arg(formatDuration(fullDirectLightTimer.elapsed())));
+ return grid;
}
// xorshift rng. this is called a lot -> rand/QRandomGenerator is out of question (way too slow)
@@ -1905,419 +1901,113 @@ static inline QVector3D cosWeightedHemisphereSample(quint32 &state)
return QVector3D(sqr1 * std::cos(r2), sqr1 * std::sin(r2), sqr1m);
}
-void QSSGLightmapperPrivate::computeIndirectLight(const StageProgressReporter &reporter)
+QVector<QVector3D> QSSGLightmapperPrivate::computeIndirectLight(int lmIdx, int wgCount, int wgSizePerGroup)
{
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Computing indirect lighting..."));
-
- int totalTexels = 0;
- for (int lmIdx = 0; lmIdx < bakedLightingModels.size(); ++lmIdx) {
- // here we only care about the models that will store the lightmap image persistently
- if (!bakedLightingModels[lmIdx].model->hasLightmap())
- continue;
-
- Lightmap &lightmap(lightmaps[lmIdx]);
- totalTexels += lightmap.entries.count();
- }
-
- totalTexels -= totalUnusedEntries;
-
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Total texels to compute: %1").arg(totalTexels));
-
- int wgSizePerGroup = qMax(1, options.indirectLightWorkgroupSize);
- int wgCount = options.indirectLightSamples / wgSizePerGroup;
- if (options.indirectLightSamples % wgSizePerGroup)
- ++wgCount;
+ const QVector<ModelTexel>& texels = modelTexels[lmIdx];
+ QVector<QVector3D> result;
+ result.resize(texels.size());
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Sample count: %1, Workgroup size: %2, Max bounces: %3, Multiplier: %4").
- arg(options.indirectLightSamples).
- arg(wgSizePerGroup).
- arg(options.indirectLightBounces).
- arg(options.indirectLightFactor));
- QElapsedTimer fullIndirectLightTimer;
- fullIndirectLightTimer.start();
+ QVector<QFuture<QVector3D>> wg(wgCount);
- const int bakedLightingModelCount = bakedLightingModels.size();
-
- int texelsDone = 0;
- constexpr int timerIntervalMs = 100;
- TimerThread timerThread;
- timerThread.setInterval(timerIntervalMs);
- // Log ETA every 5 seconds to console
- constexpr int consoleOutputInterval = 5000 / timerIntervalMs;
- int timeoutsSinceOutput = consoleOutputInterval - 1;
- timerThread.setCallback([&]()
- {
- double progress = (static_cast<double>(texelsDone) / totalTexels);
- totalProgressPercent = reporter.report(progress);
-
- double totalElapsed = fullIndirectLightTimer.elapsed();
- double avgTimePerTexel = static_cast<double>(totalElapsed) / texelsDone;
- estimatedTimeRemaining = avgTimePerTexel * (totalTexels - texelsDone);
-
- bool outputToConsole = timeoutsSinceOutput == consoleOutputInterval - 1;
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, std::nullopt, outputToConsole, outputToConsole);
- timeoutsSinceOutput = (timeoutsSinceOutput + 1) % consoleOutputInterval;
- });
-
- timerThread.start();
-
- for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
- // here we only care about the models that will store the lightmap image persistently
- if (!bakedLightingModels[lmIdx].model->hasLightmap())
+ for (int i = 0; i < texels.size(); ++i) {
+ const ModelTexel& lmPix = texels[i];
+ if (!lmPix.isValid())
continue;
- const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
- Lightmap &lightmap(lightmaps[lmIdx]);
-
- QElapsedTimer indirectLightTimer;
- indirectLightTimer.start();
-
- // indirect lighting is slow, so parallelize per groups of samples,
- // e.g. if sample count is 256 and workgroup size is 32, then do up to
- // 8 sets in parallel, each calculating 32 samples (how many of the 8
- // are really done concurrently that's up to the thread pool to manage)
-
- QVector<QFuture<QVector3D>> wg(wgCount);
-
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Computing indirect lighting for model %1").
- arg(lm.model->lightmapKey));
-
- for (LightmapEntry &lmPix : lightmap.entries) {
- if (!lmPix.isValid())
- continue;
- ++texelsDone;
-
- for (int wgIdx = 0; wgIdx < wgCount; ++wgIdx) {
- const int beginIdx = wgIdx * wgSizePerGroup;
- const int endIdx = qMin(beginIdx + wgSizePerGroup, options.indirectLightSamples);
-
- wg[wgIdx] = QtConcurrent::run([this, wgIdx, beginIdx, endIdx, &lmPix] {
- QVector3D wgResult;
- quint32 state = QRandomGenerator(wgIdx).generate();
- for (int sampleIdx = beginIdx; sampleIdx < endIdx; ++sampleIdx) {
- QVector3D position = lmPix.worldPos;
- QVector3D normal = lmPix.normal;
- QVector3D throughput(1.0f, 1.0f, 1.0f);
- QVector3D sampleResult;
-
- for (int bounce = 0; bounce < options.indirectLightBounces; ++bounce) {
- if (options.useAdaptiveBias)
- position += vectorSign(normal) * vectorAbs(position * 0.0000002f);
-
- // get a sample using a cosine-weighted hemisphere sampler
- const QVector3D sample = cosWeightedHemisphereSample(state);
-
- // transform to the point's local coordinate system
- const QVector3D v0 = qFuzzyCompare(qAbs(normal.z()), 1.0f)
- ? QVector3D(0.0f, 1.0f, 0.0f)
- : QVector3D(0.0f, 0.0f, 1.0f);
- const QVector3D tangent = QVector3D::crossProduct(v0, normal).normalized();
- const QVector3D bitangent = QVector3D::crossProduct(tangent, normal).normalized();
- QVector3D direction(
- tangent.x() * sample.x() + bitangent.x() * sample.y() + normal.x() * sample.z(),
- tangent.y() * sample.x() + bitangent.y() * sample.y() + normal.y() * sample.z(),
- tangent.z() * sample.x() + bitangent.z() * sample.y() + normal.z() * sample.z());
- direction.normalize();
-
- // probability distribution function
- const float NdotL = qMax(0.0f, QVector3D::dotProduct(normal, direction));
- const float pdf = NdotL / float(M_PI);
- if (qFuzzyIsNull(pdf))
- break;
-
- // shoot ray, stop if no hit
- RayHit ray(position, direction, options.bias);
- if (!ray.intersect(rscene))
- break;
-
- // see what (sub)mesh and which texel it intersected with
- const LightmapEntry &hitEntry = texelForLightmapUV(ray.rayhit.hit.geomID,
- ray.rayhit.hit.u,
- ray.rayhit.hit.v);
-
- // won't bounce further from a back face
- const bool hitBackFace = QVector3D::dotProduct(hitEntry.normal, direction) > 0.0f;
- if (hitBackFace)
- break;
-
- // the BRDF of a diffuse surface is albedo / PI
- const QVector3D brdf = hitEntry.baseColor.toVector3D() / float(M_PI);
-
- // calculate result for this bounce
- sampleResult += throughput * hitEntry.emission;
- throughput *= brdf * NdotL / pdf;
- sampleResult += throughput * hitEntry.directLightAll;
-
- // stop if we guess there's no point in bouncing further
- // (low throughput path wouldn't contribute much)
- const float p = qMax(qMax(throughput.x(), throughput.y()), throughput.z());
- if (p < uniformRand(state))
- break;
-
- // was not terminated: boost the energy by the probability to be terminated
- throughput /= p;
-
- // next bounce starts from the hit's position
- position = hitEntry.worldPos;
- normal = hitEntry.normal;
- }
-
- wgResult += sampleResult;
+ ++incrementsDone;
+ for (int wgIdx = 0; wgIdx < wgCount; ++wgIdx) {
+ const int beginIdx = wgIdx * wgSizePerGroup;
+ const int endIdx = qMin(beginIdx + wgSizePerGroup, options.indirectLightSamples);
+
+ wg[wgIdx] = QtConcurrent::run([this, wgIdx, beginIdx, endIdx, &lmPix] {
+ QVector3D wgResult;
+ quint32 state = QRandomGenerator(wgIdx).generate();
+ for (int sampleIdx = beginIdx; sampleIdx < endIdx; ++sampleIdx) {
+ QVector3D position = lmPix.worldPos;
+ QVector3D normal = lmPix.normal;
+ QVector3D throughput(1.0f, 1.0f, 1.0f);
+ QVector3D sampleResult;
+
+ for (int bounce = 0; bounce < options.indirectLightBounces; ++bounce) {
+ if (options.useAdaptiveBias)
+ position += vectorSign(normal) * vectorAbs(position * 0.0000002f);
+
+ // get a sample using a cosine-weighted hemisphere sampler
+ const QVector3D sample = cosWeightedHemisphereSample(state);
+
+ // transform to the point's local coordinate system
+ const QVector3D v0 = qFuzzyCompare(qAbs(normal.z()), 1.0f)
+ ? QVector3D(0.0f, 1.0f, 0.0f)
+ : QVector3D(0.0f, 0.0f, 1.0f);
+ const QVector3D tangent = QVector3D::crossProduct(v0, normal).normalized();
+ const QVector3D bitangent = QVector3D::crossProduct(tangent, normal).normalized();
+ QVector3D direction(
+ tangent.x() * sample.x() + bitangent.x() * sample.y() + normal.x() * sample.z(),
+ tangent.y() * sample.x() + bitangent.y() * sample.y() + normal.y() * sample.z(),
+ tangent.z() * sample.x() + bitangent.z() * sample.y() + normal.z() * sample.z());
+ direction.normalize();
+
+ // probability distribution function
+ const float NdotL = qMax(0.0f, QVector3D::dotProduct(normal, direction));
+ const float pdf = NdotL / float(M_PI);
+ if (qFuzzyIsNull(pdf))
+ break;
+
+ // shoot ray, stop if no hit
+ RayHit ray(position, direction, options.bias);
+ if (!ray.intersect(rscene))
+ break;
+
+ // see what (sub)mesh and which texel it intersected with
+ const ModelTexel &hitEntry = texelForLightmapUV(ray.rayhit.hit.geomID,
+ ray.rayhit.hit.u,
+ ray.rayhit.hit.v);
+
+ // won't bounce further from a back face
+ const bool hitBackFace = QVector3D::dotProduct(hitEntry.normal, direction) > 0.0f;
+ if (hitBackFace)
+ break;
+
+ // the BRDF of a diffuse surface is albedo / PI
+ const QVector3D brdf = hitEntry.baseColor.toVector3D() / float(M_PI);
+
+ // calculate result for this bounce
+ sampleResult += throughput * hitEntry.emission;
+ throughput *= brdf * NdotL / pdf;
+ QVector3D directLight = sampleDirectLight(hitEntry.worldPos, hitEntry.normal, true);
+ sampleResult += throughput * directLight;
+
+ // stop if we guess there's no point in bouncing further
+ // (low throughput path wouldn't contribute much)
+ const float p = qMax(qMax(throughput.x(), throughput.y()), throughput.z());
+ if (p < uniformRand(state))
+ break;
+
+ // was not terminated: boost the energy by the probability to be terminated
+ throughput /= p;
+
+ // next bounce starts from the hit's position
+ position = hitEntry.worldPos;
+ normal = hitEntry.normal;
}
- return wgResult;
- });
- }
-
- QVector3D totalIndirect;
- for (const auto &future : wg)
- totalIndirect += future.result();
- lmPix.indirectLight += totalIndirect * options.indirectLightFactor / options.indirectLightSamples;
-
- if (bakingControl.cancelled)
- return;
- }
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Indirect lighting computed for model %1 in %2").
- arg(lm.model->lightmapKey).
- arg(formatDuration(indirectLightTimer.elapsed())));
- }
-
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Indirect light computation completed in %1").
- arg(formatDuration(fullIndirectLightTimer.elapsed())));
-}
-
-bool QSSGLightmapperPrivate::postProcess(const StageProgressReporter &reporter)
-{
- QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
- QRhi *rhi = rhiCtx->rhi();
- QRhiCommandBuffer *cb = rhiCtx->commandBuffer();
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Post-processing..."));
-
- // Dilate
- const auto dilate = [&](QSize pixelSize, const QByteArray &image) -> std::optional<QByteArray> {
- const QRhiViewport viewport(0, 0, float(pixelSize.width()), float(pixelSize.height()));
-
- std::unique_ptr<QRhiTexture> lightmapTex(rhi->newTexture(QRhiTexture::RGBA32F, pixelSize));
- if (!lightmapTex->create()) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to create FP32 texture for postprocessing"));
- return std::nullopt;
- }
- std::unique_ptr<QRhiTexture> dilatedLightmapTex(
- rhi->newTexture(QRhiTexture::RGBA32F, pixelSize, 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
- if (!dilatedLightmapTex->create()) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Warning,
- QStringLiteral("Failed to create FP32 dest. texture for postprocessing"));
- return std::nullopt;
- }
- QRhiTextureRenderTargetDescription rtDescDilate(dilatedLightmapTex.get());
- std::unique_ptr<QRhiTextureRenderTarget> rtDilate(rhi->newTextureRenderTarget(rtDescDilate));
- std::unique_ptr<QRhiRenderPassDescriptor> rpDescDilate(rtDilate->newCompatibleRenderPassDescriptor());
- rtDilate->setRenderPassDescriptor(rpDescDilate.get());
- if (!rtDilate->create()) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Warning,
- QStringLiteral("Failed to create postprocessing texture render target"));
- return std::nullopt;
- }
- QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
- QRhiTextureSubresourceUploadDescription lightmapTexUpload(image.constData(), image.size());
- resUpd->uploadTexture(lightmapTex.get(), QRhiTextureUploadDescription({ 0, 0, lightmapTexUpload }));
- QSSGRhiShaderResourceBindingList bindings;
- QRhiSampler *nearestSampler = rhiCtx->sampler(
- { QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::Repeat });
- bindings.addTexture(0, QRhiShaderResourceBinding::FragmentStage, lightmapTex.get(), nearestSampler);
- renderer->rhiQuadRenderer()->prepareQuad(rhiCtx, resUpd);
- const auto &shaderCache = renderer->contextInterface()->shaderCache();
- const auto &lmDilatePipeline = shaderCache->getBuiltInRhiShaders().getRhiLightmapDilateShader();
- if (!lmDilatePipeline) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Warning, QStringLiteral("Failed to load shaders"));
- return std::nullopt;
- }
- QSSGRhiGraphicsPipelineState dilatePs;
- dilatePs.viewport = viewport;
- QSSGRhiGraphicsPipelineStatePrivate::setShaderPipeline(dilatePs, lmDilatePipeline.get());
- renderer->rhiQuadRenderer()->recordRenderQuadPass(rhiCtx, &dilatePs, rhiCtxD->srb(bindings), rtDilate.get(), QSSGRhiQuadRenderer::UvCoords);
- resUpd = rhi->nextResourceUpdateBatch();
- QRhiReadbackResult dilateReadResult;
- resUpd->readBackTexture({ dilatedLightmapTex.get() }, &dilateReadResult);
- cb->resourceUpdate(resUpd);
-
- // Submit and wait for completion.
- rhi->finish();
-
- return dilateReadResult.data;
- };
-
- const int bakedLightingModelCount = bakedLightingModels.size();
- for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
- QElapsedTimer postProcessTimer;
- postProcessTimer.start();
-
- const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
- // only care about the ones that will store the lightmap image persistently
- if (!lm.model->hasLightmap())
- continue;
-
- Lightmap &lightmap(lightmaps[lmIdx]);
-
- // Charts mask
- QByteArray mask(lightmap.entries.size() * sizeof(quint32), Qt::Uninitialized);
- quint32 *maskUIntPtr = reinterpret_cast<quint32 *>(mask.data());
-
- // lightmap
- QByteArray indirectFP32(lightmap.entries.size() * 4 * sizeof(float), Qt::Uninitialized);
- float *indirectFloatPtr = reinterpret_cast<float *>(indirectFP32.data());
-
- // lightmap
- QByteArray directFP32(lightmap.entries.size() * 4 * sizeof(float), Qt::Uninitialized);
- float *directFloatPtr = reinterpret_cast<float *>(directFP32.data());
-
- // Assemble the images from the baker data structures
- for (const LightmapEntry &lmPix : std::as_const(lightmap.entries)) {
- if (lmPix.isValid()) {
- *indirectFloatPtr++ = lmPix.indirectLight.x();
- *indirectFloatPtr++ = lmPix.indirectLight.y();
- *indirectFloatPtr++ = lmPix.indirectLight.z();
- *indirectFloatPtr++ = 1.0f;
-
- *directFloatPtr++ = lmPix.directLight.x();
- *directFloatPtr++ = lmPix.directLight.y();
- *directFloatPtr++ = lmPix.directLight.z();
- *directFloatPtr++ = 1.0f;
-
- *maskUIntPtr++ = PIXEL_UNSET;
- } else {
- *indirectFloatPtr++ = 0.0f;
- *indirectFloatPtr++ = 0.0f;
- *indirectFloatPtr++ = 0.0f;
- *indirectFloatPtr++ = 0.0f;
-
- *directFloatPtr++ = 0.0f;
- *directFloatPtr++ = 0.0f;
- *directFloatPtr++ = 0.0f;
- *directFloatPtr++ = 0.0f;
-
- *maskUIntPtr++ = PIXEL_VOID;
- }
- }
-
- { // Fill mask
- const int rows = lightmap.pixelSize.height();
- const int cols = lightmap.pixelSize.width();
-
- // Use flood fill so each chart has its own "color" which
- // can then be used in the denoise shader to only take into account
- // pixels in the same chart.
- floodFill(reinterpret_cast<quint32 *>(mask.data()), rows, cols);
-
- lightmap.chartsMask = mask;
- }
-
- if (auto dilated = dilate(lightmap.pixelSize, indirectFP32); dilated.has_value()) {
- lightmap.indirectFP32 = dilated.value();
- } else {
- return false;
- }
-
- if (auto dilated = dilate(lightmap.pixelSize, directFP32); dilated.has_value()) {
- lightmap.directFP32 = dilated.value();
- } else {
- return false;
- }
-
- // Reduce UV seams by collecting all edges (going through all
- // triangles), looking for (fuzzy)matching ones, then drawing lines
- // with blending on top.
- const DrawInfo &drawInfo(drawInfos[lmIdx]);
- const char *vbase = drawInfo.vertexData.constData();
- const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
-
- // topology is Triangles, would be indexed draw - get rid of the index
- // buffer, need nothing but triangles afterwards
- qsizetype assembledVertexCount = 0;
- for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx])
- assembledVertexCount += subMeshInfo.count;
- QVector<QVector3D> smPos(assembledVertexCount);
- QVector<QVector3D> smNormal(assembledVertexCount);
- QVector<QVector2D> smCoord(assembledVertexCount);
- qsizetype vertexIdx = 0;
- for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
- for (quint32 i = 0; i < subMeshInfo.count; ++i) {
- const quint32 idx = *(ibase + subMeshInfo.offset + i);
- const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
- float x = *src++;
- float y = *src++;
- float z = *src++;
- smPos[vertexIdx] = QVector3D(x, y, z);
- src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
- x = *src++;
- y = *src++;
- z = *src++;
- smNormal[vertexIdx] = QVector3D(x, y, z);
- src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
- x = *src++;
- y = *src++;
- smCoord[vertexIdx] = QVector2D(x, y);
- ++vertexIdx;
- }
- }
-
- QHash<Edge, EdgeUV> edgeUVMap;
- QVector<SeamUV> seams;
- for (vertexIdx = 0; vertexIdx < assembledVertexCount; vertexIdx += 3) {
- QVector3D triVert[3] = { smPos[vertexIdx], smPos[vertexIdx + 1], smPos[vertexIdx + 2] };
- QVector3D triNorm[3] = { smNormal[vertexIdx], smNormal[vertexIdx + 1], smNormal[vertexIdx + 2] };
- QVector2D triUV[3] = { smCoord[vertexIdx], smCoord[vertexIdx + 1], smCoord[vertexIdx + 2] };
-
- for (int i = 0; i < 3; ++i) {
- int i0 = i;
- int i1 = (i + 1) % 3;
- if (vectorLessThan(triVert[i1], triVert[i0]))
- std::swap(i0, i1);
-
- const Edge e = {
- { triVert[i0], triVert[i1] },
- { triNorm[i0], triNorm[i1] }
- };
- const EdgeUV edgeUV = { { triUV[i0], triUV[i1] } };
- auto it = edgeUVMap.find(e);
- if (it == edgeUVMap.end()) {
- edgeUVMap.insert(e, edgeUV);
- } else if (!qFuzzyCompare(it->uv[0], edgeUV.uv[0]) || !qFuzzyCompare(it->uv[1], edgeUV.uv[1])) {
- if (!it->seam) {
- seams.append(SeamUV({ { edgeUV.uv, it->uv } }));
- it->seam = true;
- }
+ wgResult += sampleResult;
}
- }
+ return wgResult;
+ });
}
- qDebug() << "lm:" << seams.size() << "UV seams in" << lm.model;
- QByteArray workBuf(lightmap.indirectFP32.size(), Qt::Uninitialized);
- for (int blendIter = 0; blendIter < LM_SEAM_BLEND_ITER_COUNT; ++blendIter) {
- memcpy(workBuf.data(), lightmap.indirectFP32.constData(), lightmap.indirectFP32.size());
- for (int seamIdx = 0, end = seams.size(); seamIdx != end; ++seamIdx) {
- const SeamUV &seam(seams[seamIdx]);
- blendLine(seam.uv[0][0], seam.uv[0][1],
- seam.uv[1][0], seam.uv[1][1],
- reinterpret_cast<const float *>(workBuf.data()),
- reinterpret_cast<float *>(lightmap.indirectFP32.data()),
- lightmap.pixelSize);
- blendLine(seam.uv[1][0], seam.uv[1][1],
- seam.uv[0][0], seam.uv[0][1],
- reinterpret_cast<const float *>(workBuf.data()),
- reinterpret_cast<float *>(lightmap.indirectFP32.data()),
- lightmap.pixelSize);
- }
- }
+ QVector3D totalIndirect;
+ for (const auto &future : wg)
+ totalIndirect += future.result();
- reporter.report(((lmIdx + 1) / (double)bakedLightingModelCount));
+ result[i] += totalIndirect * options.indirectLightFactor / options.indirectLightSamples;
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
- QStringLiteral("Post-processing for model %1 done in %2")
- .arg(lm.model->lightmapKey)
- .arg(formatDuration(postProcessTimer.elapsed())));
+ if (bakingControl.cancelled)
+ return {};
}
- return true;
+
+ return result;
}
static bool isValidSavePath(const QString &path) {
@@ -2333,104 +2023,217 @@ static inline QString indexToMeshKey(int index)
return QStringLiteral("_mesh_%1").arg(index);
}
-bool QSSGLightmapperPrivate::storeLightmaps(const StageProgressReporter &reporter)
+bool QSSGLightmapperPrivate::storeMeshes(QSharedPointer<QSSGLightmapWriter> writer)
{
- const int bakedLightingModelCount = bakedLightingModels.size();
-
if (!isValidSavePath(options.source)) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Source path %1 is not a writable location").
- arg(options.source));
+ sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
+ QStringLiteral("Source path %1 is not a writable location")
+ .arg(options.source));
return false;
}
- QElapsedTimer totalWriteTimer;
- totalWriteTimer.start();
+ for (int i = 0; i < meshes.size(); ++i) {
+ if (!writer->writeData(indexToMeshKey(i), meshes[i]))
+ return false;
+ }
- const QString finalPath = QFileInfo(options.source).absoluteFilePath();
- const QString tmpPath = QFileInfo(options.source).absoluteFilePath() + QStringLiteral(".tmp");
- QSharedPointer<QSSGLightmapWriter> tmpFile = QSSGLightmapWriter::open(tmpPath);
- QSharedPointer<QSSGLightmapWriter> finalFile = QSSGLightmapWriter::open(finalPath);
+ return true;
+}
- if (!finalFile) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Failed to open final file"));
- return false;
- }
+bool QSSGLightmapperPrivate::storeMetadata(int lmIdx, QSharedPointer<QSSGLightmapWriter> writer)
+{
+ const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
+ const DrawInfo &drawInfo(drawInfos[lmIdx]);
- if (!tmpFile) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Failed to open tmp file"));
- return false;
- }
+ QVariantMap metadata;
+ metadata[QStringLiteral("width")] = drawInfos[lmIdx].lightmapSize.width();
+ metadata[QStringLiteral("height")] = drawInfos[lmIdx].lightmapSize.height();
+ metadata[QStringLiteral("mesh_key")] = indexToMeshKey(drawInfo.meshIndex);
- // Write meshes
- for (int i = 0; i < meshes.size(); ++i) {
- tmpFile->writeData(indexToMeshKey(i), meshes[i]);
- finalFile->writeData(indexToMeshKey(i), meshes[i]);
+ return writer->writeMetadata(lm.model->lightmapKey, metadata);
+}
+
+bool QSSGLightmapperPrivate::storeDirectLightData(int lmIdx, const QVector<QVector3D> &directLight, QSharedPointer<QSSGLightmapWriter> writer)
+{
+ const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
+ const int numTexels = modelTexels[lmIdx].size();
+
+ QByteArray directFP32(numTexels * 4 * sizeof(float), Qt::Uninitialized);
+ float *directFloatPtr = reinterpret_cast<float *>(directFP32.data());
+
+ for (int i = 0; i < numTexels; ++i) {
+ const auto &lmPix = modelTexels[lmIdx][i];
+ if (lmPix.isValid()) {
+ *directFloatPtr++ = directLight[i].x();
+ *directFloatPtr++ = directLight[i].y();
+ *directFloatPtr++ = directLight[i].z();
+ *directFloatPtr++ = 1.0f;
+ } else {
+ *directFloatPtr++ = 0.0f;
+ *directFloatPtr++ = 0.0f;
+ *directFloatPtr++ = 0.0f;
+ *directFloatPtr++ = 0.0f;
+ }
}
- for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
- const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
- // only care about the ones that want to store the lightmap image persistently
- if (!lm.model->hasLightmap())
- continue;
+ const QByteArray dilated = dilate(drawInfos[lmIdx].lightmapSize, directFP32);
- const Lightmap &lightmap(lightmaps[lmIdx]);
- const DrawInfo &drawInfo(drawInfos[lmIdx]);
-
- QVariantMap metadata;
- metadata[QStringLiteral("width")] = lightmap.pixelSize.width();
- metadata[QStringLiteral("height")] = lightmap.pixelSize.height();
- metadata[QStringLiteral("mesh_key")] = indexToMeshKey(drawInfo.meshIndex);
+ if (dilated.isEmpty())
+ return false;
- finalFile->writeMetadata(lm.model->lightmapKey, metadata);
- tmpFile->writeMetadata(lm.model->lightmapKey, metadata);
+ writer->writeF32Image(lm.model->lightmapKey + getLightmapKeySuffix(QSSGLightmapKeySuffix::Direct), dilated);
- tmpFile->writeF32Image(lm.model->lightmapKey + getLightmapKeySuffix(QSSGLightmapKeySuffix::Indirect), lightmap.indirectFP32);
- tmpFile->writeF32Image(lm.model->lightmapKey + getLightmapKeySuffix(QSSGLightmapKeySuffix::Direct), lightmap.directFP32);
- tmpFile->writeU32Image(lm.model->lightmapKey + getLightmapKeySuffix(QSSGLightmapKeySuffix::Mask), lightmap.chartsMask);
+ return true;
+}
- { // Add direct light
- const int numPixels = lightmap.pixelSize.width() * lightmap.pixelSize.height();
- std::array<float, 4> *imagePtr = reinterpret_cast<std::array<float, 4> *>(
- const_cast<char *>(lightmap.indirectFP32.data()));
- std::array<float, 4> *directPtr = reinterpret_cast<std::array<float, 4>*>(const_cast<char*>(lightmap.directFP32.data()));
- for (int i = 0; i < numPixels; ++i) {
- imagePtr[i][0] += directPtr[i][0];
- imagePtr[i][1] += directPtr[i][1];
- imagePtr[i][2] += directPtr[i][2];
- // skip alpha, always 0 or 1
- Q_ASSERT(imagePtr[i][3] == directPtr[i][3]);
- Q_ASSERT(imagePtr[i][3] == 1.f || imagePtr[i][3] == 0.f);
- }
+bool QSSGLightmapperPrivate::storeIndirectLightData(int lmIdx, const QVector<QVector3D> &indirectLight, QSharedPointer<QSSGLightmapWriter> writer)
+{
+ const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
+ const int numTexels = modelTexels[lmIdx].size();
+
+ QByteArray lightmapFP32(numTexels * 4 * sizeof(float), Qt::Uninitialized);
+ float *lightmapFloatPtr = reinterpret_cast<float *>(lightmapFP32.data());
+
+ for (int i = 0; i < numTexels; ++i) {
+ const auto &lmPix = modelTexels[lmIdx][i];
+ if (lmPix.isValid()) {
+ *lightmapFloatPtr++ = indirectLight[i].x();
+ *lightmapFloatPtr++ = indirectLight[i].y();
+ *lightmapFloatPtr++ = indirectLight[i].z();
+ *lightmapFloatPtr++ = 1.0f;
+ } else {
+ *lightmapFloatPtr++ = 0.0f;
+ *lightmapFloatPtr++ = 0.0f;
+ *lightmapFloatPtr++ = 0.0f;
+ *lightmapFloatPtr++ = 0.0f;
}
+ }
- finalFile->writeF32Image(lm.model->lightmapKey + getLightmapKeySuffix(QSSGLightmapKeySuffix::Final), lightmap.indirectFP32);
+ QByteArray dilated = dilate(drawInfos[lmIdx].lightmapSize, lightmapFP32);
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
- QStringLiteral("Lightmap saved for model %1").arg(lm.model->lightmapKey));
+ if (dilated.isEmpty())
+ return false;
- reporter.report(((lmIdx + 1) / (double)bakedLightingModelCount) * 0.8); // 80% of the work
+ // Reduce UV seams by collecting all edges (going through all
+ // triangles), looking for (fuzzy)matching ones, then drawing lines
+ // with blending on top.
+ const DrawInfo &drawInfo(drawInfos[lmIdx]);
+ const char *vbase = drawInfo.vertexData.constData();
+ const quint32 *ibase = reinterpret_cast<const quint32 *>(drawInfo.indexData.constData());
+
+ // topology is Triangles, would be indexed draw - get rid of the index
+ // buffer, need nothing but triangles afterwards
+ qsizetype assembledVertexCount = 0;
+ for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx])
+ assembledVertexCount += subMeshInfo.count;
+ QVector<QVector3D> smPos(assembledVertexCount);
+ QVector<QVector3D> smNormal(assembledVertexCount);
+ QVector<QVector2D> smCoord(assembledVertexCount);
+ qsizetype vertexIdx = 0;
+ for (SubMeshInfo &subMeshInfo : subMeshInfos[lmIdx]) {
+ for (quint32 i = 0; i < subMeshInfo.count; ++i) {
+ const quint32 idx = *(ibase + subMeshInfo.offset + i);
+ const float *src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.positionOffset);
+ float x = *src++;
+ float y = *src++;
+ float z = *src++;
+ smPos[vertexIdx] = QVector3D(x, y, z);
+ src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.normalOffset);
+ x = *src++;
+ y = *src++;
+ z = *src++;
+ smNormal[vertexIdx] = QVector3D(x, y, z);
+ src = reinterpret_cast<const float *>(vbase + idx * drawInfo.vertexStride + drawInfo.lightmapUVOffset);
+ x = *src++;
+ y = *src++;
+ smCoord[vertexIdx] = QVector2D(x, y);
+ ++vertexIdx;
+ }
+ }
+
+ QHash<Edge, EdgeUV> edgeUVMap;
+ QVector<SeamUV> seams;
+ for (vertexIdx = 0; vertexIdx < assembledVertexCount; vertexIdx += 3) {
+ QVector3D triVert[3] = { smPos[vertexIdx], smPos[vertexIdx + 1], smPos[vertexIdx + 2] };
+ QVector3D triNorm[3] = { smNormal[vertexIdx], smNormal[vertexIdx + 1], smNormal[vertexIdx + 2] };
+ QVector2D triUV[3] = { smCoord[vertexIdx], smCoord[vertexIdx + 1], smCoord[vertexIdx + 2] };
+
+ for (int i = 0; i < 3; ++i) {
+ int i0 = i;
+ int i1 = (i + 1) % 3;
+ if (vectorLessThan(triVert[i1], triVert[i0]))
+ std::swap(i0, i1);
+
+ const Edge e = {
+ { triVert[i0], triVert[i1] },
+ { triNorm[i0], triNorm[i1] }
+ };
+ const EdgeUV edgeUV = { { triUV[i0], triUV[i1] } };
+ auto it = edgeUVMap.find(e);
+ if (it == edgeUVMap.end()) {
+ edgeUVMap.insert(e, edgeUV);
+ } else if (!qFuzzyCompare(it->uv[0], edgeUV.uv[0]) || !qFuzzyCompare(it->uv[1], edgeUV.uv[1])) {
+ if (!it->seam) {
+ seams.append(SeamUV({ { edgeUV.uv, it->uv } }));
+ it->seam = true;
+ }
+ }
+ }
}
+ //qDebug() << "lm:" << seams.size() << "UV seams in" << lm.model;
- if (!finalFile->close()) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Error, QStringLiteral("Failed to save lightmaps to %1").
- arg(finalPath));
- return false;
+ QByteArray workBuf(dilated.size(), Qt::Uninitialized);
+ for (int blendIter = 0; blendIter < LM_SEAM_BLEND_ITER_COUNT; ++blendIter) {
+ memcpy(workBuf.data(), dilated.constData(), dilated.size());
+ for (int seamIdx = 0, end = seams.size(); seamIdx != end; ++seamIdx) {
+ const SeamUV &seam(seams[seamIdx]);
+ blendLine(seam.uv[0][0], seam.uv[0][1],
+ seam.uv[1][0], seam.uv[1][1],
+ reinterpret_cast<const float *>(workBuf.data()),
+ reinterpret_cast<float *>(dilated.data()),
+ drawInfos[lmIdx].lightmapSize);
+ blendLine(seam.uv[1][0], seam.uv[1][1],
+ seam.uv[0][0], seam.uv[0][1],
+ reinterpret_cast<const float *>(workBuf.data()),
+ reinterpret_cast<float *>(dilated.data()),
+ drawInfos[lmIdx].lightmapSize);
+ }
}
- if (!tmpFile->close()) {
- sendOutputInfo(QSSGLightmapper::BakingStatus::Error, QStringLiteral("Failed to save lightmaps to %1").
- arg(tmpPath));
- return false;
+ writer->writeF32Image(lm.model->lightmapKey + getLightmapKeySuffix(QSSGLightmapKeySuffix::Indirect), dilated);
+
+ return true;
+}
+
+bool QSSGLightmapperPrivate::storeMaskImage(int lmIdx, QSharedPointer<QSSGLightmapWriter> writer)
+{
+ constexpr quint32 PIXEL_VOID = 0;
+ constexpr quint32 PIXEL_UNSET = -1;
+
+ const QSSGBakedLightingModel &lm(bakedLightingModels[lmIdx]);
+ const int numTexels = modelTexels[lmIdx].size();
+
+ QByteArray mask(numTexels * sizeof(quint32), Qt::Uninitialized);
+ quint32 *maskUIntPtr = reinterpret_cast<quint32 *>(mask.data());
+
+ for (int i = 0; i < numTexels; ++i) {
+ *maskUIntPtr++ = modelTexels[lmIdx][i].isValid() ? PIXEL_UNSET : PIXEL_VOID;
}
- sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Lightmap saved to %1 in %2").
- arg(finalPath).
- arg(formatDuration(totalWriteTimer.elapsed())));
- reporter.report(1);
+ const int rows = drawInfos[lmIdx].lightmapSize.height();
+ const int cols = drawInfos[lmIdx].lightmapSize.width();
+
+ // Use flood fill so each chart has its own "color" which
+ // can then be used in the denoise shader to only take into account
+ // pixels in the same chart.
+ floodFill(reinterpret_cast<quint32 *>(mask.data()), rows, cols);
+
+ writer->writeF32Image(lm.model->lightmapKey + getLightmapKeySuffix(QSSGLightmapKeySuffix::Mask), mask);
+
return true;
}
-bool QSSGLightmapperPrivate::denoiseLightmaps(const StageProgressReporter &reporter)
+bool QSSGLightmapperPrivate::denoiseLightmaps()
{
QElapsedTimer denoiseTimer;
denoiseTimer.start();
@@ -2461,7 +2264,6 @@ bool QSSGLightmapperPrivate::denoiseLightmaps(const StageProgressReporter &repor
}
}
- Q_UNUSED(reporter);
QRhi *rhi = rhiCtx->rhi();
Q_ASSERT(rhi);
if (!rhi->isFeatureSupported(QRhi::Compute)) {
@@ -2664,6 +2466,16 @@ bool QSSGLightmapperPrivate::denoiseLightmaps(const StageProgressReporter &repor
}
return true;
+
+}
+
+bool QSSGLightmapperPrivate::userCancelled()
+{
+ if (bakingControl.cancelled) {
+ sendOutputInfo(QSSGLightmapper::BakingStatus::Cancelled,
+ QStringLiteral("Cancelled by user"));
+ }
+ return bakingControl.cancelled;
}
void QSSGLightmapperPrivate::sendOutputInfo(QSSGLightmapper::BakingStatus type, std::optional<QString> msg, bool outputToConsole, bool outputConsoleTimeRemanining)
@@ -2714,7 +2526,7 @@ void QSSGLightmapperPrivate::sendOutputInfo(QSSGLightmapper::BakingStatus type,
payload[QStringLiteral("status")] = (int)type;
payload[QStringLiteral("message")] = msg.value_or(QString());
payload[QStringLiteral("totalTimeRemaining")] = estimatedTimeRemaining;
- payload[QStringLiteral("totalProgress")] = totalProgressPercent / 100.0;
+ payload[QStringLiteral("totalProgress")] = totalProgress;
payload[QStringLiteral("totalTimeElapsed")] = totalTimer.elapsed();
outputCallback(payload, &bakingControl);
}
@@ -2732,6 +2544,8 @@ bool QSSGLightmapper::bake()
return false;
}
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Source path: %1").arg(d->options.source));
+
d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Total models registered: %1").arg(d->bakedLightingModels.size()));
if (d->bakedLightingModels.isEmpty()) {
@@ -2739,77 +2553,256 @@ bool QSSGLightmapper::bake()
return false;
}
- {
- auto reporter = d->createReporter(QSSGLightmapperPrivate::Stage::CommitGeometry);
- if (!d->commitGeometry(reporter)) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Baking failed"));
+ // ------------- Commit geometry -------------
+
+ if (!d->commitGeometry()) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Baking failed"));
+ return false;
+ }
+
+ if (d->userCancelled())
+ return false;
+
+ // ------------- Prepare lightmaps -------------
+
+ if (!d->prepareLightmaps()) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Baking failed"));
+ return false;
+ }
+
+ if (d->userCancelled())
+ return false;
+
+ // indirect lighting is slow, so parallelize per groups of samples,
+ // e.g. if sample count is 256 and workgroup size is 32, then do up to
+ // 8 sets in parallel, each calculating 32 samples (how many of the 8
+ // are really done concurrently that's up to the thread pool to manage)
+ const int wgSizePerGroup = qMax(1, d->options.indirectLightWorkgroupSize);
+ const int wgCount = (d->options.indirectLightSamples / wgSizePerGroup) + (d->options.indirectLightSamples % wgSizePerGroup ? 1: 0);
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Sample count: %1, Workgroup size: %2, Max bounces: %3, Multiplier: %4").
+ arg(d->options.indirectLightSamples).
+ arg(wgSizePerGroup).
+ arg(d->options.indirectLightBounces).
+ arg(d->options.indirectLightFactor));
+
+ const int bakedLightingModelCount = d->bakedLightingModels.size();
+
+ // We use a work-file where we store the baked lightmaps accumulatively and when
+ // the baking process is finished successfully, replace the .tmp file with it.
+ // Put the work-file next to the destination file so we can just do a rename/move
+ // of the file with hopefully no potential inter-filesystem issues.
+ QSharedPointer<QFile> workFile = QSharedPointer<QFile>(new QFile(QFileInfo(d->options.source).absoluteDir().path() + "/qt_lightmapper_work_file_"
+ + QString::number(QCoreApplication::applicationPid())));
+ bool deleteWorkFile = true;
+ auto cleanupWorkFile = qScopeGuard([workFile, &deleteWorkFile] {
+ if (deleteWorkFile)
+ workFile->remove();
+ });
+
+ QElapsedTimer timer;
+ timer.start();
+
+ // ------------- Store metadata -------------
+
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Storing metadata..."));
+ auto writer = QSSGLightmapWriter::open(workFile);
+ for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
+ if (d->userCancelled())
+ return false;
+ QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
+ if (!lm.model->hasLightmap())
+ continue;
+
+ if (!d->storeMetadata(lmIdx, writer)) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
+ QStringLiteral("[%1/%2] Failed to store metadata for '%3'")
+ .arg(lmIdx + 1)
+ .arg(bakedLightingModelCount)
+ .arg(lm.model->lightmapKey));
return false;
}
}
- {
- auto reporter = d->createReporter(QSSGLightmapperPrivate::Stage::PrepareLightmaps);
- if (!d->prepareLightmaps(reporter)) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Baking failed"));
+ // ------------- Store mask -------------
+
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Storing mask images..."));
+ for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
+ if (d->userCancelled())
+ return false;
+ QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
+ if (!lm.model->hasLightmap())
+ continue;
+
+ if (!d->storeMaskImage(lmIdx, writer)) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
+ QStringLiteral("[%1/%2] Failed to store mask for '%3'")
+ .arg(lmIdx + 1)
+ .arg(bakedLightingModelCount)
+ .arg(lm.model->lightmapKey));
return false;
}
}
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
+ QStringLiteral("Took %1").arg(formatDuration(timer.restart())));
-
- if (d->bakingControl.cancelled) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
+ if (d->userCancelled())
return false;
- }
- {
- auto reporter = d->createReporter(QSSGLightmapperPrivate::Stage::ComputeDirectLight);
- d->computeDirectLight(reporter);
+ // ------------- Direct compute / store -------------
+
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Computing direct light..."));
+ for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
+ if (d->userCancelled())
+ return false;
+ QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
+ if (!lm.model->hasLightmap())
+ continue;
+
+ timer.restart();
+ const QVector<QVector3D> directLight = d->computeDirectLight(lmIdx);
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
+ QStringLiteral("[%1/%2] '%3' took %4")
+ .arg(lmIdx + 1)
+ .arg(bakedLightingModelCount)
+ .arg(lm.model->lightmapKey)
+ .arg(formatDuration(timer.elapsed())));
+
+ if (directLight.empty()) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
+ QStringLiteral("[%1/%2] Failed to compute for '%3'")
+ .arg(lmIdx + 1)
+ .arg(bakedLightingModelCount)
+ .arg(lm.model->lightmapKey));
+ return false;
+ }
+
+ if (!d->storeDirectLightData(lmIdx, directLight, writer)) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
+ QStringLiteral("[%1/%2] Failed to store data for '%3'")
+ .arg(lmIdx + 1)
+ .arg(bakedLightingModelCount)
+ .arg(lm.model->lightmapKey));
+ return false;
+ }
}
- if (d->bakingControl.cancelled) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
+ if (d->userCancelled())
return false;
- }
+
+ // ------------- Indirect compute / store -------------
if (d->options.indirectLightEnabled) {
- auto reporter = d->createReporter(QSSGLightmapperPrivate::Stage::ComputeIndirectLight);
- d->computeIndirectLight(reporter);
+ d->totalIncrementsToBeMade = std::accumulate(d->numValidTexels.begin(), d->numValidTexels.end(), 0);
+ QElapsedTimer indirectTimer;
+ constexpr int timerIntervalMs = 100;
+ TimerThread timerThread;
+ timerThread.setInterval(timerIntervalMs);
+ // Log ETA every 5 seconds to console
+ constexpr int consoleOutputInterval = 5000 / timerIntervalMs;
+ int timeoutsSinceOutput = consoleOutputInterval - 1;
+ timerThread.setCallback([&]() {
+ d->totalProgress = static_cast<double>(d->incrementsDone) / d->totalIncrementsToBeMade;
+ double totalElapsed = indirectTimer.elapsed();
+ if (totalElapsed < 500) {
+ d->estimatedTimeRemaining = -1;
+ } else {
+ double avgTimePerTexel = static_cast<double>(totalElapsed) / d->incrementsDone;
+ d->estimatedTimeRemaining = avgTimePerTexel * (d->totalIncrementsToBeMade - d->incrementsDone);
+ }
+ bool outputToConsole = timeoutsSinceOutput == consoleOutputInterval - 1;
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, std::nullopt, outputToConsole, outputToConsole);
+ timeoutsSinceOutput = (timeoutsSinceOutput + 1) % consoleOutputInterval;
+ });
+ timerThread.start();
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
+ QStringLiteral("Computing indirect light..."));
+ indirectTimer.start();
+ for (int lmIdx = 0; lmIdx < bakedLightingModelCount; ++lmIdx) {
+ if (d->userCancelled())
+ return false;
+ QSSGBakedLightingModel &lm = d->bakedLightingModels[lmIdx];
+ if (!lm.model->hasLightmap())
+ continue;
+
+ timer.restart();
+ const QVector<QVector3D> indirectLight = d->computeIndirectLight(lmIdx, wgCount, wgSizePerGroup);
+ if (indirectLight.empty()) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
+ QStringLiteral("[%1/%2] Failed to compute '%3'")
+ .arg(lmIdx + 1)
+ .arg(bakedLightingModelCount)
+ .arg(lm.model->lightmapKey));
+ return false;
+ }
+
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
+ QStringLiteral("[%1/%2] '%3' took %4")
+ .arg(lmIdx + 1)
+ .arg(bakedLightingModelCount)
+ .arg(lm.model->lightmapKey)
+ .arg(formatDuration(timer.elapsed())));
+
+ if (d->userCancelled())
+ return false;
+
+ if (!d->storeIndirectLightData(lmIdx, indirectLight, writer)) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed,
+ QStringLiteral("[%1/%2] Failed to store data for '%3'")
+ .arg(lmIdx + 1)
+ .arg(bakedLightingModelCount)
+ .arg(lm.model->lightmapKey));
+ return false;
+ }
+ }
}
- if (d->bakingControl.cancelled) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
+ // ------------- Store meshes -------------
+
+ if (!d->storeMeshes(writer)) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Failed to store meshes"));
return false;
}
- {
- auto reporter = d->createReporter(QSSGLightmapperPrivate::Stage::PostProcess);
- if (!d->postProcess(reporter)) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Baking failed"));
- return false;
- }
- }
+ if (d->userCancelled())
+ return false;
- if (d->bakingControl.cancelled) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Cancelled, QStringLiteral("Cancelled by user"));
+ // ------------- Copy file from tmp -------------
+
+ if (!writer->close()) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Error,
+ QStringLiteral("Failed to save temp file to %1").arg(workFile->fileName()));
return false;
}
- {
- auto reporter = d->createReporter(QSSGLightmapperPrivate::Stage::StoreLightmaps);
- if (!d->storeLightmaps(reporter)) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Baking failed"));
- return false;
- }
+ const QString tmpPath = QFileInfo(d->options.source).absoluteFilePath() + ".tmp";
+ QFile::remove(tmpPath);
+ if (workFile->rename(tmpPath)) {
+ deleteWorkFile = false;
+ } else {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Error,
+ QStringLiteral("Failed to copy temp file to %1").arg(tmpPath));
+ return false;
}
- {
- auto reporter = d->createReporter(QSSGLightmapperPrivate::Stage::DenoiseLightmaps);
- if (!d->denoiseLightmaps(reporter)) {
- d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Denoising failed"));
- return false;
- }
+ if (d->userCancelled())
+ return false;
+
+ // ------------- Denoising -------------
+
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Denoising..."));
+ timer.restart();
+ if (!d->denoiseLightmaps()) {
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Denoising failed"));
+ return false;
}
+ d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Took %1").arg(formatDuration(timer.elapsed())));
+
+ if (d->userCancelled())
+ return false;
+
+ // -------------------------------------
+ d->totalProgress = 1.0;
d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info,
QStringLiteral("Baking took %1").arg(formatDuration(d->totalTimer.elapsed())));
d->sendOutputInfo(QSSGLightmapper::BakingStatus::Complete, std::nullopt);
@@ -2822,8 +2815,7 @@ bool QSSGLightmapper::denoise() {
d->sendOutputInfo(QSSGLightmapper::BakingStatus::Info, QStringLiteral("Denoise starting..."));
- auto reporter = d->createReporter(QSSGLightmapperPrivate::Stage::DenoiseLightmaps);
- if (!d->denoiseLightmaps(reporter)) {
+ if (!d->denoiseLightmaps()) {
d->sendOutputInfo(QSSGLightmapper::BakingStatus::Failed, QStringLiteral("Denoising failed"));
return false;
}