Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qssgrenderskymaterial.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6#include <QtQuick3DRuntimeRender/private/qssgrenderskymaterial_p.h>
7#include <QtQuick3DRuntimeRender/private/qssgrendererimplshaders_p.h>
8#include <QtQuick3DRuntimeRender/private/qssgrhieffectsystem_p.h>
9#include <QtQuick3DRuntimeRender/private/qssgshadermaterialadapter_p.h>
10
11#include <QtCore/qhash.h>
12#include <QtCore/qspan.h>
13#include <QtGui/qvector4d.h>
14#include <rhi/qshaderbaker.h>
15
16#include <algorithm>
17
18using namespace Qt::StringLiterals;
19
21
22static const char *vertexShaderStr = R"(
23void main()
24{
25 vec4 qt_vertPosition = vec4(attr_pos, 1.0);
26 qt_eyeDir = qt_vertPosition.xyz;
27 gl_Position = qt_projectionMatrix * qt_viewMatrix * vec4(qt_eyeDir, 1.0);
28}
29
30)";
31
32// Screen-space variant: the sky shader is evaluated directly on a fullscreen quad
33// for the visible background (no cubemap round-trip). The per-pixel world-space view
34// direction is reconstructed exactly like skybox.vert: inverse-project the NDC position
35// into view space, then rotate into world space. qt_viewMatrix here carries the combined
36// orientation * world-rotation (translation-free), so a single matrix suffices.
37static const char *vertexShaderStrScreen = R"(
38void main()
39{
40 gl_Position = vec4(attr_pos, 1.0);
41#if QSHADER_VIEW_COUNT >= 2
42 vec3 qt_unprojected = (qt_inverseProjection[gl_ViewIndex] * gl_Position).xyz;
43 qt_eyeDir = (qt_viewMatrix[gl_ViewIndex] * vec4(qt_unprojected, 0.0)).xyz;
44#else
45 vec3 qt_unprojected = (qt_inverseProjection * gl_Position).xyz;
46 qt_eyeDir = (qt_viewMatrix * vec4(qt_unprojected, 0.0)).xyz;
47#endif
48 gl_Position.y *= qt_adjustY;
49}
50
51)";
52
53static const char *mainFragmentSnippet = R"(
54
55void main()
56{
57 qt_customMain();
58}
59
60)";
61
62// Screen-space (background) fragment main: the user shader writes linear HDR into
63// FRAGCOLOR, so we apply exposure + tonemapping here, exactly as skybox.frag does when
64// it samples the IBL cube. qt_exposure / qt_tonemap are no-ops unless a tonemapping
65// feature is compiled in (TonemapMode::None/Custom -> passthrough), matching the skybox.
66static const char *mainFragmentSnippetScreen = R"(
67
68void main()
69{
70 qt_customMain();
71 FRAGCOLOR = vec4(qt_tonemap(qt_exposure(FRAGCOLOR.rgb, qt_skyExposure)), 1.0);
72}
73
74)";
75
76static const char *debugFragStr = R"(
77// Helper: outer square border of a face
78float line(vec2 uv, float width)
79{
80 vec2 d = abs(uv - 0.5);
81 return step(max(d.x, d.y), 0.5) - step(max(d.x, d.y), 0.5 - width);
82}
83
84// Simple plus/minus symbol
85float drawPlus(vec2 uv)
86{
87 float h = step(abs(uv.y - 0.5), 0.05) * step(abs(uv.x - 0.5), 0.2);
88 float v = step(abs(uv.x - 0.5), 0.05) * step(abs(uv.y - 0.5), 0.2);
89 return max(h, v);
90}
91
92float drawMinus(vec2 uv)
93{
94 return step(abs(uv.y - 0.5), 0.05) * step(abs(uv.x - 0.5), 0.2);
95}
96
97void MAIN()
98{
99 vec3 d = normalize(qt_eyeDir);
100 vec3 ad = abs(d);
101
102 vec2 uv;
103 vec3 faceColor;
104 float label = 0.0;
105
106 // Determine dominant axis (cubemap face)
107 if (ad.x > ad.y && ad.x > ad.z)
108 {
109 uv = d.zy / ad.x * 0.5 + 0.5;
110 if (d.x > 0.0)
111 {
112 faceColor = vec3(1.0, 0.0, 0.0); // +X red
113 label = drawPlus(uv);
114 }
115 else
116 {
117 faceColor = vec3(0.5, 0.0, 0.0); // -X dark red
118 label = drawMinus(uv);
119 }
120 }
121 else if (ad.y > ad.x && ad.y > ad.z)
122 {
123 uv = d.xz / ad.y * 0.5 + 0.5;
124 if (d.y > 0.0)
125 {
126 faceColor = vec3(0.0, 1.0, 0.0); // +Y green
127 label = drawPlus(uv);
128 }
129 else
130 {
131 faceColor = vec3(0.0, 0.5, 0.0); // -Y dark green
132 label = drawMinus(uv);
133 }
134 }
135 else
136 {
137 uv = d.xy / ad.z * 0.5 + 0.5;
138 if (d.z > 0.0)
139 {
140 faceColor = vec3(0.0, 0.0, 1.0); // +Z blue
141 label = drawPlus(uv);
142 }
143 else
144 {
145 faceColor = vec3(0.0, 0.0, 0.5); // -Z dark blue
146 label = drawMinus(uv);
147 }
148 }
149
150 // Outer border
151 float border = line(uv, 0.02);
152
153 // Combine
154 vec3 color = faceColor;
155 color = mix(color, vec3(1.0), border); // white border
156 color = mix(color, vec3(1.0), label); // white + or - label
157
158 FRAGCOLOR = vec4(color, 1.0);
159}
160)";
161
162QSSGRenderSkyMaterial::QSSGRenderSkyMaterial() : QSSGRenderGraphObject(QSSGRenderGraphObject::Type::SkyMaterial) { }
163
164QSSGRenderSkyMaterial::~QSSGRenderSkyMaterial() = default;
165
166// Returns false if any custom-property texture is not ready yet (compilation must wait
167// until next frame, since the GLSL sampler type depends on the actual texture: textures
168// not created yet are assumed to be sampler2DArray, which could cause compile failures).
169static bool skyShaderTexturesReady(const QList<QSSGBaseTypeProperty> &propertyUniforms, QSSGBufferManager &bufferManager)
170{
171 for (const auto &u : std::as_const(propertyUniforms)) {
172 if (u.shaderDataType == QSSGRenderShaderValue::Texture) {
173 QSSGRenderImage *image = u.value.value<QSSGRenderImage *>();
174 const QSSGRenderImageTexture texture = image ? bufferManager.loadRenderImage(image) : QSSGRenderImageTexture { };
175 if (!image || !texture.m_texture)
176 return false;
177 }
178 }
179 return true;
180}
181
182// Appends the user shader's custom-property texture bindings to an SRB list. Shared by the
183// cube (updateUniforms) and screen-space (updateBackgroundUniforms) paths.
184static void appendCustomTextureBindings(QSSGRhiShaderPipeline *pipeline,
185 QSSGRhiShaderResourceBindingList &bindings,
186 QSSGRhiContext *rhiCtx)
187{
188 int maxSamplerBinding = -1;
189 QVector<QShaderDescription::InOutVariable> samplerVars =
190 pipeline->fragmentStage()->shader().description().combinedImageSamplers();
191 for (const QShaderDescription::InOutVariable &var :
192 pipeline->vertexStage()->shader().description().combinedImageSamplers()) {
193 auto it = std::find_if(samplerVars.cbegin(), samplerVars.cend(), [&var](const QShaderDescription::InOutVariable &v) {
194 return var.binding == v.binding;
195 });
196 if (it == samplerVars.cend())
197 samplerVars.append(var);
198 }
199 for (const QShaderDescription::InOutVariable &var : std::as_const(samplerVars))
200 maxSamplerBinding = qMax(maxSamplerBinding, var.binding);
201
202 if (maxSamplerBinding < 0)
203 return;
204
205 const int customTexCount = pipeline->extraTextureCount();
206 for (int i = 0; i < customTexCount; ++i) {
207 const QSSGRhiTexture &t(pipeline->extraTextureAt(i));
208 const int samplerBinding = pipeline->bindingForTexture(t.name);
209 if (samplerBinding >= 0) {
210 QRhiSampler *sampler = rhiCtx->sampler(t.samplerDesc);
211 bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::FragmentStage, t.texture, sampler);
212 }
213 }
214}
215
216// Compiles a sky pipeline from the shared user fragment shader and a given vertex
217// variant. The user fragment is identical for the cube (IBL) and screen (background)
218// paths since it only reads qt_eyeDir; the stages differ in the vertex code and uniforms
219// and in the fragment main snippet (the screen path tonemaps the linear output for
220// display). cacheKeyTag keeps the variants from aliasing in the shader cache.
221QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::buildPipeline(const QSSGRenderContextInterface &sgContext,
222 QByteArray vertexShader,
223 const QSSGShaderCustomMaterialAdapter::StringPairList &vertexViewDependentUniforms,
224 const QSSGShaderCustomMaterialAdapter::StringPairList &vertexUniforms,
225 const QByteArray &fragmentMainSnippet,
226 const QSSGShaderCustomMaterialAdapter::StringPairList &fragmentUniforms,
227 const QSSGShaderFeatures &features,
228 int viewCount,
229 const QByteArray &cacheKeyTag)
230{
231 const bool multiViewCompatible = viewCount >= 2;
232
233 QByteArray fragmentShader = (!fragmentShaderSource.isEmpty() ? fragmentShaderSource : QByteArray(debugFragStr)) + fragmentMainSnippet;
234
235 QSSGShaderCustomMaterialAdapter::StringPairList propertyBaseUniforms;
236 for (const auto &u : std::as_const(propertyUniforms))
237 propertyBaseUniforms.append({ u.typeName, u.name });
238
239 QSSGShaderCustomMaterialAdapter::StringPairList inputOutputs;
240 inputOutputs.append({ "vec3", "qt_eyeDir"_ba });
241
242 {
243 QSSGShaderCustomMaterialAdapter::StringPairList vertexBaseUniforms = propertyBaseUniforms;
244 vertexBaseUniforms.append(vertexUniforms.constData(), vertexUniforms.size());
245
246 QSSGShaderCustomMaterialAdapter::ShaderCodeAndMetaData result;
247 QByteArray buf;
248 QSSGShaderCustomMaterialAdapter::CustomShaderPrepWorkData scratch;
249 QSSGShaderCustomMaterialAdapter::beginPrepareCustomShader(&scratch, &result, vertexShader, QSSGShaderCache::ShaderType::Vertex, multiViewCompatible);
250 QSSGShaderCustomMaterialAdapter::finishPrepareCustomShader(&buf,
251 scratch,
252 result,
253 QSSGShaderCache::ShaderType::Vertex,
254 multiViewCompatible,
255 vertexBaseUniforms,
256 { },
257 inputOutputs,
258 { },
259 vertexViewDependentUniforms);
260 vertexShader = result.first;
261 vertexShader.append(buf);
262 }
263
264 {
265 // Fragment uniforms (e.g. qt_skyExposure) are view-independent.
266 QSSGShaderCustomMaterialAdapter::StringPairList fragmentBaseUniforms = propertyBaseUniforms;
267 fragmentBaseUniforms.append(fragmentUniforms.constData(), fragmentUniforms.size());
268
269 QSSGShaderCustomMaterialAdapter::ShaderCodeAndMetaData result;
270 QByteArray buf;
271 QSSGShaderCustomMaterialAdapter::CustomShaderPrepWorkData scratch;
272 QSSGShaderCustomMaterialAdapter::beginPrepareCustomShader(&scratch, &result, fragmentShader, QSSGShaderCache::ShaderType::Fragment, multiViewCompatible);
273 QSSGShaderCustomMaterialAdapter::finishPrepareCustomShader(&buf,
274 scratch,
275 result,
276 QSSGShaderCache::ShaderType::Fragment,
277 multiViewCompatible,
278 fragmentBaseUniforms,
279 inputOutputs,
280 { },
281 { },
282 { });
283 fragmentShader = result.first;
284 fragmentShader.append(buf);
285 }
286
287 auto generator = sgContext.shaderProgramGenerator().get();
288 auto shaderLib = sgContext.shaderLibraryManager().get();
289 auto shaderCache = sgContext.shaderCache().get();
290
291 generator->beginProgram();
292 auto vertex = generator->getStage(QSSGShaderGeneratorStage::Vertex);
293 vertex->addIncoming("attr_pos"_ba, "vec3"_ba);
294 vertex->append(vertexShader);
295
296 auto fragment = generator->getStage(QSSGShaderGeneratorStage::Fragment);
297 fragment->addInclude("tonemapping.glsllib"_ba);
298 fragment->append(fragmentShader);
299
300 const QByteArray key = shaderPathKey + cacheKeyTag + ':'
301 + QCryptographicHash::hash(QByteArray(vertexShader + fragmentShader), QCryptographicHash::Algorithm::Sha1).toHex();
302 return generator->compileGeneratedRhiShader(key, features, *shaderLib, *shaderCache, QSSGRhiShaderPipeline::UsedWithoutIa, { }, viewCount, false);
303}
304
305QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::ensurePipeline(const QSSGRenderContextInterface &sgContext)
306{
307 if (iblPassPipeline && !isFragmentShaderDirty)
308 return iblPassPipeline;
309
310 if (!skyShaderTexturesReady(propertyUniforms, *sgContext.bufferManager().get()))
311 return nullptr;
312
313 QSSGShaderCustomMaterialAdapter::StringPairList vertexViewDependentUniforms;
314 vertexViewDependentUniforms.append({ "mat4"_ba, "qt_projectionMatrix"_ba });
315 vertexViewDependentUniforms.append({ "mat4"_ba, "qt_viewMatrix"_ba });
316
317 // The cube render stores linear radiance for IBL, so no tonemapping is applied
318 // (AcesTonemapping is set only to satisfy the tonemapping.glsllib include; the cube
319 // fragment never calls qt_tonemap).
320 QSSGShaderFeatures features;
321 features.set(QSSGShaderFeatures::Feature::AcesTonemapping, true);
322
323 iblPassPipeline = buildPipeline(sgContext, vertexShaderStr, vertexViewDependentUniforms, { }, mainFragmentSnippet, { }, features, 1, QByteArrayLiteral(":cube"));
324
325 isFragmentShaderDirty = false;
326
327 return iblPassPipeline;
328}
329
330QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::ensureBackgroundPipeline(const QSSGRenderContextInterface &sgContext,
331 const QSSGShaderFeatures &tonemapFeatures,
332 quint32 tonemapKey,
333 int viewCount)
334{
335 if (backgroundPipeline && !isBackgroundShaderDirty && m_backgroundTonemapKey == tonemapKey
336 && m_backgroundViewCount == viewCount) {
337 return backgroundPipeline;
338 }
339
340 if (!skyShaderTexturesReady(propertyUniforms, *sgContext.bufferManager().get()))
341 return nullptr;
342
343 QSSGShaderCustomMaterialAdapter::StringPairList vertexViewDependentUniforms;
344 vertexViewDependentUniforms.append({ "mat4"_ba, "qt_inverseProjection"_ba });
345 vertexViewDependentUniforms.append({ "mat4"_ba, "qt_viewMatrix"_ba });
346
347 QSSGShaderCustomMaterialAdapter::StringPairList vertexUniforms;
348 vertexUniforms.append({ "float"_ba, "qt_adjustY"_ba });
349
350 QSSGShaderCustomMaterialAdapter::StringPairList fragmentUniforms;
351 fragmentUniforms.append({ "float"_ba, "qt_skyExposure"_ba });
352
353 backgroundPipeline = buildPipeline(sgContext, vertexShaderStrScreen, vertexViewDependentUniforms, vertexUniforms,
354 mainFragmentSnippetScreen, fragmentUniforms, tonemapFeatures, viewCount,
355 QByteArrayLiteral(":screen:") + QByteArray::number(tonemapKey)
356 + ':' + QByteArray::number(viewCount));
357
358 m_backgroundTonemapKey = tonemapKey;
359 m_backgroundViewCount = viewCount;
360 isBackgroundShaderDirty = false;
361
362 return backgroundPipeline;
363}
364
365quint32 QSSGRenderSkyMaterial::updateUniforms(const QSSGRenderContextInterface &sgContext,
366 const QMatrix4x4 &mvp,
367 const QVarLengthArray<QMatrix4x4, 6> views)
368{
369 constexpr int cMatrixSize = 64;
370
371 if (!iblPassPipeline)
372 return 0;
373
374 QSSGRhiContext *rhiCtx = sgContext.rhiContext().get();
375 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
376 QSSGRhiDrawCallData *dcd = &rhiCtxD->drawCallData({ (void *)this, nullptr, nullptr, 0 });
377
378 const int uniformStride = rhiCtx->rhi()->ubufAligned(iblPassPipeline->ub0Size());
379 const int totalBufferSize = uniformStride * 6;
380
381 if (!dcd->ubuf) {
382 dcd->ubuf = rhiCtx->rhi()->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, totalBufferSize);
383 dcd->ubuf->create();
384 }
385
386 char *ubufData = dcd->ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
387 auto bufferManager = sgContext.bufferManager().get();
388
389 iblPassPipeline->resetExtraTextures();
390
391 for (const auto &u : std::as_const(propertyUniforms))
392 iblPassPipeline->setShaderResources(ubufData, *bufferManager, u.name, u.value, u.shaderDataType);
393
394 // same for all faces
395 iblPassPipeline->setShaderResources(ubufData, *bufferManager, "qt_projectionMatrix"_ba, mvp, QSSGRenderShaderValue::Type::Matrix4x4);
396 iblPassPipeline->setShaderResources(ubufData, *bufferManager, "qt_viewMatrix"_ba, views[0], QSSGRenderShaderValue::Type::Matrix4x4);
397
398 // Kinda hacky way of filling out all the views
399 // Copy first properties then clone these six times (each face)
400 // view matrix is the only unique property per face
401 const int viewMatrixOffset = iblPassPipeline->offsetOfUniform("qt_viewMatrix"_ba);
402
403 Q_ASSERT(ubufData != nullptr);
404 Q_ASSERT(totalBufferSize >= 6 * uniformStride); // buffer must hold all 6 faces
405 Q_ASSERT(uniformStride >= cMatrixSize); // view matrix fits
406 Q_ASSERT(viewMatrixOffset >= 0);
407 Q_ASSERT(viewMatrixOffset + cMatrixSize <= uniformStride);
408
409 auto buffer = QSpan<char>(ubufData, totalBufferSize);
410 auto firstFace = buffer.first(uniformStride);
411
412 for (int face = 1; face < 6; ++face) {
413 auto dst = buffer.sliced(face * uniformStride, uniformStride);
414
415 // Copy the entire first face block
416 std::copy(firstFace.begin(), firstFace.end(), dst.begin());
417
418 // Copy only the per-face view matrix
419 std::copy_n(reinterpret_cast<const char *>(views[face].constData()), cMatrixSize, dst.data() + viewMatrixOffset);
420 }
421 dcd->ubuf->endFullDynamicBufferUpdateForCurrentFrame();
422
423 rhiCtxD->releaseCachedSrb(bindings);
424 bindings = QSSGRhiShaderResourceBindingList();
425 bindings.addUniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, dcd->ubuf, 0, uniformStride, true);
426 appendCustomTextureBindings(iblPassPipeline.get(), bindings, rhiCtx);
427
428 return uniformStride;
429}
430
431void QSSGRenderSkyMaterial::updateBackgroundUniforms(const QSSGRenderContextInterface &sgContext,
432 const QVarLengthArray<QMatrix4x4, 2> &inverseProjections,
433 const QVarLengthArray<QMatrix4x4, 2> &viewRotations,
434 float adjustY,
435 float exposure)
436{
437 if (!backgroundPipeline)
438 return;
439
440 QSSGRhiContext *rhiCtx = sgContext.rhiContext().get();
441 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
442 // Distinct drawCallData key index (1) so the background UBO does not collide with
443 // the per-face IBL UBO (index 0) held for the same QSSGRenderSkyMaterial.
444 QSSGRhiDrawCallData *dcd = &rhiCtxD->drawCallData({ (void *)this, nullptr, nullptr, 1 });
445
446 const int bufferSize = rhiCtx->rhi()->ubufAligned(backgroundPipeline->ub0Size());
447
448 if (!dcd->ubuf || int(dcd->ubuf->size()) != bufferSize) {
449 delete dcd->ubuf;
450 dcd->ubuf = rhiCtx->rhi()->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, bufferSize);
451 dcd->ubuf->create();
452 }
453
454 char *ubufData = dcd->ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
455 auto bufferManager = sgContext.bufferManager().get();
456
457 backgroundPipeline->resetExtraTextures();
458
459 for (const auto &u : std::as_const(propertyUniforms))
460 backgroundPipeline->setShaderResources(ubufData, *bufferManager, u.name, u.value, u.shaderDataType);
461
462 backgroundPipeline->setShaderResources(ubufData, *bufferManager, "qt_adjustY"_ba, adjustY, QSSGRenderShaderValue::Type::Float);
463 backgroundPipeline->setShaderResources(ubufData, *bufferManager, "qt_skyExposure"_ba, exposure, QSSGRenderShaderValue::Type::Float);
464
465 constexpr int matSize = 64;
466 const int invProjOffset = backgroundPipeline->offsetOfUniform("qt_inverseProjection"_ba);
467 const int viewMatOffset = backgroundPipeline->offsetOfUniform("qt_viewMatrix"_ba);
468 const int viewCount = int(inverseProjections.size());
469 for (int v = 0; v < viewCount; ++v) {
470 if (invProjOffset >= 0)
471 std::copy_n(reinterpret_cast<const char *>(inverseProjections[v].constData()), matSize, ubufData + invProjOffset + v * matSize);
472 if (viewMatOffset >= 0)
473 std::copy_n(reinterpret_cast<const char *>(viewRotations[v].constData()), matSize, ubufData + viewMatOffset + v * matSize);
474 }
475
476 dcd->ubuf->endFullDynamicBufferUpdateForCurrentFrame();
477
478 rhiCtxD->releaseCachedSrb(backgroundBindings);
479 backgroundBindings = QSSGRhiShaderResourceBindingList();
480 backgroundBindings.addUniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, dcd->ubuf);
481 appendCustomTextureBindings(backgroundPipeline.get(), backgroundBindings, rhiCtx);
482}
483
484QT_END_NAMESPACE
static const char * mainFragmentSnippetScreen
static void appendCustomTextureBindings(QSSGRhiShaderPipeline *pipeline, QSSGRhiShaderResourceBindingList &bindings, QSSGRhiContext *rhiCtx)
static const char * mainFragmentSnippet
static QT_BEGIN_NAMESPACE const char * vertexShaderStr
static const char * vertexShaderStrScreen
static bool skyShaderTexturesReady(const QList< QSSGBaseTypeProperty > &propertyUniforms, QSSGBufferManager &bufferManager)
static const char * debugFragStr