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.
87
88 Higher values improve reflection sharpness and lighting quality,
89 especially for glossy materials, but increase memory usage and
90 rendering cost.
91
92 Values are snapped to the nearest power of two and clamped to the
93 range [8, 2048].
94*/
95
96/*!
97 \qmlproperty url SkyMaterial::fragmentShader
98 \since 6.12
99
100 Specifies a fragment shader loaded from a file or resource URL.
101
102 If both \l fragmentShader and \l fragmentShaderCode are set,
103 \l fragmentShaderCode takes precedence.
104
105 If no shader is specified, a built-in debug shader is used.
106
107 \sa fragmentShaderCode
108*/
109
110/*!
111 \qmlproperty string SkyMaterial::fragmentShaderCode
112 \since 6.12
113
114 Specifies inline fragment shader source code.
115
116 This behaves identically to \l fragmentShader and uses the same
117 execution model, built-ins, and automatic property-to-uniform mapping.
118
119 If both \l fragmentShader and \l fragmentShaderCode are set,
120 this property takes precedence.
121
122 \sa fragmentShader
123*/
124
125/*!
126 \qmlproperty bool SkyMaterial::enableIBL
127 \since 6.12
128 \default true
129
130 Determines whether the generated sky cubemap is used for image-based
131 lighting.
132
133 When enabled, the engine generates filtered radiance and irradiance
134 maps used for ambient lighting and reflections in physically based
135 rendering.
136
137 When disabled, the sky is only rendered as a visual background and no
138 IBL preprocessing is performed.
139
140 \sa SceneEnvironment::lightProbe
141*/
142
143/*!
144 \qmlproperty int SkyMaterial::iblSampleCount
145 \since 6.12
146 \default 32
147
148 The total number of GGX importance samples evaluated per output texel
149 of the filtered radiance map. Higher values reduce shimmering on
150 high-frequency features such as a sun disk, at the cost of generation
151 time.
152
153 Values are clamped to the range [1, 1024].
154
155 \sa iblRenderFrames
156*/
157
158/*!
159 \qmlproperty int SkyMaterial::iblRenderFrames
160 \since 6.12
161 \default 2
162
163 The number of frames over which the IBL prefilter integrates \l iblSampleCount
164 samples before reaching full convergence. Higher values spread the prefilter
165 cost across more frames, so no single frame pays the full cost; lower values
166 converge faster but make each frame more expensive.
167
168 When this value is \c 1 (or less), the prefilter is evaluated in a single
169 frame (no time-slicing).
170
171 Values greater than \l iblSampleCount have no further effect: the prefilter
172 cannot spread N samples across more than N frames, so the effective
173 convergence period is capped at \l iblSampleCount frames.
174
175 Spreading the work across frames is useful when the procedural sky changes
176 infrequently relative to the frame rate: a high \l iblSampleCount can be
177 amortized over many frames. Whenever the sky content changes (a tracked
178 property updates, the fragment shader changes, or \l radianceMapSize
179 changes), accumulation restarts from scratch.
180
181 \sa iblSampleCount
182*/
183
184QQuick3DSkyMaterial::QQuick3DSkyMaterial(QQuick3DObject *parent)
185 : QQuick3DObject(parent), QQuick3DPropertyChangedTracker(this, QQuick3DSuperClassInfo<QQuick3DSkyMaterial>())
186{
187}
188
189int QQuick3DSkyMaterial::radianceMapSize() const
190{
191 return m_radianceMapSize;
192}
193
194QSSGRenderGraphObject *QQuick3DSkyMaterial::updateSpatialNode(QSSGRenderGraphObject *node)
195{
196 QSSGRenderSkyMaterial *material = static_cast<QSSGRenderSkyMaterial *>(node);
197 if (!material) {
198 material = new QSSGRenderSkyMaterial;
199 material->isDirty = true;
200 }
201
202 // isDirty tracks whether the procedural cubemap content needs to be regenerated.
203 // Toggling enableIBL alone reuses the same cubemap, so it isn't a content change.
204 if (material->radianceMapSize != m_radianceMapSize || (m_dirtyFlag & (Dirty::FragmentShader | Dirty::TrackedProperty))) {
205 material->isDirty = true;
206 }
207
208 material->radianceMapSize = m_radianceMapSize;
209 material->enableIBL = m_enableIBL;
210 material->iblSampleCount = m_iblSampleCount;
211 // Convert user-facing "frames to converge" to the render-side per-frame sample budget.
212 // 1 frame = no time-slicing (mapped to 0, the render struct's "single-frame" sentinel).
213 // Otherwise ceil(sampleCount/frameCount) so accumulation always finishes within the
214 // requested frame budget, even when the division isn't exact.
215 material->iblSamplesPerFrame = (m_iblRenderFrames <= 1) ? 0 : (m_iblSampleCount + m_iblRenderFrames - 1) / m_iblRenderFrames;
216
217 if (m_dirtyFlag & Dirty::FragmentShader) {
218 const QQmlContext *context = qmlContext(this);
219 material->fragmentShaderSource = !m_fragmentShaderCode.isEmpty() ? m_fragmentShaderCode.toUtf8()
220 : !m_fragmentShader.isEmpty() ? QSSGShaderUtils::resolveShader(m_fragmentShader, context, material->shaderPathKey)
221 : QByteArray();
222 material->isFragmentShaderDirty = true;
223 }
224
225 if (m_dirtyFlag & Dirty::TrackedProperty) {
226 material->propertyUniforms = extractProperties();
227 }
228
229 m_dirtyFlag = 0;
230
231 return QQuick3DObject::updateSpatialNode(material);
232}
233
234void QQuick3DSkyMaterial::markTrackedPropertyDirty(QMetaProperty property, DirtyPropertyHint hint)
235{
236 Q_UNUSED(property);
237 Q_UNUSED(hint);
238 markDirty(Dirty::TrackedProperty);
239}
240
241void QQuick3DSkyMaterial::markDirty(Dirty v)
242{
243 m_dirtyFlag |= v;
244 update();
245}
246
247void QQuick3DSkyMaterial::setRadianceMapSize(int radianceMapSize)
248{
249 // Snap to the nearest power of two, then clamp to [8, 2048]. We store the
250 // normalized value so reading the property back reflects the resolution the
251 // renderer will actually use.
252 const int upper = qNextPowerOfTwo(qMax(1, radianceMapSize));
253 const int lower = upper >> 1;
254 radianceMapSize = (radianceMapSize - lower < upper - radianceMapSize) ? lower : upper;
255 radianceMapSize = qBound(8, radianceMapSize, 2048);
256 if (m_radianceMapSize == radianceMapSize)
257 return;
258
259 m_radianceMapSize = radianceMapSize;
260 emit radianceMapSizeChanged();
261 update();
262}
263
264QUrl QQuick3DSkyMaterial::fragmentShader() const
265{
266 return m_fragmentShader;
267}
268
269void QQuick3DSkyMaterial::setFragmentShader(const QUrl &newFragmentShader)
270{
271 if (m_fragmentShader == newFragmentShader)
272 return;
273 m_fragmentShader = newFragmentShader;
274 emit fragmentShaderChanged();
275 markDirty(Dirty::FragmentShader);
276}
277
278QString QQuick3DSkyMaterial::fragmentShaderCode() const
279{
280 return m_fragmentShaderCode;
281}
282
283void QQuick3DSkyMaterial::setFragmentShaderCode(const QString &newFragmentShaderCode)
284{
285 if (m_fragmentShaderCode == newFragmentShaderCode)
286 return;
287 m_fragmentShaderCode = newFragmentShaderCode;
288 emit fragmentShaderCodeChanged();
289 markDirty(Dirty::FragmentShader);
290}
291
292bool QQuick3DSkyMaterial::enableIBL() const
293{
294 return m_enableIBL;
295}
296
297void QQuick3DSkyMaterial::setEnableIBL(bool newEnableIBL)
298{
299 if (m_enableIBL == newEnableIBL)
300 return;
301 m_enableIBL = newEnableIBL;
302 emit enableIBLChanged();
303 update();
304}
305
306int QQuick3DSkyMaterial::iblSampleCount() const
307{
308 return m_iblSampleCount;
309}
310
311void QQuick3DSkyMaterial::setIblSampleCount(int newIblSampleCount)
312{
313 const int clamped = qBound(1, newIblSampleCount, 1024);
314 if (m_iblSampleCount == clamped)
315 return;
316 m_iblSampleCount = clamped;
317 emit iblSampleCountChanged();
318 update();
319}
320
321int QQuick3DSkyMaterial::iblRenderFrames() const
322{
323 return m_iblRenderFrames;
324}
325
326void QQuick3DSkyMaterial::setIblRenderFrames(int newIblRenderFrames)
327{
328 const int clamped = qMax(1, newIblRenderFrames);
329 if (m_iblRenderFrames == clamped)
330 return;
331 m_iblRenderFrames = clamped;
332 emit iblRenderFramesChanged();
333 update();
334}
335
336QT_END_NAMESPACE