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
qquick3dskymaterial.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
5#include <QtQuick3D/private/qquick3dskymaterial_p.h>
6#include <QtQuick3D/private/qquick3dobject_p.h>
7
8#include <QtQuick3DRuntimeRender/private/qssgrenderskymaterial_p.h>
9
10#include <QtCore/qmath.h>
11#include <QtGui/qquaternion.h>
12
13QT_BEGIN_NAMESPACE
14/*!
15 \qmltype SkyMaterial
16 \inqmlmodule QtQuick3D
17 \since 6.12
18 \brief Renders a procedural sky and generates image-based lighting data.
19
20 SkyMaterial renders a sky environment into a cubemap using custom
21 fragment shader code. The resulting cubemap can optionally be filtered
22 and used as image-based lighting (IBL) for physically based rendering.
23
24 The shader is evaluated for all six faces of the cubemap and may
25 implement arbitrary procedural skies, gradients, or analytical models.
26
27 The generated cubemap is used both as the visible scene background and
28 as the source for image-based lighting when enabled.
29
30 \note Changes to shader code or properties may take a frame or more
31 before being reflected in the generated IBL.
32
33 A ready-made SkyMaterial with a gradient sky, ground, and sun disk is
34 available as \l ProceduralSkyMaterial in the \l{Qt Quick 3D Helpers QML
35 Types}{QtQuick3D.Helpers} module. It can be used directly without writing
36 any shader code.
37
38 \section1 Shader
39
40 The shader has access to the built-in variable \c qt_eyeDir (\c vec3),
41 which provides the non-normalized direction vector from the center of
42 the cubemap to the current sample direction.
43
44 The fragment shader must define a \c MAIN() entry point, which is
45 executed for each fragment generated while rendering the cubemap.
46
47 The final pixel color must be written to \c FRAGCOLOR (\c vec4), which
48 is the output variable of the fragment shader.
49
50 A minimal shader example:
51
52 \badcode
53 void MAIN()
54 {
55 vec3 dir = normalize(qt_eyeDir);
56
57 // Output color (encoded direction as RGB)
58 FRAGCOLOR = vec4(dir * 0.5 + 0.5, 1.0);
59 }
60 \endcode
61
62 Custom QML properties declared on SkyMaterial are automatically exposed
63 as uniforms in the fragment shader. The property name must match the GLSL
64 variable name. No explicit uniform declaration is required.
65
66 The following type mappings are supported:
67
68 \table
69 \header
70 \li QML Type
71 \li Shader Type
72 \li Notes
73 \row \li real, int, bool \li float, int, bool \li
74 \row \li color \li vec4 \li sRGB is converted to linear space
75 \row \li vector2d, vector3d, vector4d \li vec2, vec3, vec4 \li
76 \row \li matrix4x4 \li mat4 \li
77 \row \li quaternion \li vec4 \li w stores scalar component
78 \row \li rect \li vec4 \li
79 \row \li point, size \li vec2 \li
80 \row \li Texture \li sampler2D, samplerCube, sampler3D
81 \li The sampler type follows the referenced texture: a \l CubeMapTexture
82 becomes a \c samplerCube, a \l Texture with 3D texture data becomes a
83 \c sampler3D, and any other \l Texture becomes a \c sampler2D.
84 \endtable
85
86 A texture property that has no texture set is not exposed to the shader at
87 all, so referencing it in the shader code results in a compilation error.
88
89 \sa SceneEnvironment::skyMaterial, ProceduralSkyMaterial
90*/
91
92/*!
93 \qmlproperty int SkyMaterial::radianceMapSize
94 \since 6.12
95 \default 512
96
97 Specifies the resolution of the generated environment cubemap used for
98 image-based lighting and reflection probes.
99
100 Higher values improve reflection sharpness and lighting quality,
101 especially for glossy materials, but increase memory usage and
102 rendering cost.
103
104 Values are snapped to the nearest power of two and clamped to the
105 range [8, 2048].
106
107 \note With \l skyboxMode set to a \c ScreenSpace value, the visible sky is
108 evaluated directly on screen and this property only affects the IBL/reflection
109 cubemap. With \c SkyMaterial.Cubemap it also determines the sharpness of the
110 visible background. When \l enableIBL is \c false, \l skyboxMode is a
111 \c ScreenSpace value, and no reflection probes are present, no cubemap is
112 generated at all.
113
114 \sa skyboxMode, enableIBL
115*/
116
117/*!
118 \qmlproperty url SkyMaterial::fragmentShader
119 \since 6.12
120
121 Specifies a fragment shader loaded from a file or resource URL.
122
123 If both \l fragmentShader and \l fragmentShaderCode are set,
124 \l fragmentShaderCode takes precedence.
125
126 If no shader is specified, a built-in debug shader is used.
127
128 \sa fragmentShaderCode
129*/
130
131/*!
132 \qmlproperty string SkyMaterial::fragmentShaderCode
133 \since 6.12
134
135 Specifies inline fragment shader source code.
136
137 This behaves identically to \l fragmentShader and uses the same
138 execution model, built-ins, and automatic property-to-uniform mapping.
139
140 If both \l fragmentShader and \l fragmentShaderCode are set,
141 this property takes precedence.
142
143 \sa fragmentShader
144*/
145
146/*!
147 \qmlproperty bool SkyMaterial::enableIBL
148 \since 6.12
149 \default true
150
151 Determines whether the generated sky cubemap is used for image-based
152 lighting.
153
154 When enabled, the engine generates filtered radiance and irradiance
155 maps used for ambient lighting and reflections in physically based
156 rendering.
157
158 When disabled, the sky is only rendered as a visual background and no
159 IBL preprocessing is performed.
160
161 \sa SceneEnvironment::lightProbe
162*/
163
164/*!
165 \qmlproperty int SkyMaterial::iblSampleCount
166 \since 6.12
167 \default 32
168
169 The total number of GGX importance samples evaluated per output texel
170 of the filtered radiance map. Higher values reduce shimmering on
171 high-frequency features such as a sun disk, at the cost of generation
172 time.
173
174 Values are clamped to the range [1, 1024].
175
176 \note This property has no effect when \l enableIBL is \c false.
177
178 \sa iblRenderFrames, enableIBL
179*/
180
181/*!
182 \qmlproperty int SkyMaterial::iblRenderFrames
183 \since 6.12
184 \default 0
185
186 Controls how the IBL prefilter work is spread across frames.
187
188 \list
189 \li \b 0 — Everything in one frame: sample accumulation, normalization, and
190 irradiance are all evaluated in the same frame.
191 \li \b {N ≥ 1} — The \l iblSampleCount samples are divided across N slice
192 frames (\c ceil(iblSampleCount / N) samples per frame), followed by a
193 dedicated normalization and irradiance frame.
194 \endlist
195
196 Higher values amortize the GGX prefilter cost across more frames, which is
197 useful when the sky changes infrequently and a high \l iblSampleCount is
198 required for quality.
199
200 \note This property has no effect when \l enableIBL is \c false.
201
202 \sa iblSampleCount, enableIBL
203*/
204
205/*!
206 \qmlproperty enumeration SkyMaterial::skyboxMode
207 \since 6.12
208 \default SkyMaterial.Cubemap
209
210 Selects what the SkyMaterial produces for the visible background.
211
212 \value SkyMaterial.Cubemap The background samples the radiance
213 cubemap (the same cube used for image-based lighting), whose resolution
214 follows \l radianceMapSize. For a static sky the cube is rendered once and
215 then sampled cheaply every frame, so this is the lowest per-frame cost when
216 nothing but the camera is moving. Sharpness is capped at \l radianceMapSize.
217 \value SkyMaterial.ScreenSpaceFull The sky shader is evaluated directly on
218 screen at full viewport resolution every frame (sharpest, most costly).
219 \value SkyMaterial.ScreenSpaceHalf Direct screen-space evaluation at half
220 resolution per axis (quarter the pixels), upscaled to the viewport.
221 \value SkyMaterial.ScreenSpaceQuarter Direct screen-space evaluation at quarter
222 resolution per axis (one sixteenth the pixels), upscaled to the viewport.
223
224 The \c ScreenSpace modes decouple the visible background from the IBL cube and
225 are best for dynamic skies (e.g. moving sun, animated volumetric clouds);
226 \c Cubemap is best for static skies. \c Cubemap forces the cubemap to be
227 generated even when \l enableIBL is \c false.
228
229 \sa radianceMapSize, enableIBL
230*/
231
232QQuick3DSkyMaterial::QQuick3DSkyMaterial(QQuick3DObject *parent)
233 : QQuick3DObject(parent), QQuick3DPropertyChangedTracker(this, QQuick3DSuperClassInfo<QQuick3DSkyMaterial>())
234{
235}
236
237int QQuick3DSkyMaterial::radianceMapSize() const
238{
239 return m_radianceMapSize;
240}
241
242QSSGRenderGraphObject *QQuick3DSkyMaterial::updateSpatialNode(QSSGRenderGraphObject *node)
243{
244 QSSGRenderSkyMaterial *material = static_cast<QSSGRenderSkyMaterial *>(node);
245 if (!material) {
246 material = new QSSGRenderSkyMaterial;
247 material->isDirty = true;
248 }
249
250 // isDirty tracks whether the procedural cubemap content needs to be regenerated.
251 // Toggling enableIBL alone reuses the same cubemap, so it isn't a content change.
252 if (material->radianceMapSize != m_radianceMapSize || (m_dirtyFlag & (Dirty::FragmentShader | Dirty::TrackedProperty))) {
253 material->isDirty = true;
254 }
255
256 material->radianceMapSize = m_radianceMapSize;
257 material->enableIBL = m_enableIBL;
258 material->iblSampleCount = m_iblSampleCount;
259 // Convert user-facing "frames to converge" to the render-side per-frame sample budget.
260 // 1 frame = no time-slicing (mapped to 0, the render struct's "single-frame" sentinel).
261 // Otherwise ceil(sampleCount/frameCount) so accumulation always finishes within the
262 // requested frame budget, even when the division isn't exact.
263 material->iblSamplesPerFrame = (m_iblRenderFrames <= 1) ? 0 : (m_iblSampleCount + m_iblRenderFrames - 1) / m_iblRenderFrames;
264 material->iblRenderFrames = m_iblRenderFrames;
265 material->skyboxMode = static_cast<QSSGRenderSkyMaterial::SkyboxMode>(m_skyboxMode);
266
267 if (m_dirtyFlag & Dirty::FragmentShader) {
268 const QQmlContext *context = qmlContext(this);
269 material->fragmentShaderSource = !m_fragmentShaderCode.isEmpty() ? m_fragmentShaderCode.toUtf8()
270 : !m_fragmentShader.isEmpty() ? QSSGShaderUtils::resolveShader(m_fragmentShader, context, material->shaderPathKey)
271 : QByteArray();
272 material->isFragmentShaderDirty = true;
273 material->isBackgroundShaderDirty = true;
274 }
275
276 if (m_dirtyFlag & Dirty::TrackedProperty) {
277 material->propertyUniforms = extractProperties();
278 }
279
280 m_dirtyFlag = 0;
281
282 return QQuick3DObject::updateSpatialNode(material);
283}
284
285void QQuick3DSkyMaterial::markTrackedPropertyDirty(QMetaProperty property, DirtyPropertyHint hint)
286{
287 Q_UNUSED(property);
288 Q_UNUSED(hint);
289 markDirty(Dirty::TrackedProperty);
290}
291
292void QQuick3DSkyMaterial::markDirty(Dirty v)
293{
294 m_dirtyFlag |= v;
295 update();
296}
297
298void QQuick3DSkyMaterial::setRadianceMapSize(int radianceMapSize)
299{
300 // Snap to the nearest power of two, then clamp to [8, 2048]. We store the
301 // normalized value so reading the property back reflects the resolution the
302 // renderer will actually use.
303 const int upper = qNextPowerOfTwo(qMax(1, radianceMapSize));
304 const int lower = upper >> 1;
305 radianceMapSize = (radianceMapSize - lower < upper - radianceMapSize) ? lower : upper;
306 radianceMapSize = qBound(8, radianceMapSize, 2048);
307 if (m_radianceMapSize == radianceMapSize)
308 return;
309
310 m_radianceMapSize = radianceMapSize;
311 emit radianceMapSizeChanged();
312 update();
313}
314
315QUrl QQuick3DSkyMaterial::fragmentShader() const
316{
317 return m_fragmentShader;
318}
319
320void QQuick3DSkyMaterial::setFragmentShader(const QUrl &newFragmentShader)
321{
322 if (m_fragmentShader == newFragmentShader)
323 return;
324 m_fragmentShader = newFragmentShader;
325 emit fragmentShaderChanged();
326 markDirty(Dirty::FragmentShader);
327}
328
329QString QQuick3DSkyMaterial::fragmentShaderCode() const
330{
331 return m_fragmentShaderCode;
332}
333
334void QQuick3DSkyMaterial::setFragmentShaderCode(const QString &newFragmentShaderCode)
335{
336 if (m_fragmentShaderCode == newFragmentShaderCode)
337 return;
338 m_fragmentShaderCode = newFragmentShaderCode;
339 emit fragmentShaderCodeChanged();
340 markDirty(Dirty::FragmentShader);
341}
342
343bool QQuick3DSkyMaterial::enableIBL() const
344{
345 return m_enableIBL;
346}
347
348void QQuick3DSkyMaterial::setEnableIBL(bool newEnableIBL)
349{
350 if (m_enableIBL == newEnableIBL)
351 return;
352 m_enableIBL = newEnableIBL;
353 emit enableIBLChanged();
354 update();
355}
356
357int QQuick3DSkyMaterial::iblSampleCount() const
358{
359 return m_iblSampleCount;
360}
361
362void QQuick3DSkyMaterial::setIblSampleCount(int newIblSampleCount)
363{
364 const int clamped = qBound(1, newIblSampleCount, 1024);
365 if (m_iblSampleCount == clamped)
366 return;
367 m_iblSampleCount = clamped;
368 emit iblSampleCountChanged();
369 update();
370}
371
372int QQuick3DSkyMaterial::iblRenderFrames() const
373{
374 return m_iblRenderFrames;
375}
376
377void QQuick3DSkyMaterial::setIblRenderFrames(int newIblRenderFrames)
378{
379 const int clamped = qMax(0, newIblRenderFrames);
380 if (m_iblRenderFrames == clamped)
381 return;
382 m_iblRenderFrames = clamped;
383 emit iblRenderFramesChanged();
384 update();
385}
386
387QQuick3DSkyMaterial::SkyboxMode QQuick3DSkyMaterial::skyboxMode() const
388{
389 return m_skyboxMode;
390}
391
392void QQuick3DSkyMaterial::setSkyboxMode(SkyboxMode skyboxMode)
393{
394 if (m_skyboxMode == skyboxMode)
395 return;
396 m_skyboxMode = skyboxMode;
397 emit skyboxModeChanged();
398 update();
399}
400
401QT_END_NAMESPACE