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