Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include "qssgiblbaker_p.h"
++ #include <QFile>
++ #include <QFileInfo>
++ #include <QScopeGuard>
++
++ #include <QtQuick3DRuntimeRender/private/qssgrhicontext_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderloadedtexture_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendershadercache_p.h>
++
++ #if QT_CONFIG(opengl)
++ #include <QOffscreenSurface>
++ #include <QOpenGLContext>
++ #endif
++
++ QT_BEGIN_NAMESPACE
++
++ #define GL_FLOAT 0x1406
++ #define GL_HALF_FLOAT 0x140B
++ #define GL_UNSIGNED_BYTE 0x1401
++ #define GL_RGBA 0x1908
++ #define GL_RGBA8 0x8058
++ #define GL_RGBA16F 0x881A
++ #define GL_RGBA32F 0x8814
++
++ static constexpr QSSGRenderTextureFormat FORMAT(QSSGRenderTextureFormat::RGBA16F);
++
++ const QStringList QSSGIblBaker::inputExtensions() const
++ {
++ return { QStringLiteral("hdr"), QStringLiteral("exr")};
++ }
++
++ const QString QSSGIblBaker::outputExtension() const
++ {
++ return QStringLiteral(".ktx");
++ }
++
++ namespace {
++ void writeUInt32(QIODevice &device, quint32 value)
++ {
++ device.write(reinterpret_cast<char *>(&value), sizeof(qint32));
++ }
++
++ void appendBinaryVector(QVector<char> &dest, const quint32 src)
++ {
++ qsizetype oldsize = dest.size();
++ dest.resize(dest.size() + sizeof(src));
++ memcpy(dest.data() + oldsize, &src, sizeof(src));
++ }
++
++ void appendBinaryVector(QVector<char> &dest, const std::string &src)
++ {
++ qsizetype oldsize = dest.size();
++ dest.resize(dest.size() + src.size() + 1);
++ memcpy(dest.data() + oldsize, src.c_str(), src.size() + 1);
++ }
++ }
++
++ // Vertex data for rendering environment cube map
++ static const float cube[] = {
++ -1.0f, -1.0f, -1.0f, // -X side
++ -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f,
++
++ -1.0f, -1.0f, -1.0f, // -Z side
++ 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f,
++
++ -1.0f, -1.0f, -1.0f, // -Y side
++ 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f,
++
++ -1.0f, 1.0f, -1.0f, // +Y side
++ -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f,
++
++ 1.0f, 1.0f, -1.0f, // +X side
++ 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f,
++
++ -1.0f, 1.0f, 1.0f, // +Z side
++ -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
++
++ 0.0f, 1.0f, // -X side
++ 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
++
++ 1.0f, 1.0f, // -Z side
++ 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
++
++ 1.0f, 0.0f, // -Y side
++ 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
++
++ 1.0f, 0.0f, // +Y side
++ 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
++
++ 1.0f, 0.0f, // +X side
++ 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,
++
++ 0.0f, 0.0f, // +Z side
++ 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,
++ };
++
++ QString renderToKTXFileInternal(const char *name, const QString &inPath, const QString &outPath, QRhi::Implementation impl, QRhiInitParams *initParams)
++ {
++ qDebug() << "Using RHI backend" << name;
++
++ // Open output file
++ QFile ktxOutputFile(outPath);
++ if (!ktxOutputFile.open(QIODevice::WriteOnly)) {
++ return QStringLiteral("Could not open file: %1").arg(outPath);
++ }
++
++ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
++ if (!rhi)
++ return QStringLiteral("Failed to initialize QRhi");
++
++ qDebug() << rhi->driverInfo();
++
++ QRhiCommandBuffer *cb;
++ rhi->beginOffscreenFrame(&cb);
++
++ const auto rhiContext = std::make_unique<QSSGRhiContext>(rhi.get());
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiContext.get());
++ rhiCtxD->setCommandBuffer(cb);
++
++ QScopedPointer<QSSGLoadedTexture> inImage(QSSGLoadedTexture::loadHdrImage(QSSGInputUtil::getStreamForFile(inPath), FORMAT));
++ if (!inImage)
++ return QStringLiteral("Failed to load hdr file");
++
++ auto shaderCache = std::make_unique<QSSGShaderCache>(*rhiContext);
++
++ // The objective of this method is to take the equirectangular texture
++ // provided by inImage and create a cubeMap that contains both pre-filtered
++ // specular environment maps, as well as a irradiance map for diffuse
++ // operations.
++ // To achieve this though we first convert convert the Equirectangular texture
++ // to a cubeMap with genereated mip map levels (no filtering) to make the
++ // process of creating the prefiltered and irradiance maps eaiser. This
++ // intermediate texture as well as the original equirectangular texture are
++ // destroyed after this frame completes, and all further associations with
++ // the source lightProbe texture are instead associated with the final
++ // generated environment map.
++ // The intermediate environment cubemap is used to generate the final
++ // cubemap. This cubemap will generate 6 mip levels for each face
++ // (the remaining faces are unused). This is what the contents of each
++ // face mip level looks like:
++ // 0: Pre-filtered with roughness 0 (basically unfiltered)
++ // 1: Pre-filtered with roughness 0.25
++ // 2: Pre-filtered with roughness 0.5
++ // 3: Pre-filtered with roughness 0.75
++ // 4: Pre-filtered with rougnness 1.0
++ // 5: Irradiance map (ideally at least 16x16)
++ // It would be better if we could use a separate cubemap for irradiance, but
++ // right now there is a 1:1 association between texture sources on the front-
++ // end and backend.
++
++ // Right now minimum face size needs to be 512x512 to be able to have 6 reasonably sized mips
++ const int suggestedSize = qMax(512.f, inImage->height * 0.5f);
++ const QSize environmentMapSize(suggestedSize, suggestedSize);
++ const bool isRGBE = inImage->format.format == QSSGRenderTextureFormat::Format::RGBE8;
++ const int colorSpace = inImage->isSRGB ? 1 : 0; // 0 Linear | 1 sRGB
++
++ // Phase 1: Convert the Equirectangular texture to a Cubemap
++ QRhiTexture *envCubeMap = rhi->newTexture(QRhiTexture::RGBA16F,
++ environmentMapSize,
++ 1,
++ QRhiTexture::RenderTarget | QRhiTexture::CubeMap | QRhiTexture::MipMapped
++ | QRhiTexture::UsedWithGenerateMips);
++ if (!envCubeMap->create()) {
++ return QStringLiteral("Failed to create Environment Cube Map");
++ }
++ envCubeMap->deleteLater();
++
++ // Create a renderbuffer the size of a the cubeMap face
++ QRhiRenderBuffer *envMapRenderBuffer = rhi->newRenderBuffer(QRhiRenderBuffer::Color, environmentMapSize);
++ if (!envMapRenderBuffer->create()) {
++ return QStringLiteral("Failed to create Environment Map Render Buffer");
++ }
++ envMapRenderBuffer->deleteLater();
++
++ // Setup the 6 render targets for each cube face
++ QVarLengthArray<QRhiTextureRenderTarget *, 6> renderTargets;
++ QRhiRenderPassDescriptor *renderPassDesc = nullptr;
++ for (int face = 0; face < 6; ++face) {
++ QRhiColorAttachment att(envCubeMap);
++ att.setLayer(face);
++ QRhiTextureRenderTargetDescription rtDesc;
++ rtDesc.setColorAttachments({ att });
++ auto renderTarget = rhi->newTextureRenderTarget(rtDesc);
++ renderTarget->setDescription(rtDesc);
++ if (!renderPassDesc)
++ renderPassDesc = renderTarget->newCompatibleRenderPassDescriptor();
++ renderTarget->setRenderPassDescriptor(renderPassDesc);
++ if (!renderTarget->create()) {
++ return QStringLiteral("Failed to build env map render target");
++ }
++ renderTarget->deleteLater();
++ renderTargets << renderTarget;
++ }
++ renderPassDesc->deleteLater();
++
++ // Setup the sampler for reading the equirectangular loaded texture
++ QSize size(inImage->width, inImage->height);
++ auto *sourceTexture = rhi->newTexture(QRhiTexture::RGBA16F, size, 1);
++ if (!sourceTexture->create()) {
++ return QStringLiteral("Failed to create source env map texture");
++ }
++ sourceTexture->deleteLater();
++
++ // Upload the equirectangular texture
++ QRhiTextureUploadDescription desc;
++ if (inImage->textureFileData.isValid()) {
++ desc = { { 0,
++ 0,
++ { inImage->textureFileData.data().constData() + inImage->textureFileData.dataOffset(0),
++ quint32(inImage->textureFileData.dataLength(0)) } } };
++ } else {
++ desc = { { 0, 0, { inImage->data, inImage->dataSizeInBytes } } };
++ }
++ auto *rub = rhi->nextResourceUpdateBatch();
++ rub->uploadTexture(sourceTexture, desc);
++
++ const QSSGRhiSamplerDescription samplerDesc {
++ QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::Repeat
++ };
++ QRhiSampler *sampler = rhiContext->sampler(samplerDesc);
++
++ // Load shader and setup render pipeline
++ const auto &envMapShaderStages = shaderCache->getBuiltInRhiShaders().getRhiEnvironmentmapShader();
++
++ // Vertex Buffer - Just a single cube that will be viewed from inside
++ QRhiBuffer *vertexBuffer = rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
++ vertexBuffer->create();
++ vertexBuffer->deleteLater();
++ rub->uploadStaticBuffer(vertexBuffer, cube);
++
++ // Uniform Buffer - 2x mat4
++ int ubufElementSize = rhi->ubufAligned(128);
++ QRhiBuffer *uBuf = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, ubufElementSize * 6);
++ uBuf->create();
++ uBuf->deleteLater();
++
++ int ubufEnvMapElementSize = rhi->ubufAligned(4);
++ QRhiBuffer *uBufEnvMap = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, ubufEnvMapElementSize * 6);
++ uBufEnvMap->create();
++ uBufEnvMap->deleteLater();
++
++ // Shader Resource Bindings
++ QRhiShaderResourceBindings *envMapSrb = rhi->newShaderResourceBindings();
++ envMapSrb->setBindings(
++ { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, uBuf, 128),
++ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, uBufEnvMap, ubufEnvMapElementSize),
++ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, sourceTexture, sampler) });
++ envMapSrb->create();
++ envMapSrb->deleteLater();
++
++ // Pipeline
++ QRhiGraphicsPipeline *envMapPipeline = rhi->newGraphicsPipeline();
++ envMapPipeline->setCullMode(QRhiGraphicsPipeline::Front);
++ envMapPipeline->setFrontFace(QRhiGraphicsPipeline::CCW);
++ envMapPipeline->setShaderStages({ *envMapShaderStages->vertexStage(), *envMapShaderStages->fragmentStage() });
++
++ QRhiVertexInputLayout inputLayout;
++ inputLayout.setBindings({ { 3 * sizeof(float) } });
++ inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 } });
++
++ envMapPipeline->setVertexInputLayout(inputLayout);
++ envMapPipeline->setShaderResourceBindings(envMapSrb);
++ envMapPipeline->setRenderPassDescriptor(renderPassDesc);
++ if (!envMapPipeline->create()) {
++ return QStringLiteral("Failed to create source env map pipeline state");
++ }
++ envMapPipeline->deleteLater();
++
++ // Do the actual render passes
++ cb->debugMarkBegin("Environment Cubemap Generation");
++ const QRhiCommandBuffer::VertexInput vbufBinding(vertexBuffer, 0);
++
++ // Set the Uniform Data
++ QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
++ mvp.perspective(90.0f, 1.0f, 0.1f, 10.0f);
++
++ auto lookAt = [](const QVector3D &eye, const QVector3D ¢er, const QVector3D &up) {
++ QMatrix4x4 viewMatrix;
++ viewMatrix.lookAt(eye, center, up);
++ return viewMatrix;
++ };
++ QVarLengthArray<QMatrix4x4, 6> views;
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f)));
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(-1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f)));
++ if (rhi->isYUpInFramebuffer()) {
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f)));
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f)));
++ } else {
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f)));
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f)));
++ }
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, 1.0), QVector3D(0.0f, -1.0f, 0.0f)));
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, -1.0), QVector3D(0.0f, -1.0f, 0.0f)));
++ for (int face = 0; face < 6; ++face) {
++ rub->updateDynamicBuffer(uBuf, face * ubufElementSize, 64, mvp.constData());
++ rub->updateDynamicBuffer(uBuf, face * ubufElementSize + 64, 64, views[face].constData());
++ rub->updateDynamicBuffer(uBufEnvMap, face * ubufEnvMapElementSize, 4, &colorSpace);
++ }
++ cb->resourceUpdate(rub);
++
++ for (int face = 0; face < 6; ++face) {
++ cb->beginPass(renderTargets[face], QColor(0, 0, 0, 1), { 1.0f, 0 }, nullptr, rhiContext->commonPassFlags());
++
++ // Execute render pass
++ cb->setGraphicsPipeline(envMapPipeline);
++ cb->setVertexInput(0, 1, &vbufBinding);
++ cb->setViewport(QRhiViewport(0, 0, environmentMapSize.width(), environmentMapSize.height()));
++ QVector<QPair<int, quint32>> dynamicOffset = {
++ { 0, quint32(ubufElementSize * face) },
++ { 2, quint32(ubufEnvMapElementSize * face )}
++ };
++ cb->setShaderResources(envMapSrb, 2, dynamicOffset.constData());
++
++ cb->draw(36);
++ cb->endPass();
++ }
++ cb->debugMarkEnd();
++
++ if (!isRGBE) {
++ // Generate mipmaps for envMap
++ rub = rhi->nextResourceUpdateBatch();
++ rub->generateMips(envCubeMap);
++ cb->resourceUpdate(rub);
++ }
++
++ // Phase 2: Generate the pre-filtered environment cubemap
++ cb->debugMarkBegin("Pre-filtered Environment Cubemap Generation");
++ QRhiTexture *preFilteredEnvCubeMap = rhi->newTexture(QRhiTexture::RGBA16F,
++ environmentMapSize,
++ 1,
++ QRhiTexture::RenderTarget | QRhiTexture::CubeMap | QRhiTexture::MipMapped);
++ if (!preFilteredEnvCubeMap->create())
++ qWarning("Failed to create Pre-filtered Environment Cube Map");
++ int mipmapCount = rhi->mipLevelsForSize(environmentMapSize);
++ mipmapCount = qMin(mipmapCount, 6); // don't create more than 6 mip levels
++ QMap<int, QSize> mipLevelSizes;
++ QMap<int, QVarLengthArray<QRhiTextureRenderTarget *, 6>> renderTargetsMap;
++ QRhiRenderPassDescriptor *renderPassDescriptorPhase2 = nullptr;
++
++ // Create a renderbuffer for each mip level
++ for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) {
++ const QSize levelSize = QSize(environmentMapSize.width() * std::pow(0.5, mipLevel),
++ environmentMapSize.height() * std::pow(0.5, mipLevel));
++ mipLevelSizes.insert(mipLevel, levelSize);
++ // Setup Render targets (6 * mipmapCount)
++ QVarLengthArray<QRhiTextureRenderTarget *, 6> renderTargets;
++ for (int face = 0; face < 6; ++face) {
++ QRhiColorAttachment att(preFilteredEnvCubeMap);
++ att.setLayer(face);
++ att.setLevel(mipLevel);
++ QRhiTextureRenderTargetDescription rtDesc;
++ rtDesc.setColorAttachments({ att });
++ auto renderTarget = rhi->newTextureRenderTarget(rtDesc);
++ renderTarget->setDescription(rtDesc);
++ if (!renderPassDescriptorPhase2)
++ renderPassDescriptorPhase2 = renderTarget->newCompatibleRenderPassDescriptor();
++ renderTarget->setRenderPassDescriptor(renderPassDescriptorPhase2);
++ if (!renderTarget->create())
++ qWarning("Failed to build prefilter env map render target");
++ renderTarget->deleteLater();
++ renderTargets << renderTarget;
++ }
++ renderTargetsMap.insert(mipLevel, renderTargets);
++ renderPassDescriptorPhase2->deleteLater();
++ }
++
++ // Load the prefilter shader stages
++ const auto &prefilterShaderStages = shaderCache->getBuiltInRhiShaders().getRhienvironmentmapPreFilterShader(isRGBE);
++
++ // Create a new Sampler
++ const QSSGRhiSamplerDescription samplerMipMapDesc {
++ QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge, QRhiSampler::Repeat
++ };
++
++ QRhiSampler *envMapCubeSampler = nullptr;
++ // Only use mipmap interpoliation if not using RGBE
++ if (!isRGBE)
++ envMapCubeSampler = rhiContext->sampler(samplerMipMapDesc);
++ else
++ envMapCubeSampler = sampler;
++
++ // Reuse Vertex Buffer from phase 1
++ // Reuse UniformBuffer from phase 1 (for vertex shader)
++
++ // UniformBuffer
++ // float roughness;
++ // float resolution;
++ // float lodBias;
++ // int sampleCount;
++ // int distribution;
++
++ int ubufPrefilterElementSize = rhi->ubufAligned(20);
++ QRhiBuffer *uBufPrefilter = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, ubufPrefilterElementSize * mipmapCount);
++ uBufPrefilter->create();
++ uBufPrefilter->deleteLater();
++
++ // Shader Resource Bindings
++ QRhiShaderResourceBindings *preFilterSrb = rhi->newShaderResourceBindings();
++ preFilterSrb->setBindings({
++ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, uBuf, 128),
++ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, uBufPrefilter, 20),
++ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, envCubeMap, envMapCubeSampler)
++ });
++ preFilterSrb->create();
++ preFilterSrb->deleteLater();
++
++ // Pipeline
++ QRhiGraphicsPipeline *prefilterPipeline = rhi->newGraphicsPipeline();
++ prefilterPipeline->setCullMode(QRhiGraphicsPipeline::Front);
++ prefilterPipeline->setFrontFace(QRhiGraphicsPipeline::CCW);
++ prefilterPipeline->setDepthOp(QRhiGraphicsPipeline::LessOrEqual);
++ prefilterPipeline->setShaderStages({
++ *prefilterShaderStages->vertexStage(),
++ *prefilterShaderStages->fragmentStage()
++ });
++ // same as phase 1
++ prefilterPipeline->setVertexInputLayout(inputLayout);
++ prefilterPipeline->setShaderResourceBindings(preFilterSrb);
++ prefilterPipeline->setRenderPassDescriptor(renderPassDescriptorPhase2);
++ if (!prefilterPipeline->create())
++ return QStringLiteral("Failed to create pre-filter env map pipeline state");
++ prefilterPipeline->deleteLater();
++
++ // Uniform Data
++ // set the roughness uniform buffer data
++ rub = rhi->nextResourceUpdateBatch();
++ const float resolution = environmentMapSize.width();
++ const float lodBias = 0.0f;
++ const int sampleCount = 1024;
++ for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) {
++ Q_ASSERT(mipmapCount - 2);
++ const float roughness = float(mipLevel) / float(mipmapCount - 2);
++ const int distribution = mipLevel == (mipmapCount - 1) ? 0 : 1; // last mip level is for irradiance
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize, 4, &roughness);
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize + 4, 4, &resolution);
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize + 4 + 4, 4, &lodBias);
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize + 4 + 4 + 4, 4, &sampleCount);
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize + 4 + 4 + 4 + 4, 4, &distribution);
++ }
++
++ cb->resourceUpdate(rub);
++
++ // Render
++ for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) {
++ for (int face = 0; face < 6; ++face) {
++ cb->beginPass(renderTargetsMap[mipLevel][face], QColor(0, 0, 0, 1), { 1.0f, 0 }, nullptr, rhiContext->commonPassFlags());
++ cb->setGraphicsPipeline(prefilterPipeline);
++ cb->setVertexInput(0, 1, &vbufBinding);
++ cb->setViewport(QRhiViewport(0, 0, mipLevelSizes[mipLevel].width(), mipLevelSizes[mipLevel].height()));
++ QVector<QPair<int, quint32>> dynamicOffsets = {
++ { 0, quint32(ubufElementSize * face) },
++ { 2, quint32(ubufPrefilterElementSize * mipLevel) }
++ };
++ cb->setShaderResources(preFilterSrb, 2, dynamicOffsets.constData());
++ cb->draw(36);
++ cb->endPass();
++ }
++ }
++ cb->debugMarkEnd();
++
++ // Write ktx
++
++ const quint32 numberOfMipmapLevels = renderTargetsMap.size();
++ const quint32 numberOfFaces = 6;
++
++ constexpr size_t KTX_IDENTIFIER_LENGTH = 12;
++ constexpr char ktxIdentifier[KTX_IDENTIFIER_LENGTH] = { '\xAB', 'K', 'T', 'X', ' ', '1',
++ '1', '\xBB', '\r', '\n', '\x1A', '\n' };
++ constexpr quint32 platformEndianIdentifier = 0x04030201;
++ QVector<char> keyValueData;
++
++ // Prepare Key/Value array
++ {
++ // Add a key to the metadata to know it was created by our IBL baker
++ static const char key[] = "QT_IBL_BAKER_VERSION";
++ static const char value[] = "1";
++
++ constexpr size_t keyAndValueByteSize = sizeof(key) + sizeof(value); // NB: 2x null terminator
++ appendBinaryVector(keyValueData, keyAndValueByteSize);
++ appendBinaryVector(keyValueData, key);
++ appendBinaryVector(keyValueData, value);
++
++ // Pad until next multiple of 4
++ const size_t padding = 3 - ((keyAndValueByteSize + 3) % 4); // Pad until next multiple of 4
++ keyValueData.resize(keyValueData.size() + padding);
++ }
++
++ // Header
++
++ // identifier
++ ktxOutputFile.write(ktxIdentifier, KTX_IDENTIFIER_LENGTH);
++
++ // endianness
++ writeUInt32(ktxOutputFile, quint32(platformEndianIdentifier));
++
++ // glType
++ writeUInt32(ktxOutputFile, quint32(GL_HALF_FLOAT));
++
++ // glTypeSize (in bytes per component)
++ writeUInt32(ktxOutputFile, quint32(FORMAT.getSizeofFormat()) / quint32(FORMAT.getNumberOfComponent()));
++
++ // glFormat
++ writeUInt32(ktxOutputFile, quint32(GL_RGBA));
++
++ // glInternalFormat
++ writeUInt32(ktxOutputFile, quint32(GL_RGBA16F));
++
++ // glBaseInternalFormat
++ writeUInt32(ktxOutputFile, quint32(GL_RGBA));
++
++ // pixelWidth
++ writeUInt32(ktxOutputFile, quint32(environmentMapSize.width()));
++
++ // pixelHeight
++ writeUInt32(ktxOutputFile, quint32(environmentMapSize.height()));
++
++ // pixelDepth
++ writeUInt32(ktxOutputFile, quint32(0));
++
++ // numberOfArrayElements
++ writeUInt32(ktxOutputFile, quint32(0));
++
++ // numberOfFaces
++ writeUInt32(ktxOutputFile, quint32(numberOfFaces));
++
++ // numberOfMipLevels
++ writeUInt32(ktxOutputFile, quint32(numberOfMipmapLevels));
++
++ // bytesOfKeyValueData
++ writeUInt32(ktxOutputFile, quint32(keyValueData.size()));
++
++ // Key/Value
++ ktxOutputFile.write(keyValueData.data(), keyValueData.size());
++
++ // Images
++ for (quint32 mipmap_level = 0; mipmap_level < numberOfMipmapLevels; mipmap_level++) {
++ quint32 imageSize = 0;
++ for (size_t face = 0; face < numberOfFaces; face++) {
++ QRhiTextureRenderTarget *renderTarget = renderTargetsMap[mipmap_level][face];
++
++ // Read back texture
++ Q_ASSERT(rhi->isRecordingFrame());
++
++ const auto descr = renderTarget->description();
++ const auto texture = descr.cbeginColorAttachments()->texture();
++
++ QRhiReadbackResult result;
++ QRhiReadbackDescription readbackDesc(texture); // null src == read from swapchain backbuffer
++ readbackDesc.setLayer(int(face));
++ readbackDesc.setLevel(mipmap_level);
++
++ QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch();
++ resourceUpdates->readBackTexture(readbackDesc, &result);
++
++ cb->resourceUpdate(resourceUpdates);
++ rhi->finish(); // make sure the readback has finished, stall the pipeline if needed
++
++ // Write imageSize once size is known
++ if (imageSize == 0) {
++ imageSize = result.data.size();
++ writeUInt32(ktxOutputFile, quint32(imageSize));
++ }
++
++ ktxOutputFile.write(result.data);
++ }
++ }
++
++ ktxOutputFile.close();
++
++ preFilteredEnvCubeMap->deleteLater();
++
++ rhi->endOffscreenFrame();
++ rhi->finish();
++
++ return {};
++ }
++
++ void adjustToPlatformQuirks(QRhi::Implementation &impl)
++ {
++ #if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
++ // A macOS VM may not have Metal support at all. We have to decide at this
++ // point, it will be too late afterwards, and the only way is to see if
++ // MTLCreateSystemDefaultDevice succeeds.
++ if (impl == QRhi::Metal) {
++ QRhiMetalInitParams rhiParams;
++ QRhi *tempRhi = QRhi::create(impl, &rhiParams, {});
++ if (!tempRhi) {
++ impl = QRhi::OpenGLES2;
++ qDebug("Metal does not seem to be supported. Falling back to OpenGL.");
++ } else {
++ delete tempRhi;
++ }
++ }
++ #else
++ Q_UNUSED(impl);
++ #endif
++ }
++
++ QRhi::Implementation getRhiImplementation()
++ {
++ QRhi::Implementation implementation = QRhi::Implementation::Null;
++
++ // check env.vars., fall back to platform-specific defaults when backend is not set
++ const QByteArray rhiBackend = qgetenv("QSG_RHI_BACKEND");
++ if (rhiBackend == QByteArrayLiteral("gl") || rhiBackend == QByteArrayLiteral("gles2")
++ || rhiBackend == QByteArrayLiteral("opengl")) {
++ implementation = QRhi::OpenGLES2;
++ } else if (rhiBackend == QByteArrayLiteral("d3d11") || rhiBackend == QByteArrayLiteral("d3d")) {
++ implementation = QRhi::D3D11;
++ } else if (rhiBackend == QByteArrayLiteral("d3d12")) {
++ implementation = QRhi::D3D12;
++ } else if (rhiBackend == QByteArrayLiteral("vulkan")) {
++ implementation = QRhi::Vulkan;
++ } else if (rhiBackend == QByteArrayLiteral("metal")) {
++ implementation = QRhi::Metal;
++ } else if (rhiBackend == QByteArrayLiteral("null")) {
++ implementation = QRhi::Null;
++ } else {
++ if (!rhiBackend.isEmpty()) {
++ qWarning("Unknown key \"%s\" for QSG_RHI_BACKEND, falling back to default backend.", rhiBackend.constData());
++ }
++ #if defined(Q_OS_WIN)
++ implementation = QRhi::D3D11;
++ #elif defined(Q_OS_MACOS) || defined(Q_OS_IOS)
++ implementation = QRhi::Metal;
++ #elif QT_CONFIG(opengl)
++ implementation = QRhi::OpenGLES2;
++ #else
++ implementation = QRhi::Vulkan;
++ #endif
++ }
++
++ adjustToPlatformQuirks(implementation);
++
++ return implementation;
++ }
++
++ QString renderToKTXFile(const QString &inPath, const QString &outPath)
++ {
++ const auto rhiImplementation = getRhiImplementation();
++
++ #if QT_CONFIG(opengl)
++ if (rhiImplementation == QRhi::OpenGLES2) {
++ QRhiGles2InitParams params;
++ if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) {
++ // OpenGL 3.2 or higher
++ params.format.setProfile(QSurfaceFormat::CoreProfile);
++ params.format.setVersion(3, 2);
++ } else {
++ // OpenGL ES 3.0 or higher
++ params.format.setVersion(3, 0);
++ }
++ params.fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
++ const QString result = renderToKTXFileInternal("OpenGL", inPath, outPath, QRhi::OpenGLES2, ¶ms);
++ delete params.fallbackSurface;
++ return result;
++ }
++ #endif
++
++ #if QT_CONFIG(vulkan)
++ if (rhiImplementation == QRhi::Vulkan) {
++ QVulkanInstance vulkanInstance;
++ vulkanInstance.create();
++ QRhiVulkanInitParams params;
++ params.inst = &vulkanInstance;
++ return renderToKTXFileInternal("Vulkan", inPath, outPath, QRhi::Vulkan, ¶ms);
++ }
++ #endif
++
++ #ifdef Q_OS_WIN
++ if (rhiImplementation == QRhi::D3D11) {
++ QRhiD3D11InitParams params;
++ return renderToKTXFileInternal("Direct3D 11", inPath, outPath, QRhi::D3D11, ¶ms);
++ } else if (rhiImplementation == QRhi::D3D12) {
++ QRhiD3D12InitParams params;
++ return renderToKTXFileInternal("Direct3D 12", inPath, outPath, QRhi::D3D12, ¶ms);
++ }
++ #endif
++
++ #if QT_CONFIG(metal)
++ if (rhiImplementation == QRhi::Metal) {
++ QRhiMetalInitParams params;
++ return renderToKTXFileInternal("Metal", inPath, outPath, QRhi::Metal, ¶ms);
++ }
++ #endif
++
++ return QStringLiteral("No RHI backend");
++ }
++
++ const QString QSSGIblBaker::import(const QString &sourceFile, const QDir &savePath, QStringList *generatedFiles)
++ {
++ qDebug() << "IBL lightprobe baker" << sourceFile;
++
++ QString outFileName = savePath.absoluteFilePath(QFileInfo(sourceFile).baseName() + QStringLiteral(".ktx"));
++
++ QString error = renderToKTXFile(sourceFile, outFileName);
++ if (!error.isEmpty())
++ return error;
++
++ m_generatedFiles.append(outFileName);
++
++ if (generatedFiles)
++ *generatedFiles = m_generatedFiles;
++
++ return QString();
++ }
++
++ QT_END_NAMESPACE