| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "qssgiblbaker_p.h" |
| 5 | #include <QFile> |
| 6 | #include <QFileInfo> |
| 7 | #include <QScopeGuard> |
| 8 | |
| 9 | #include <QtQuick3DRuntimeRender/private/qssgrhicontext_p.h> |
| 10 | #include <QtQuick3DRuntimeRender/private/qssgrenderloadedtexture_p.h> |
| 11 | #include <QtQuick3DRuntimeRender/private/qssgrendershadercache_p.h> |
| 12 | |
| 13 | #if QT_CONFIG(opengl) |
| 14 | #include <QOffscreenSurface> |
| 15 | #include <QOpenGLContext> |
| 16 | #endif |
| 17 | |
| 18 | QT_BEGIN_NAMESPACE |
| 19 | |
| 20 | #define GL_FLOAT 0x1406 |
| 21 | #define GL_HALF_FLOAT 0x140B |
| 22 | #define GL_UNSIGNED_BYTE 0x1401 |
| 23 | #define GL_RGBA 0x1908 |
| 24 | #define GL_RGBA8 0x8058 |
| 25 | #define GL_RGBA16F 0x881A |
| 26 | #define GL_RGBA32F 0x8814 |
| 27 | |
| 28 | static constexpr QSSGRenderTextureFormat FORMAT(QSSGRenderTextureFormat::RGBA16F); |
| 29 | |
| 30 | const QStringList QSSGIblBaker::inputExtensions() const |
| 31 | { |
| 32 | return { QStringLiteral("hdr" ), QStringLiteral("exr" )}; |
| 33 | } |
| 34 | |
| 35 | const QString QSSGIblBaker::outputExtension() const |
| 36 | { |
| 37 | return QStringLiteral(".ktx" ); |
| 38 | } |
| 39 | |
| 40 | namespace { |
| 41 | void writeUInt32(QIODevice &device, quint32 value) |
| 42 | { |
| 43 | device.write(data: reinterpret_cast<char *>(&value), len: sizeof(qint32)); |
| 44 | } |
| 45 | |
| 46 | void appendBinaryVector(QVector<char> &dest, const quint32 src) |
| 47 | { |
| 48 | qsizetype oldsize = dest.size(); |
| 49 | dest.resize(size: dest.size() + sizeof(src)); |
| 50 | memcpy(dest: dest.data() + oldsize, src: &src, n: sizeof(src)); |
| 51 | } |
| 52 | |
| 53 | void appendBinaryVector(QVector<char> &dest, const std::string &src) |
| 54 | { |
| 55 | qsizetype oldsize = dest.size(); |
| 56 | dest.resize(size: dest.size() + src.size() + 1); |
| 57 | memcpy(dest: dest.data() + oldsize, src: src.c_str(), n: src.size() + 1); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // Vertex data for rendering environment cube map |
| 62 | static const float cube[] = { |
| 63 | -1.0f, -1.0f, -1.0f, // -X side |
| 64 | -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, |
| 65 | |
| 66 | -1.0f, -1.0f, -1.0f, // -Z side |
| 67 | 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, |
| 68 | |
| 69 | -1.0f, -1.0f, -1.0f, // -Y side |
| 70 | 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, |
| 71 | |
| 72 | -1.0f, 1.0f, -1.0f, // +Y side |
| 73 | -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, |
| 74 | |
| 75 | 1.0f, 1.0f, -1.0f, // +X side |
| 76 | 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, |
| 77 | |
| 78 | -1.0f, 1.0f, 1.0f, // +Z side |
| 79 | -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, |
| 80 | |
| 81 | 0.0f, 1.0f, // -X side |
| 82 | 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, |
| 83 | |
| 84 | 1.0f, 1.0f, // -Z side |
| 85 | 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, |
| 86 | |
| 87 | 1.0f, 0.0f, // -Y side |
| 88 | 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, |
| 89 | |
| 90 | 1.0f, 0.0f, // +Y side |
| 91 | 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, |
| 92 | |
| 93 | 1.0f, 0.0f, // +X side |
| 94 | 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, |
| 95 | |
| 96 | 0.0f, 0.0f, // +Z side |
| 97 | 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, |
| 98 | }; |
| 99 | |
| 100 | QString renderToKTXFileInternal(const char *name, const QString &inPath, const QString &outPath, QRhi::Implementation impl, QRhiInitParams *initParams) |
| 101 | { |
| 102 | qDebug() << "Using RHI backend" << name; |
| 103 | |
| 104 | // Open output file |
| 105 | QFile ktxOutputFile(outPath); |
| 106 | if (!ktxOutputFile.open(flags: QIODevice::WriteOnly)) { |
| 107 | return QStringLiteral("Could not open file: %1" ).arg(a: outPath); |
| 108 | } |
| 109 | |
| 110 | QScopedPointer<QRhi> rhi(QRhi::create(impl, params: initParams, flags: QRhi::Flags(), importDevice: nullptr)); |
| 111 | if (!rhi) |
| 112 | return QStringLiteral("Failed to initialize QRhi" ); |
| 113 | |
| 114 | qDebug() << rhi->driverInfo(); |
| 115 | |
| 116 | QRhiCommandBuffer *cb; |
| 117 | rhi->beginOffscreenFrame(cb: &cb); |
| 118 | |
| 119 | const auto rhiContext = std::make_unique<QSSGRhiContext>(args: rhi.get()); |
| 120 | QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(q: rhiContext.get()); |
| 121 | rhiCtxD->setCommandBuffer(cb); |
| 122 | |
| 123 | QScopedPointer<QSSGLoadedTexture> inImage(QSSGLoadedTexture::loadHdrImage(source: QSSGInputUtil::getStreamForFile(inPath), inFormat: FORMAT)); |
| 124 | if (!inImage) |
| 125 | return QStringLiteral("Failed to load hdr file" ); |
| 126 | |
| 127 | auto shaderCache = std::make_unique<QSSGShaderCache>(args&: *rhiContext); |
| 128 | |
| 129 | // The objective of this method is to take the equirectangular texture |
| 130 | // provided by inImage and create a cubeMap that contains both pre-filtered |
| 131 | // specular environment maps, as well as a irradiance map for diffuse |
| 132 | // operations. |
| 133 | // To achieve this though we first convert convert the Equirectangular texture |
| 134 | // to a cubeMap with genereated mip map levels (no filtering) to make the |
| 135 | // process of creating the prefiltered and irradiance maps eaiser. This |
| 136 | // intermediate texture as well as the original equirectangular texture are |
| 137 | // destroyed after this frame completes, and all further associations with |
| 138 | // the source lightProbe texture are instead associated with the final |
| 139 | // generated environment map. |
| 140 | // The intermediate environment cubemap is used to generate the final |
| 141 | // cubemap. This cubemap will generate 6 mip levels for each face |
| 142 | // (the remaining faces are unused). This is what the contents of each |
| 143 | // face mip level looks like: |
| 144 | // 0: Pre-filtered with roughness 0 (basically unfiltered) |
| 145 | // 1: Pre-filtered with roughness 0.25 |
| 146 | // 2: Pre-filtered with roughness 0.5 |
| 147 | // 3: Pre-filtered with roughness 0.75 |
| 148 | // 4: Pre-filtered with rougnness 1.0 |
| 149 | // 5: Irradiance map (ideally at least 16x16) |
| 150 | // It would be better if we could use a separate cubemap for irradiance, but |
| 151 | // right now there is a 1:1 association between texture sources on the front- |
| 152 | // end and backend. |
| 153 | |
| 154 | // Right now minimum face size needs to be 512x512 to be able to have 6 reasonably sized mips |
| 155 | const int suggestedSize = qMax(a: 512.f, b: inImage->height * 0.5f); |
| 156 | const QSize environmentMapSize(suggestedSize, suggestedSize); |
| 157 | const bool isRGBE = inImage->format.format == QSSGRenderTextureFormat::Format::RGBE8; |
| 158 | const int colorSpace = inImage->isSRGB ? 1 : 0; // 0 Linear | 1 sRGB |
| 159 | |
| 160 | // Phase 1: Convert the Equirectangular texture to a Cubemap |
| 161 | QRhiTexture *envCubeMap = rhi->newTexture(format: QRhiTexture::RGBA16F, |
| 162 | pixelSize: environmentMapSize, |
| 163 | sampleCount: 1, |
| 164 | flags: QRhiTexture::RenderTarget | QRhiTexture::CubeMap | QRhiTexture::MipMapped |
| 165 | | QRhiTexture::UsedWithGenerateMips); |
| 166 | if (!envCubeMap->create()) { |
| 167 | return QStringLiteral("Failed to create Environment Cube Map" ); |
| 168 | } |
| 169 | envCubeMap->deleteLater(); |
| 170 | |
| 171 | // Create a renderbuffer the size of a the cubeMap face |
| 172 | QRhiRenderBuffer *envMapRenderBuffer = rhi->newRenderBuffer(type: QRhiRenderBuffer::Color, pixelSize: environmentMapSize); |
| 173 | if (!envMapRenderBuffer->create()) { |
| 174 | return QStringLiteral("Failed to create Environment Map Render Buffer" ); |
| 175 | } |
| 176 | envMapRenderBuffer->deleteLater(); |
| 177 | |
| 178 | // Setup the 6 render targets for each cube face |
| 179 | QVarLengthArray<QRhiTextureRenderTarget *, 6> renderTargets; |
| 180 | QRhiRenderPassDescriptor *renderPassDesc = nullptr; |
| 181 | for (int face = 0; face < 6; ++face) { |
| 182 | QRhiColorAttachment att(envCubeMap); |
| 183 | att.setLayer(face); |
| 184 | QRhiTextureRenderTargetDescription rtDesc; |
| 185 | rtDesc.setColorAttachments({ att }); |
| 186 | auto renderTarget = rhi->newTextureRenderTarget(desc: rtDesc); |
| 187 | renderTarget->setDescription(rtDesc); |
| 188 | if (!renderPassDesc) |
| 189 | renderPassDesc = renderTarget->newCompatibleRenderPassDescriptor(); |
| 190 | renderTarget->setRenderPassDescriptor(renderPassDesc); |
| 191 | if (!renderTarget->create()) { |
| 192 | return QStringLiteral("Failed to build env map render target" ); |
| 193 | } |
| 194 | renderTarget->deleteLater(); |
| 195 | renderTargets << renderTarget; |
| 196 | } |
| 197 | renderPassDesc->deleteLater(); |
| 198 | |
| 199 | // Setup the sampler for reading the equirectangular loaded texture |
| 200 | QSize size(inImage->width, inImage->height); |
| 201 | auto *sourceTexture = rhi->newTexture(format: QRhiTexture::RGBA16F, pixelSize: size, sampleCount: 1); |
| 202 | if (!sourceTexture->create()) { |
| 203 | return QStringLiteral("Failed to create source env map texture" ); |
| 204 | } |
| 205 | sourceTexture->deleteLater(); |
| 206 | |
| 207 | // Upload the equirectangular texture |
| 208 | QRhiTextureUploadDescription desc; |
| 209 | if (inImage->textureFileData.isValid()) { |
| 210 | desc = { { 0, |
| 211 | 0, |
| 212 | { inImage->textureFileData.data().constData() + inImage->textureFileData.dataOffset(level: 0), |
| 213 | quint32(inImage->textureFileData.dataLength(level: 0)) } } }; |
| 214 | } else { |
| 215 | desc = { { 0, 0, { inImage->data, inImage->dataSizeInBytes } } }; |
| 216 | } |
| 217 | auto *rub = rhi->nextResourceUpdateBatch(); |
| 218 | rub->uploadTexture(tex: sourceTexture, desc); |
| 219 | |
| 220 | const QSSGRhiSamplerDescription samplerDesc { |
| 221 | .minFilter: QRhiSampler::Linear, .magFilter: QRhiSampler::Linear, .mipmap: QRhiSampler::None, .hTiling: QRhiSampler::ClampToEdge, .vTiling: QRhiSampler::ClampToEdge, .zTiling: QRhiSampler::Repeat |
| 222 | }; |
| 223 | QRhiSampler *sampler = rhiContext->sampler(samplerDescription: samplerDesc); |
| 224 | |
| 225 | // Load shader and setup render pipeline |
| 226 | const auto &envMapShaderStages = shaderCache->getBuiltInRhiShaders().getRhiEnvironmentmapShader(); |
| 227 | |
| 228 | // Vertex Buffer - Just a single cube that will be viewed from inside |
| 229 | QRhiBuffer *vertexBuffer = rhi->newBuffer(type: QRhiBuffer::Immutable, usage: QRhiBuffer::VertexBuffer, size: sizeof(cube)); |
| 230 | vertexBuffer->create(); |
| 231 | vertexBuffer->deleteLater(); |
| 232 | rub->uploadStaticBuffer(buf: vertexBuffer, data: cube); |
| 233 | |
| 234 | // Uniform Buffer - 2x mat4 |
| 235 | int ubufElementSize = rhi->ubufAligned(v: 128); |
| 236 | QRhiBuffer *uBuf = rhi->newBuffer(type: QRhiBuffer::Dynamic, usage: QRhiBuffer::UniformBuffer, size: ubufElementSize * 6); |
| 237 | uBuf->create(); |
| 238 | uBuf->deleteLater(); |
| 239 | |
| 240 | int ubufEnvMapElementSize = rhi->ubufAligned(v: 4); |
| 241 | QRhiBuffer *uBufEnvMap = rhi->newBuffer(type: QRhiBuffer::Dynamic, usage: QRhiBuffer::UniformBuffer, size: ubufEnvMapElementSize * 6); |
| 242 | uBufEnvMap->create(); |
| 243 | uBufEnvMap->deleteLater(); |
| 244 | |
| 245 | // Shader Resource Bindings |
| 246 | QRhiShaderResourceBindings *envMapSrb = rhi->newShaderResourceBindings(); |
| 247 | envMapSrb->setBindings( |
| 248 | { QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(binding: 0, stage: QRhiShaderResourceBinding::VertexStage, buf: uBuf, size: 128), |
| 249 | QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(binding: 2, stage: QRhiShaderResourceBinding::FragmentStage, buf: uBufEnvMap, size: ubufEnvMapElementSize), |
| 250 | QRhiShaderResourceBinding::sampledTexture(binding: 1, stage: QRhiShaderResourceBinding::FragmentStage, tex: sourceTexture, sampler) }); |
| 251 | envMapSrb->create(); |
| 252 | envMapSrb->deleteLater(); |
| 253 | |
| 254 | // Pipeline |
| 255 | QRhiGraphicsPipeline *envMapPipeline = rhi->newGraphicsPipeline(); |
| 256 | envMapPipeline->setCullMode(QRhiGraphicsPipeline::Front); |
| 257 | envMapPipeline->setFrontFace(QRhiGraphicsPipeline::CCW); |
| 258 | envMapPipeline->setShaderStages({ *envMapShaderStages->vertexStage(), *envMapShaderStages->fragmentStage() }); |
| 259 | |
| 260 | QRhiVertexInputLayout inputLayout; |
| 261 | inputLayout.setBindings({ { 3 * sizeof(float) } }); |
| 262 | inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 } }); |
| 263 | |
| 264 | envMapPipeline->setVertexInputLayout(inputLayout); |
| 265 | envMapPipeline->setShaderResourceBindings(envMapSrb); |
| 266 | envMapPipeline->setRenderPassDescriptor(renderPassDesc); |
| 267 | if (!envMapPipeline->create()) { |
| 268 | return QStringLiteral("Failed to create source env map pipeline state" ); |
| 269 | } |
| 270 | envMapPipeline->deleteLater(); |
| 271 | |
| 272 | // Do the actual render passes |
| 273 | cb->debugMarkBegin(name: "Environment Cubemap Generation" ); |
| 274 | const QRhiCommandBuffer::VertexInput vbufBinding(vertexBuffer, 0); |
| 275 | |
| 276 | // Set the Uniform Data |
| 277 | QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix(); |
| 278 | mvp.perspective(verticalAngle: 90.0f, aspectRatio: 1.0f, nearPlane: 0.1f, farPlane: 10.0f); |
| 279 | |
| 280 | auto lookAt = [](const QVector3D &eye, const QVector3D ¢er, const QVector3D &up) { |
| 281 | QMatrix4x4 viewMatrix; |
| 282 | viewMatrix.lookAt(eye, center, up); |
| 283 | return viewMatrix; |
| 284 | }; |
| 285 | QVarLengthArray<QMatrix4x4, 6> views; |
| 286 | views.append(t: lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f))); |
| 287 | views.append(t: lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(-1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f))); |
| 288 | if (rhi->isYUpInFramebuffer()) { |
| 289 | views.append(t: lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f))); |
| 290 | views.append(t: lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f))); |
| 291 | } else { |
| 292 | views.append(t: lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f))); |
| 293 | views.append(t: lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f))); |
| 294 | } |
| 295 | views.append(t: lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, 1.0), QVector3D(0.0f, -1.0f, 0.0f))); |
| 296 | views.append(t: lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, -1.0), QVector3D(0.0f, -1.0f, 0.0f))); |
| 297 | for (int face = 0; face < 6; ++face) { |
| 298 | rub->updateDynamicBuffer(buf: uBuf, offset: face * ubufElementSize, size: 64, data: mvp.constData()); |
| 299 | rub->updateDynamicBuffer(buf: uBuf, offset: face * ubufElementSize + 64, size: 64, data: views[face].constData()); |
| 300 | rub->updateDynamicBuffer(buf: uBufEnvMap, offset: face * ubufEnvMapElementSize, size: 4, data: &colorSpace); |
| 301 | } |
| 302 | cb->resourceUpdate(resourceUpdates: rub); |
| 303 | |
| 304 | for (int face = 0; face < 6; ++face) { |
| 305 | cb->beginPass(rt: renderTargets[face], colorClearValue: QColor(0, 0, 0, 1), depthStencilClearValue: { 1.0f, 0 }, resourceUpdates: nullptr, flags: rhiContext->commonPassFlags()); |
| 306 | |
| 307 | // Execute render pass |
| 308 | cb->setGraphicsPipeline(envMapPipeline); |
| 309 | cb->setVertexInput(startBinding: 0, bindingCount: 1, bindings: &vbufBinding); |
| 310 | cb->setViewport(QRhiViewport(0, 0, environmentMapSize.width(), environmentMapSize.height())); |
| 311 | QVector<QPair<int, quint32>> dynamicOffset = { |
| 312 | { 0, quint32(ubufElementSize * face) }, |
| 313 | { 2, quint32(ubufEnvMapElementSize * face )} |
| 314 | }; |
| 315 | cb->setShaderResources(srb: envMapSrb, dynamicOffsetCount: 2, dynamicOffsets: dynamicOffset.constData()); |
| 316 | |
| 317 | cb->draw(vertexCount: 36); |
| 318 | cb->endPass(); |
| 319 | } |
| 320 | cb->debugMarkEnd(); |
| 321 | |
| 322 | if (!isRGBE) { |
| 323 | // Generate mipmaps for envMap |
| 324 | rub = rhi->nextResourceUpdateBatch(); |
| 325 | rub->generateMips(tex: envCubeMap); |
| 326 | cb->resourceUpdate(resourceUpdates: rub); |
| 327 | } |
| 328 | |
| 329 | // Phase 2: Generate the pre-filtered environment cubemap |
| 330 | cb->debugMarkBegin(name: "Pre-filtered Environment Cubemap Generation" ); |
| 331 | QRhiTexture *preFilteredEnvCubeMap = rhi->newTexture(format: QRhiTexture::RGBA16F, |
| 332 | pixelSize: environmentMapSize, |
| 333 | sampleCount: 1, |
| 334 | flags: QRhiTexture::RenderTarget | QRhiTexture::CubeMap | QRhiTexture::MipMapped); |
| 335 | if (!preFilteredEnvCubeMap->create()) |
| 336 | qWarning(msg: "Failed to create Pre-filtered Environment Cube Map" ); |
| 337 | int mipmapCount = rhi->mipLevelsForSize(size: environmentMapSize); |
| 338 | mipmapCount = qMin(a: mipmapCount, b: 6); // don't create more than 6 mip levels |
| 339 | QMap<int, QSize> mipLevelSizes; |
| 340 | QMap<int, QVarLengthArray<QRhiTextureRenderTarget *, 6>> renderTargetsMap; |
| 341 | QRhiRenderPassDescriptor *renderPassDescriptorPhase2 = nullptr; |
| 342 | |
| 343 | // Create a renderbuffer for each mip level |
| 344 | for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) { |
| 345 | const QSize levelSize = QSize(environmentMapSize.width() * std::pow(x: 0.5, y: mipLevel), |
| 346 | environmentMapSize.height() * std::pow(x: 0.5, y: mipLevel)); |
| 347 | mipLevelSizes.insert(key: mipLevel, value: levelSize); |
| 348 | // Setup Render targets (6 * mipmapCount) |
| 349 | QVarLengthArray<QRhiTextureRenderTarget *, 6> renderTargets; |
| 350 | for (int face = 0; face < 6; ++face) { |
| 351 | QRhiColorAttachment att(preFilteredEnvCubeMap); |
| 352 | att.setLayer(face); |
| 353 | att.setLevel(mipLevel); |
| 354 | QRhiTextureRenderTargetDescription rtDesc; |
| 355 | rtDesc.setColorAttachments({ att }); |
| 356 | auto renderTarget = rhi->newTextureRenderTarget(desc: rtDesc); |
| 357 | renderTarget->setDescription(rtDesc); |
| 358 | if (!renderPassDescriptorPhase2) |
| 359 | renderPassDescriptorPhase2 = renderTarget->newCompatibleRenderPassDescriptor(); |
| 360 | renderTarget->setRenderPassDescriptor(renderPassDescriptorPhase2); |
| 361 | if (!renderTarget->create()) |
| 362 | qWarning(msg: "Failed to build prefilter env map render target" ); |
| 363 | renderTarget->deleteLater(); |
| 364 | renderTargets << renderTarget; |
| 365 | } |
| 366 | renderTargetsMap.insert(key: mipLevel, value: renderTargets); |
| 367 | renderPassDescriptorPhase2->deleteLater(); |
| 368 | } |
| 369 | |
| 370 | // Load the prefilter shader stages |
| 371 | const auto &prefilterShaderStages = shaderCache->getBuiltInRhiShaders().getRhienvironmentmapPreFilterShader(isRGBE); |
| 372 | |
| 373 | // Create a new Sampler |
| 374 | const QSSGRhiSamplerDescription samplerMipMapDesc { |
| 375 | .minFilter: QRhiSampler::Linear, .magFilter: QRhiSampler::Linear, .mipmap: QRhiSampler::Linear, .hTiling: QRhiSampler::ClampToEdge, .vTiling: QRhiSampler::ClampToEdge, .zTiling: QRhiSampler::Repeat |
| 376 | }; |
| 377 | |
| 378 | QRhiSampler *envMapCubeSampler = nullptr; |
| 379 | // Only use mipmap interpoliation if not using RGBE |
| 380 | if (!isRGBE) |
| 381 | envMapCubeSampler = rhiContext->sampler(samplerDescription: samplerMipMapDesc); |
| 382 | else |
| 383 | envMapCubeSampler = sampler; |
| 384 | |
| 385 | // Reuse Vertex Buffer from phase 1 |
| 386 | // Reuse UniformBuffer from phase 1 (for vertex shader) |
| 387 | |
| 388 | // UniformBuffer |
| 389 | // float roughness; |
| 390 | // float resolution; |
| 391 | // float lodBias; |
| 392 | // int sampleCount; |
| 393 | // int distribution; |
| 394 | |
| 395 | int ubufPrefilterElementSize = rhi->ubufAligned(v: 20); |
| 396 | QRhiBuffer *uBufPrefilter = rhi->newBuffer(type: QRhiBuffer::Dynamic, usage: QRhiBuffer::UniformBuffer, size: ubufPrefilterElementSize * mipmapCount); |
| 397 | uBufPrefilter->create(); |
| 398 | uBufPrefilter->deleteLater(); |
| 399 | |
| 400 | // Shader Resource Bindings |
| 401 | QRhiShaderResourceBindings *preFilterSrb = rhi->newShaderResourceBindings(); |
| 402 | preFilterSrb->setBindings({ |
| 403 | QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(binding: 0, stage: QRhiShaderResourceBinding::VertexStage, buf: uBuf, size: 128), |
| 404 | QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(binding: 2, stage: QRhiShaderResourceBinding::FragmentStage, buf: uBufPrefilter, size: 20), |
| 405 | QRhiShaderResourceBinding::sampledTexture(binding: 1, stage: QRhiShaderResourceBinding::FragmentStage, tex: envCubeMap, sampler: envMapCubeSampler) |
| 406 | }); |
| 407 | preFilterSrb->create(); |
| 408 | preFilterSrb->deleteLater(); |
| 409 | |
| 410 | // Pipeline |
| 411 | QRhiGraphicsPipeline *prefilterPipeline = rhi->newGraphicsPipeline(); |
| 412 | prefilterPipeline->setCullMode(QRhiGraphicsPipeline::Front); |
| 413 | prefilterPipeline->setFrontFace(QRhiGraphicsPipeline::CCW); |
| 414 | prefilterPipeline->setDepthOp(QRhiGraphicsPipeline::LessOrEqual); |
| 415 | prefilterPipeline->setShaderStages({ |
| 416 | *prefilterShaderStages->vertexStage(), |
| 417 | *prefilterShaderStages->fragmentStage() |
| 418 | }); |
| 419 | // same as phase 1 |
| 420 | prefilterPipeline->setVertexInputLayout(inputLayout); |
| 421 | prefilterPipeline->setShaderResourceBindings(preFilterSrb); |
| 422 | prefilterPipeline->setRenderPassDescriptor(renderPassDescriptorPhase2); |
| 423 | if (!prefilterPipeline->create()) |
| 424 | return QStringLiteral("Failed to create pre-filter env map pipeline state" ); |
| 425 | prefilterPipeline->deleteLater(); |
| 426 | |
| 427 | // Uniform Data |
| 428 | // set the roughness uniform buffer data |
| 429 | rub = rhi->nextResourceUpdateBatch(); |
| 430 | const float resolution = environmentMapSize.width(); |
| 431 | const float lodBias = 0.0f; |
| 432 | const int sampleCount = 1024; |
| 433 | for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) { |
| 434 | Q_ASSERT(mipmapCount - 2); |
| 435 | const float roughness = float(mipLevel) / float(mipmapCount - 2); |
| 436 | const int distribution = mipLevel == (mipmapCount - 1) ? 0 : 1; // last mip level is for irradiance |
| 437 | rub->updateDynamicBuffer(buf: uBufPrefilter, offset: mipLevel * ubufPrefilterElementSize, size: 4, data: &roughness); |
| 438 | rub->updateDynamicBuffer(buf: uBufPrefilter, offset: mipLevel * ubufPrefilterElementSize + 4, size: 4, data: &resolution); |
| 439 | rub->updateDynamicBuffer(buf: uBufPrefilter, offset: mipLevel * ubufPrefilterElementSize + 4 + 4, size: 4, data: &lodBias); |
| 440 | rub->updateDynamicBuffer(buf: uBufPrefilter, offset: mipLevel * ubufPrefilterElementSize + 4 + 4 + 4, size: 4, data: &sampleCount); |
| 441 | rub->updateDynamicBuffer(buf: uBufPrefilter, offset: mipLevel * ubufPrefilterElementSize + 4 + 4 + 4 + 4, size: 4, data: &distribution); |
| 442 | } |
| 443 | |
| 444 | cb->resourceUpdate(resourceUpdates: rub); |
| 445 | |
| 446 | // Render |
| 447 | for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) { |
| 448 | for (int face = 0; face < 6; ++face) { |
| 449 | cb->beginPass(rt: renderTargetsMap[mipLevel][face], colorClearValue: QColor(0, 0, 0, 1), depthStencilClearValue: { 1.0f, 0 }, resourceUpdates: nullptr, flags: rhiContext->commonPassFlags()); |
| 450 | cb->setGraphicsPipeline(prefilterPipeline); |
| 451 | cb->setVertexInput(startBinding: 0, bindingCount: 1, bindings: &vbufBinding); |
| 452 | cb->setViewport(QRhiViewport(0, 0, mipLevelSizes[mipLevel].width(), mipLevelSizes[mipLevel].height())); |
| 453 | QVector<QPair<int, quint32>> dynamicOffsets = { |
| 454 | { 0, quint32(ubufElementSize * face) }, |
| 455 | { 2, quint32(ubufPrefilterElementSize * mipLevel) } |
| 456 | }; |
| 457 | cb->setShaderResources(srb: preFilterSrb, dynamicOffsetCount: 2, dynamicOffsets: dynamicOffsets.constData()); |
| 458 | cb->draw(vertexCount: 36); |
| 459 | cb->endPass(); |
| 460 | } |
| 461 | } |
| 462 | cb->debugMarkEnd(); |
| 463 | |
| 464 | // Write ktx |
| 465 | |
| 466 | const quint32 numberOfMipmapLevels = renderTargetsMap.size(); |
| 467 | const quint32 numberOfFaces = 6; |
| 468 | |
| 469 | constexpr size_t KTX_IDENTIFIER_LENGTH = 12; |
| 470 | constexpr char ktxIdentifier[KTX_IDENTIFIER_LENGTH] = { '\xAB', 'K', 'T', 'X', ' ', '1', |
| 471 | '1', '\xBB', '\r', '\n', '\x1A', '\n' }; |
| 472 | constexpr quint32 platformEndianIdentifier = 0x04030201; |
| 473 | QVector<char> keyValueData; |
| 474 | |
| 475 | // Prepare Key/Value array |
| 476 | { |
| 477 | // Add a key to the metadata to know it was created by our IBL baker |
| 478 | static const char key[] = "QT_IBL_BAKER_VERSION" ; |
| 479 | static const char value[] = "1" ; |
| 480 | |
| 481 | constexpr size_t keyAndValueByteSize = sizeof(key) + sizeof(value); // NB: 2x null terminator |
| 482 | appendBinaryVector(dest&: keyValueData, src: keyAndValueByteSize); |
| 483 | appendBinaryVector(dest&: keyValueData, src: key); |
| 484 | appendBinaryVector(dest&: keyValueData, src: value); |
| 485 | |
| 486 | // Pad until next multiple of 4 |
| 487 | const size_t padding = 3 - ((keyAndValueByteSize + 3) % 4); // Pad until next multiple of 4 |
| 488 | keyValueData.resize(size: keyValueData.size() + padding); |
| 489 | } |
| 490 | |
| 491 | // Header |
| 492 | |
| 493 | // identifier |
| 494 | ktxOutputFile.write(data: ktxIdentifier, len: KTX_IDENTIFIER_LENGTH); |
| 495 | |
| 496 | // endianness |
| 497 | writeUInt32(device&: ktxOutputFile, value: quint32(platformEndianIdentifier)); |
| 498 | |
| 499 | // glType |
| 500 | writeUInt32(device&: ktxOutputFile, value: quint32(GL_HALF_FLOAT)); |
| 501 | |
| 502 | // glTypeSize (in bytes per component) |
| 503 | writeUInt32(device&: ktxOutputFile, value: quint32(FORMAT.getSizeofFormat()) / quint32(FORMAT.getNumberOfComponent())); |
| 504 | |
| 505 | // glFormat |
| 506 | writeUInt32(device&: ktxOutputFile, value: quint32(GL_RGBA)); |
| 507 | |
| 508 | // glInternalFormat |
| 509 | writeUInt32(device&: ktxOutputFile, value: quint32(GL_RGBA16F)); |
| 510 | |
| 511 | // glBaseInternalFormat |
| 512 | writeUInt32(device&: ktxOutputFile, value: quint32(GL_RGBA)); |
| 513 | |
| 514 | // pixelWidth |
| 515 | writeUInt32(device&: ktxOutputFile, value: quint32(environmentMapSize.width())); |
| 516 | |
| 517 | // pixelHeight |
| 518 | writeUInt32(device&: ktxOutputFile, value: quint32(environmentMapSize.height())); |
| 519 | |
| 520 | // pixelDepth |
| 521 | writeUInt32(device&: ktxOutputFile, value: quint32(0)); |
| 522 | |
| 523 | // numberOfArrayElements |
| 524 | writeUInt32(device&: ktxOutputFile, value: quint32(0)); |
| 525 | |
| 526 | // numberOfFaces |
| 527 | writeUInt32(device&: ktxOutputFile, value: quint32(numberOfFaces)); |
| 528 | |
| 529 | // numberOfMipLevels |
| 530 | writeUInt32(device&: ktxOutputFile, value: quint32(numberOfMipmapLevels)); |
| 531 | |
| 532 | // bytesOfKeyValueData |
| 533 | writeUInt32(device&: ktxOutputFile, value: quint32(keyValueData.size())); |
| 534 | |
| 535 | // Key/Value |
| 536 | ktxOutputFile.write(data: keyValueData.data(), len: keyValueData.size()); |
| 537 | |
| 538 | // Images |
| 539 | for (quint32 mipmap_level = 0; mipmap_level < numberOfMipmapLevels; mipmap_level++) { |
| 540 | quint32 imageSize = 0; |
| 541 | for (size_t face = 0; face < numberOfFaces; face++) { |
| 542 | QRhiTextureRenderTarget *renderTarget = renderTargetsMap[mipmap_level][face]; |
| 543 | |
| 544 | // Read back texture |
| 545 | Q_ASSERT(rhi->isRecordingFrame()); |
| 546 | |
| 547 | const auto texture = renderTarget->description().cbeginColorAttachments()->texture(); |
| 548 | |
| 549 | QRhiReadbackResult result; |
| 550 | QRhiReadbackDescription readbackDesc(texture); // null src == read from swapchain backbuffer |
| 551 | readbackDesc.setLayer(int(face)); |
| 552 | readbackDesc.setLevel(mipmap_level); |
| 553 | |
| 554 | QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch(); |
| 555 | resourceUpdates->readBackTexture(rb: readbackDesc, result: &result); |
| 556 | |
| 557 | cb->resourceUpdate(resourceUpdates); |
| 558 | rhi->finish(); // make sure the readback has finished, stall the pipeline if needed |
| 559 | |
| 560 | // Write imageSize once size is known |
| 561 | if (imageSize == 0) { |
| 562 | imageSize = result.data.size(); |
| 563 | writeUInt32(device&: ktxOutputFile, value: quint32(imageSize)); |
| 564 | } |
| 565 | |
| 566 | ktxOutputFile.write(data: result.data); |
| 567 | } |
| 568 | } |
| 569 | |
| 570 | ktxOutputFile.close(); |
| 571 | |
| 572 | preFilteredEnvCubeMap->deleteLater(); |
| 573 | |
| 574 | rhi->endOffscreenFrame(); |
| 575 | rhi->finish(); |
| 576 | |
| 577 | return {}; |
| 578 | } |
| 579 | |
| 580 | void adjustToPlatformQuirks(QRhi::Implementation &impl) |
| 581 | { |
| 582 | #if defined(Q_OS_MACOS) || defined(Q_OS_IOS) |
| 583 | // A macOS VM may not have Metal support at all. We have to decide at this |
| 584 | // point, it will be too late afterwards, and the only way is to see if |
| 585 | // MTLCreateSystemDefaultDevice succeeds. |
| 586 | if (impl == QRhi::Metal) { |
| 587 | QRhiMetalInitParams rhiParams; |
| 588 | QRhi *tempRhi = QRhi::create(impl, &rhiParams, {}); |
| 589 | if (!tempRhi) { |
| 590 | impl = QRhi::OpenGLES2; |
| 591 | qDebug("Metal does not seem to be supported. Falling back to OpenGL." ); |
| 592 | } else { |
| 593 | delete tempRhi; |
| 594 | } |
| 595 | } |
| 596 | #else |
| 597 | Q_UNUSED(impl); |
| 598 | #endif |
| 599 | } |
| 600 | |
| 601 | QRhi::Implementation getRhiImplementation() |
| 602 | { |
| 603 | QRhi::Implementation implementation = QRhi::Implementation::Null; |
| 604 | |
| 605 | // check env.vars., fall back to platform-specific defaults when backend is not set |
| 606 | const QByteArray rhiBackend = qgetenv(varName: "QSG_RHI_BACKEND" ); |
| 607 | if (rhiBackend == QByteArrayLiteral("gl" ) || rhiBackend == QByteArrayLiteral("gles2" ) |
| 608 | || rhiBackend == QByteArrayLiteral("opengl" )) { |
| 609 | implementation = QRhi::OpenGLES2; |
| 610 | } else if (rhiBackend == QByteArrayLiteral("d3d11" ) || rhiBackend == QByteArrayLiteral("d3d" )) { |
| 611 | implementation = QRhi::D3D11; |
| 612 | } else if (rhiBackend == QByteArrayLiteral("d3d12" )) { |
| 613 | implementation = QRhi::D3D12; |
| 614 | } else if (rhiBackend == QByteArrayLiteral("vulkan" )) { |
| 615 | implementation = QRhi::Vulkan; |
| 616 | } else if (rhiBackend == QByteArrayLiteral("metal" )) { |
| 617 | implementation = QRhi::Metal; |
| 618 | } else if (rhiBackend == QByteArrayLiteral("null" )) { |
| 619 | implementation = QRhi::Null; |
| 620 | } else { |
| 621 | if (!rhiBackend.isEmpty()) { |
| 622 | qWarning(msg: "Unknown key \"%s\" for QSG_RHI_BACKEND, falling back to default backend." , rhiBackend.constData()); |
| 623 | } |
| 624 | #if defined(Q_OS_WIN) |
| 625 | implementation = QRhi::D3D11; |
| 626 | #elif defined(Q_OS_MACOS) || defined(Q_OS_IOS) |
| 627 | implementation = QRhi::Metal; |
| 628 | #elif QT_CONFIG(opengl) |
| 629 | implementation = QRhi::OpenGLES2; |
| 630 | #else |
| 631 | implementation = QRhi::Vulkan; |
| 632 | #endif |
| 633 | } |
| 634 | |
| 635 | adjustToPlatformQuirks(impl&: implementation); |
| 636 | |
| 637 | return implementation; |
| 638 | } |
| 639 | |
| 640 | QString renderToKTXFile(const QString &inPath, const QString &outPath) |
| 641 | { |
| 642 | const auto rhiImplementation = getRhiImplementation(); |
| 643 | |
| 644 | #if QT_CONFIG(opengl) |
| 645 | if (rhiImplementation == QRhi::OpenGLES2) { |
| 646 | QRhiGles2InitParams params; |
| 647 | if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { |
| 648 | // OpenGL 3.2 or higher |
| 649 | params.format.setProfile(QSurfaceFormat::CoreProfile); |
| 650 | params.format.setVersion(major: 3, minor: 2); |
| 651 | } else { |
| 652 | // OpenGL ES 3.0 or higher |
| 653 | params.format.setVersion(major: 3, minor: 0); |
| 654 | } |
| 655 | params.fallbackSurface = QRhiGles2InitParams::newFallbackSurface(); |
| 656 | const QString result = renderToKTXFileInternal(name: "OpenGL" , inPath, outPath, impl: QRhi::OpenGLES2, initParams: ¶ms); |
| 657 | delete params.fallbackSurface; |
| 658 | return result; |
| 659 | } |
| 660 | #endif |
| 661 | |
| 662 | #if QT_CONFIG(vulkan) |
| 663 | if (rhiImplementation == QRhi::Vulkan) { |
| 664 | QVulkanInstance vulkanInstance; |
| 665 | vulkanInstance.create(); |
| 666 | QRhiVulkanInitParams params; |
| 667 | params.inst = &vulkanInstance; |
| 668 | return renderToKTXFileInternal(name: "Vulkan" , inPath, outPath, impl: QRhi::Vulkan, initParams: ¶ms); |
| 669 | } |
| 670 | #endif |
| 671 | |
| 672 | #ifdef Q_OS_WIN |
| 673 | if (rhiImplementation == QRhi::D3D11) { |
| 674 | QRhiD3D11InitParams params; |
| 675 | return renderToKTXFileInternal("Direct3D 11" , inPath, outPath, QRhi::D3D11, ¶ms); |
| 676 | } else if (rhiImplementation == QRhi::D3D12) { |
| 677 | QRhiD3D12InitParams params; |
| 678 | return renderToKTXFileInternal("Direct3D 12" , inPath, outPath, QRhi::D3D12, ¶ms); |
| 679 | } |
| 680 | #endif |
| 681 | |
| 682 | #if QT_CONFIG(metal) |
| 683 | if (rhiImplementation == QRhi::Metal) { |
| 684 | QRhiMetalInitParams params; |
| 685 | return renderToKTXFileInternal("Metal" , inPath, outPath, QRhi::Metal, ¶ms); |
| 686 | } |
| 687 | #endif |
| 688 | |
| 689 | return QStringLiteral("No RHI backend" ); |
| 690 | } |
| 691 | |
| 692 | const QString QSSGIblBaker::import(const QString &sourceFile, const QDir &savePath, QStringList *generatedFiles) |
| 693 | { |
| 694 | qDebug() << "IBL lightprobe baker" << sourceFile; |
| 695 | |
| 696 | QString outFileName = savePath.absoluteFilePath(fileName: QFileInfo(sourceFile).baseName() + QStringLiteral(".ktx" )); |
| 697 | |
| 698 | QString error = renderToKTXFile(inPath: sourceFile, outPath: outFileName); |
| 699 | if (!error.isEmpty()) |
| 700 | return error; |
| 701 | |
| 702 | m_generatedFiles.append(t: outFileName); |
| 703 | |
| 704 | if (generatedFiles) |
| 705 | *generatedFiles = m_generatedFiles; |
| 706 | |
| 707 | return QString(); |
| 708 | } |
| 709 | |
| 710 | QT_END_NAMESPACE |
| 711 | |