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()
163 : QSSGRenderGraphObject(QSSGRenderGraphObject::Type::SkyMaterial, FlagT(Flags::HasGraphicsResources))
164{
165}
166
167QSSGRenderSkyMaterial::~QSSGRenderSkyMaterial() = default;
168
169// Returns false if any custom-property texture is not ready yet (compilation must wait
170// until next frame, since the GLSL sampler type depends on the actual texture: textures
171// not created yet are assumed to be sampler2DArray, which could cause compile failures).
172static bool skyShaderTexturesReady(const QList<QSSGBaseTypeProperty> &propertyUniforms, QSSGBufferManager &bufferManager)
173{
174 for (const auto &u : std::as_const(propertyUniforms)) {
175 if (u.shaderDataType == QSSGRenderShaderValue::Texture) {
176 QSSGRenderImage *image = u.value.value<QSSGRenderImage *>();
177 const QSSGRenderImageTexture texture = image ? bufferManager.loadRenderImage(image) : QSSGRenderImageTexture { };
178 if (!image || !texture.m_texture)
179 return false;
180 }
181 }
182 return true;
183}
184
185// Appends the user shader's custom-property texture bindings to an SRB list. Shared by the
186// cube (updateUniforms) and screen-space (updateBackgroundUniforms) paths.
187static void appendCustomTextureBindings(QSSGRhiShaderPipeline *pipeline,
188 QSSGRhiShaderResourceBindingList &bindings,
189 QSSGRhiContext *rhiCtx)
190{
191 int maxSamplerBinding = -1;
192 QVector<QShaderDescription::InOutVariable> samplerVars =
193 pipeline->fragmentStage()->shader().description().combinedImageSamplers();
194 for (const QShaderDescription::InOutVariable &var :
195 pipeline->vertexStage()->shader().description().combinedImageSamplers()) {
196 auto it = std::find_if(samplerVars.cbegin(), samplerVars.cend(), [&var](const QShaderDescription::InOutVariable &v) {
197 return var.binding == v.binding;
198 });
199 if (it == samplerVars.cend())
200 samplerVars.append(var);
201 }
202 for (const QShaderDescription::InOutVariable &var : std::as_const(samplerVars))
203 maxSamplerBinding = qMax(maxSamplerBinding, var.binding);
204
205 if (maxSamplerBinding < 0)
206 return;
207
208 const int customTexCount = pipeline->extraTextureCount();
209 for (int i = 0; i < customTexCount; ++i) {
210 const QSSGRhiTexture &t(pipeline->extraTextureAt(i));
211 const int samplerBinding = pipeline->bindingForTexture(t.name);
212 if (samplerBinding >= 0) {
213 QRhiSampler *sampler = rhiCtx->sampler(t.samplerDesc);
214 bindings.addTexture(samplerBinding, QRhiShaderResourceBinding::FragmentStage, t.texture, sampler);
215 }
216 }
217}
218
219// Compiles a sky pipeline from the shared user fragment shader and a given vertex
220// variant. The user fragment is identical for the cube (IBL) and screen (background)
221// paths since it only reads qt_eyeDir; the stages differ in the vertex code and uniforms
222// and in the fragment main snippet (the screen path tonemaps the linear output for
223// display). cacheKeyTag keeps the variants from aliasing in the shader cache.
224QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::buildPipeline(const QSSGRenderContextInterface &sgContext,
225 QByteArray vertexShader,
226 const QSSGShaderCustomMaterialAdapter::StringPairList &vertexViewDependentUniforms,
227 const QSSGShaderCustomMaterialAdapter::StringPairList &vertexUniforms,
228 const QByteArray &fragmentMainSnippet,
229 const QSSGShaderCustomMaterialAdapter::StringPairList &fragmentUniforms,
230 const QSSGShaderFeatures &features,
231 int viewCount,
232 const QByteArray &cacheKeyTag)
233{
234 const bool multiViewCompatible = viewCount >= 2;
235
236 QByteArray fragmentShader = (!fragmentShaderSource.isEmpty() ? fragmentShaderSource : QByteArray(debugFragStr)) + fragmentMainSnippet;
237
238 QSSGShaderCustomMaterialAdapter::StringPairList propertyBaseUniforms;
239 for (const auto &u : std::as_const(propertyUniforms))
240 propertyBaseUniforms.append({ u.typeName, u.name });
241
242 QSSGShaderCustomMaterialAdapter::StringPairList inputOutputs;
243 inputOutputs.append({ "vec3", "qt_eyeDir"_ba });
244
245 {
246 QSSGShaderCustomMaterialAdapter::StringPairList vertexBaseUniforms = propertyBaseUniforms;
247 vertexBaseUniforms.append(vertexUniforms.constData(), vertexUniforms.size());
248
249 QSSGShaderCustomMaterialAdapter::ShaderCodeAndMetaData result;
250 QByteArray buf;
251 QSSGShaderCustomMaterialAdapter::CustomShaderPrepWorkData scratch;
252 QSSGShaderCustomMaterialAdapter::beginPrepareCustomShader(&scratch, &result, vertexShader, QSSGShaderCache::ShaderType::Vertex, multiViewCompatible);
253 QSSGShaderCustomMaterialAdapter::finishPrepareCustomShader(&buf,
254 scratch,
255 result,
256 QSSGShaderCache::ShaderType::Vertex,
257 multiViewCompatible,
258 vertexBaseUniforms,
259 { },
260 inputOutputs,
261 { },
262 vertexViewDependentUniforms);
263 vertexShader = result.first;
264 vertexShader.append(buf);
265 }
266
267 {
268 // Fragment uniforms (e.g. qt_skyExposure) are view-independent.
269 QSSGShaderCustomMaterialAdapter::StringPairList fragmentBaseUniforms = propertyBaseUniforms;
270 fragmentBaseUniforms.append(fragmentUniforms.constData(), fragmentUniforms.size());
271
272 QSSGShaderCustomMaterialAdapter::ShaderCodeAndMetaData result;
273 QByteArray buf;
274 QSSGShaderCustomMaterialAdapter::CustomShaderPrepWorkData scratch;
275 QSSGShaderCustomMaterialAdapter::beginPrepareCustomShader(&scratch, &result, fragmentShader, QSSGShaderCache::ShaderType::Fragment, multiViewCompatible);
276 QSSGShaderCustomMaterialAdapter::finishPrepareCustomShader(&buf,
277 scratch,
278 result,
279 QSSGShaderCache::ShaderType::Fragment,
280 multiViewCompatible,
281 fragmentBaseUniforms,
282 inputOutputs,
283 { },
284 { },
285 { });
286 fragmentShader = result.first;
287 fragmentShader.append(buf);
288 }
289
290 auto generator = sgContext.shaderProgramGenerator().get();
291 auto shaderLib = sgContext.shaderLibraryManager().get();
292 auto shaderCache = sgContext.shaderCache().get();
293
294 generator->beginProgram();
295 auto vertex = generator->getStage(QSSGShaderGeneratorStage::Vertex);
296 vertex->addIncoming("attr_pos"_ba, "vec3"_ba);
297 vertex->append(vertexShader);
298
299 auto fragment = generator->getStage(QSSGShaderGeneratorStage::Fragment);
300 fragment->addInclude("tonemapping.glsllib"_ba);
301 fragment->append(fragmentShader);
302
303 const QByteArray key = shaderPathKey + cacheKeyTag + ':'
304 + QCryptographicHash::hash(QByteArray(vertexShader + fragmentShader), QCryptographicHash::Algorithm::Sha1).toHex();
305 return generator->compileGeneratedRhiShader(key, features, *shaderLib, *shaderCache, QSSGRhiShaderPipeline::UsedWithoutIa, { }, viewCount, false);
306}
307
308QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::ensurePipeline(const QSSGRenderContextInterface &sgContext)
309{
310 if (iblPassPipeline && !isFragmentShaderDirty)
311 return iblPassPipeline;
312
313 if (!skyShaderTexturesReady(propertyUniforms, *sgContext.bufferManager().get()))
314 return nullptr;
315
316 QSSGShaderCustomMaterialAdapter::StringPairList vertexViewDependentUniforms;
317 vertexViewDependentUniforms.append({ "mat4"_ba, "qt_projectionMatrix"_ba });
318 vertexViewDependentUniforms.append({ "mat4"_ba, "qt_viewMatrix"_ba });
319
320 // The cube render stores linear radiance for IBL, so no tonemapping is applied
321 // (AcesTonemapping is set only to satisfy the tonemapping.glsllib include; the cube
322 // fragment never calls qt_tonemap).
323 QSSGShaderFeatures features;
324 features.set(QSSGShaderFeatures::Feature::AcesTonemapping, true);
325
326 iblPassPipeline = buildPipeline(sgContext, vertexShaderStr, vertexViewDependentUniforms, { }, mainFragmentSnippet, { }, features, 1, QByteArrayLiteral(":cube"));
327
328 isFragmentShaderDirty = false;
329
330 return iblPassPipeline;
331}
332
333QSSGRhiShaderPipelinePtr QSSGRenderSkyMaterial::ensureBackgroundPipeline(const QSSGRenderContextInterface &sgContext,
334 const QSSGShaderFeatures &tonemapFeatures,
335 quint32 tonemapKey,
336 int viewCount)
337{
338 if (backgroundPipeline && !isBackgroundShaderDirty && m_backgroundTonemapKey == tonemapKey
339 && m_backgroundViewCount == viewCount) {
340 return backgroundPipeline;
341 }
342
343 if (!skyShaderTexturesReady(propertyUniforms, *sgContext.bufferManager().get()))
344 return nullptr;
345
346 QSSGShaderCustomMaterialAdapter::StringPairList vertexViewDependentUniforms;
347 vertexViewDependentUniforms.append({ "mat4"_ba, "qt_inverseProjection"_ba });
348 vertexViewDependentUniforms.append({ "mat4"_ba, "qt_viewMatrix"_ba });
349
350 QSSGShaderCustomMaterialAdapter::StringPairList vertexUniforms;
351 vertexUniforms.append({ "float"_ba, "qt_adjustY"_ba });
352
353 QSSGShaderCustomMaterialAdapter::StringPairList fragmentUniforms;
354 fragmentUniforms.append({ "float"_ba, "qt_skyExposure"_ba });
355
356 backgroundPipeline = buildPipeline(sgContext, vertexShaderStrScreen, vertexViewDependentUniforms, vertexUniforms,
357 mainFragmentSnippetScreen, fragmentUniforms, tonemapFeatures, viewCount,
358 QByteArrayLiteral(":screen:") + QByteArray::number(tonemapKey)
359 + ':' + QByteArray::number(viewCount));
360
361 m_backgroundTonemapKey = tonemapKey;
362 m_backgroundViewCount = viewCount;
363 isBackgroundShaderDirty = false;
364
365 return backgroundPipeline;
366}
367
368quint32 QSSGRenderSkyMaterial::updateUniforms(const QSSGRenderContextInterface &sgContext,
369 QSSGPassKey passKey,
370 const QMatrix4x4 &mvp,
371 const QVarLengthArray<QMatrix4x4, 6> views)
372{
373 constexpr int cMatrixSize = 64;
374
375 if (!iblPassPipeline)
376 return 0;
377
378 QSSGRhiContext *rhiCtx = sgContext.rhiContext().get();
379 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
380 // Keyed on the driving pass (the layer's sky material pass) and the
381 // material; the material must be the key's resource so that
382 // cleanupDrawCallDataForResource() finds the entry when it is destroyed.
383 QSSGRhiDrawCallData *dcd = &rhiCtxD->drawCallData({ passKey, nullptr, this, nullptr, 0 });
384
385 const int uniformStride = rhiCtx->rhi()->ubufAligned(iblPassPipeline->ub0Size());
386 const int totalBufferSize = uniformStride * 6;
387
388 if (!dcd->ubuf) {
389 dcd->ubuf = rhiCtx->rhi()->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, totalBufferSize);
390 dcd->ubuf->create();
391 }
392
393 char *ubufData = dcd->ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
394 auto bufferManager = sgContext.bufferManager().get();
395
396 iblPassPipeline->resetExtraTextures();
397
398 for (const auto &u : std::as_const(propertyUniforms))
399 iblPassPipeline->setShaderResources(ubufData, *bufferManager, u.name, u.value, u.shaderDataType);
400
401 // same for all faces
402 iblPassPipeline->setShaderResources(ubufData, *bufferManager, "qt_projectionMatrix"_ba, mvp, QSSGRenderShaderValue::Type::Matrix4x4);
403 iblPassPipeline->setShaderResources(ubufData, *bufferManager, "qt_viewMatrix"_ba, views[0], QSSGRenderShaderValue::Type::Matrix4x4);
404
405 // Kinda hacky way of filling out all the views
406 // Copy first properties then clone these six times (each face)
407 // view matrix is the only unique property per face
408 const int viewMatrixOffset = iblPassPipeline->offsetOfUniform("qt_viewMatrix"_ba);
409
410 Q_ASSERT(ubufData != nullptr);
411 Q_ASSERT(totalBufferSize >= 6 * uniformStride); // buffer must hold all 6 faces
412 Q_ASSERT(uniformStride >= cMatrixSize); // view matrix fits
413 Q_ASSERT(viewMatrixOffset >= 0);
414 Q_ASSERT(viewMatrixOffset + cMatrixSize <= uniformStride);
415
416 auto buffer = QSpan<char>(ubufData, totalBufferSize);
417 auto firstFace = buffer.first(uniformStride);
418
419 for (int face = 1; face < 6; ++face) {
420 auto dst = buffer.sliced(face * uniformStride, uniformStride);
421
422 // Copy the entire first face block
423 std::copy(firstFace.begin(), firstFace.end(), dst.begin());
424
425 // Copy only the per-face view matrix
426 std::copy_n(reinterpret_cast<const char *>(views[face].constData()), cMatrixSize, dst.data() + viewMatrixOffset);
427 }
428 dcd->ubuf->endFullDynamicBufferUpdateForCurrentFrame();
429
430 rhiCtxD->releaseCachedSrb(bindings);
431 bindings = QSSGRhiShaderResourceBindingList();
432 bindings.addUniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, dcd->ubuf, 0, uniformStride, true);
433 appendCustomTextureBindings(iblPassPipeline.get(), bindings, rhiCtx);
434
435 return uniformStride;
436}
437
438void QSSGRenderSkyMaterial::updateBackgroundUniforms(const QSSGRenderContextInterface &sgContext,
439 QSSGPassKey passKey,
440 const QVarLengthArray<QMatrix4x4, 2> &inverseProjections,
441 const QVarLengthArray<QMatrix4x4, 2> &viewRotations,
442 float adjustY,
443 float exposure)
444{
445 if (!backgroundPipeline)
446 return;
447
448 QSSGRhiContext *rhiCtx = sgContext.rhiContext().get();
449 QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx);
450 // Keyed on the driving pass (the layer's background pass) and the
451 // material; the material must be the key's resource so that
452 // cleanupDrawCallDataForResource() finds the entry when it is destroyed.
453 QSSGRhiDrawCallData *dcd = &rhiCtxD->drawCallData({ passKey, nullptr, this, nullptr, 0 });
454
455 const int bufferSize = rhiCtx->rhi()->ubufAligned(backgroundPipeline->ub0Size());
456
457 if (!dcd->ubuf || int(dcd->ubuf->size()) != bufferSize) {
458 delete dcd->ubuf;
459 dcd->ubuf = rhiCtx->rhi()->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, bufferSize);
460 dcd->ubuf->create();
461 }
462
463 char *ubufData = dcd->ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
464 auto bufferManager = sgContext.bufferManager().get();
465
466 backgroundPipeline->resetExtraTextures();
467
468 for (const auto &u : std::as_const(propertyUniforms))
469 backgroundPipeline->setShaderResources(ubufData, *bufferManager, u.name, u.value, u.shaderDataType);
470
471 backgroundPipeline->setShaderResources(ubufData, *bufferManager, "qt_adjustY"_ba, adjustY, QSSGRenderShaderValue::Type::Float);
472 backgroundPipeline->setShaderResources(ubufData, *bufferManager, "qt_skyExposure"_ba, exposure, QSSGRenderShaderValue::Type::Float);
473
474 constexpr int matSize = 64;
475 const int invProjOffset = backgroundPipeline->offsetOfUniform("qt_inverseProjection"_ba);
476 const int viewMatOffset = backgroundPipeline->offsetOfUniform("qt_viewMatrix"_ba);
477 const int viewCount = int(inverseProjections.size());
478 for (int v = 0; v < viewCount; ++v) {
479 if (invProjOffset >= 0)
480 std::copy_n(reinterpret_cast<const char *>(inverseProjections[v].constData()), matSize, ubufData + invProjOffset + v * matSize);
481 if (viewMatOffset >= 0)
482 std::copy_n(reinterpret_cast<const char *>(viewRotations[v].constData()), matSize, ubufData + viewMatOffset + v * matSize);
483 }
484
485 dcd->ubuf->endFullDynamicBufferUpdateForCurrentFrame();
486
487 rhiCtxD->releaseCachedSrb(backgroundBindings);
488 backgroundBindings = QSSGRhiShaderResourceBindingList();
489 backgroundBindings.addUniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, dcd->ubuf);
490 appendCustomTextureBindings(backgroundPipeline.get(), backgroundBindings, rhiCtx);
491}
492
493QT_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