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 | |