Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include <QtQuick3D/private/qquick3dskymaterial_p.h>
++ #include <QtQuick3D/private/qquick3dobject_p.h>
++
++ #include <QtQuick3DRuntimeRender/private/qssgrenderskymaterial_p.h>
++
++ #include <QtCore/qmath.h>
++ #include <QtGui/qquaternion.h>
++
++ QT_BEGIN_NAMESPACE
++ /*!
++ \qmltype SkyMaterial
++ \inqmlmodule QtQuick3D
++ \since 6.12
++ \brief Renders a procedural sky and generates image-based lighting data.
++
++ SkyMaterial renders a sky environment into a cubemap using custom
++ fragment shader code. The resulting cubemap can optionally be filtered
++ and used as image-based lighting (IBL) for physically based rendering.
++
++ The shader is evaluated for all six faces of the cubemap and may
++ implement arbitrary procedural skies, gradients, or analytical models.
++
++ The generated cubemap is used both as the visible scene background and
++ as the source for image-based lighting when enabled.
++
++ \note Changes to shader code or properties may take a frame or more
++ before being reflected in the generated IBL.
++
++ A ready-made SkyMaterial with a gradient sky, ground, and sun disk is
++ available as \l ProceduralSkyMaterial in the \l{Qt Quick 3D Helpers QML
++ Types}{QtQuick3D.Helpers} module. It can be used directly without writing
++ any shader code.
++
++ \section1 Shader
++
++ The shader has access to the built-in variable \c qt_eyeDir (\c vec3),
++ which provides the non-normalized direction vector from the center of
++ the cubemap to the current sample direction.
++
++ The fragment shader must define a \c MAIN() entry point, which is
++ executed for each fragment generated while rendering the cubemap.
++
++ The final pixel color must be written to \c FRAGCOLOR (\c vec4), which
++ is the output variable of the fragment shader.
++
++ A minimal shader example:
++
++ \badcode
++ void MAIN()
++ {
++ vec3 dir = normalize(qt_eyeDir);
++
++ // Output color (encoded direction as RGB)
++ FRAGCOLOR = vec4(dir * 0.5 + 0.5, 1.0);
++ }
++ \endcode
++
++ Custom QML properties declared on SkyMaterial are automatically exposed
++ as uniforms in the fragment shader. The property name must match the GLSL
++ variable name. No explicit uniform declaration is required.
++
++ The following type mappings are supported:
++
++ \table
++ \header
++ \li QML Type
++ \li Shader Type
++ \li Notes
++ \row \li real, int, bool \li float, int, bool \li
++ \row \li color \li vec4 \li sRGB is converted to linear space
++ \row \li vector2d, vector3d, vector4d \li vec2, vec3, vec4 \li
++ \row \li matrix4x4 \li mat4 \li
++ \row \li quaternion \li vec4 \li w stores scalar component
++ \row \li rect \li vec4 \li
++ \row \li point, size \li vec2 \li
++ \row \li Texture \li sampler2D, samplerCube, sampler3D
++ \li The sampler type follows the referenced texture: a \l CubeMapTexture
++ becomes a \c samplerCube, a \l Texture with 3D texture data becomes a
++ \c sampler3D, and any other \l Texture becomes a \c sampler2D.
++ \endtable
++
++ A texture property that has no texture set is not exposed to the shader at
++ all, so referencing it in the shader code results in a compilation error.
++
++ \sa SceneEnvironment::skyMaterial, ProceduralSkyMaterial
++ */
++
++ /*!
++ \qmlproperty int SkyMaterial::radianceMapSize
++ \since 6.12
++ \default 512
++
++ Specifies the resolution of the generated environment cubemap used for
++ image-based lighting and reflection probes.
++
++ Higher values improve reflection sharpness and lighting quality,
++ especially for glossy materials, but increase memory usage and
++ rendering cost.
++
++ Values are snapped to the nearest power of two and clamped to the
++ range [8, 2048].
++
++ \note With \l skyboxMode set to a \c ScreenSpace value, the visible sky is
++ evaluated directly on screen and this property only affects the IBL/reflection
++ cubemap. With \c SkyMaterial.Cubemap it also determines the sharpness of the
++ visible background. When \l enableIBL is \c false, \l skyboxMode is a
++ \c ScreenSpace value, and no reflection probes are present, no cubemap is
++ generated at all.
++
++ \sa skyboxMode, enableIBL
++ */
++
++ /*!
++ \qmlproperty url SkyMaterial::fragmentShader
++ \since 6.12
++
++ Specifies a fragment shader loaded from a file or resource URL.
++
++ If both \l fragmentShader and \l fragmentShaderCode are set,
++ \l fragmentShaderCode takes precedence.
++
++ If no shader is specified, a built-in debug shader is used.
++
++ \sa fragmentShaderCode
++ */
++
++ /*!
++ \qmlproperty string SkyMaterial::fragmentShaderCode
++ \since 6.12
++
++ Specifies inline fragment shader source code.
++
++ This behaves identically to \l fragmentShader and uses the same
++ execution model, built-ins, and automatic property-to-uniform mapping.
++
++ If both \l fragmentShader and \l fragmentShaderCode are set,
++ this property takes precedence.
++
++ \sa fragmentShader
++ */
++
++ /*!
++ \qmlproperty bool SkyMaterial::enableIBL
++ \since 6.12
++ \default true
++
++ Determines whether the generated sky cubemap is used for image-based
++ lighting.
++
++ When enabled, the engine generates filtered radiance and irradiance
++ maps used for ambient lighting and reflections in physically based
++ rendering.
++
++ When disabled, the sky is only rendered as a visual background and no
++ IBL preprocessing is performed.
++
++ \sa SceneEnvironment::lightProbe
++ */
++
++ /*!
++ \qmlproperty int SkyMaterial::iblSampleCount
++ \since 6.12
++ \default 32
++
++ The total number of GGX importance samples evaluated per output texel
++ of the filtered radiance map. Higher values reduce shimmering on
++ high-frequency features such as a sun disk, at the cost of generation
++ time.
++
++ Values are clamped to the range [1, 1024].
++
++ \note This property has no effect when \l enableIBL is \c false.
++
++ \sa iblRenderFrames, enableIBL
++ */
++
++ /*!
++ \qmlproperty int SkyMaterial::iblRenderFrames
++ \since 6.12
++ \default 0
++
++ Controls how the IBL prefilter work is spread across frames.
++
++ \list
++ \li \b 0 — Everything in one frame: sample accumulation, normalization, and
++ irradiance are all evaluated in the same frame.
++ \li \b {N ≥ 1} — The \l iblSampleCount samples are divided across N slice
++ frames (\c ceil(iblSampleCount / N) samples per frame), followed by a
++ dedicated normalization and irradiance frame.
++ \endlist
++
++ Higher values amortize the GGX prefilter cost across more frames, which is
++ useful when the sky changes infrequently and a high \l iblSampleCount is
++ required for quality.
++
++ \note This property has no effect when \l enableIBL is \c false.
++
++ \sa iblSampleCount, enableIBL
++ */
++
++ /*!
++ \qmlproperty enumeration SkyMaterial::skyboxMode
++ \since 6.12
++ \default SkyMaterial.Cubemap
++
++ Selects what the SkyMaterial produces for the visible background.
++
++ \value SkyMaterial.Cubemap The background samples the radiance
++ cubemap (the same cube used for image-based lighting), whose resolution
++ follows \l radianceMapSize. For a static sky the cube is rendered once and
++ then sampled cheaply every frame, so this is the lowest per-frame cost when
++ nothing but the camera is moving. Sharpness is capped at \l radianceMapSize.
++ \value SkyMaterial.ScreenSpaceFull The sky shader is evaluated directly on
++ screen at full viewport resolution every frame (sharpest, most costly).
++ \value SkyMaterial.ScreenSpaceHalf Direct screen-space evaluation at half
++ resolution per axis (quarter the pixels), upscaled to the viewport.
++ \value SkyMaterial.ScreenSpaceQuarter Direct screen-space evaluation at quarter
++ resolution per axis (one sixteenth the pixels), upscaled to the viewport.
++
++ The \c ScreenSpace modes decouple the visible background from the IBL cube and
++ are best for dynamic skies (e.g. moving sun, animated volumetric clouds);
++ \c Cubemap is best for static skies. \c Cubemap forces the cubemap to be
++ generated even when \l enableIBL is \c false.
++
++ \sa radianceMapSize, enableIBL
++ */
++
++ QQuick3DSkyMaterial::QQuick3DSkyMaterial(QQuick3DObject *parent)
++ : QQuick3DObject(parent), QQuick3DPropertyChangedTracker(this, QQuick3DSuperClassInfo<QQuick3DSkyMaterial>())
++ {
++ }
++
++ int QQuick3DSkyMaterial::radianceMapSize() const
++ {
++ return m_radianceMapSize;
++ }
++
++ QSSGRenderGraphObject *QQuick3DSkyMaterial::updateSpatialNode(QSSGRenderGraphObject *node)
++ {
++ QSSGRenderSkyMaterial *material = static_cast<QSSGRenderSkyMaterial *>(node);
++ if (!material) {
++ material = new QSSGRenderSkyMaterial;
++ material->isDirty = true;
++ }
++
++ // isDirty tracks whether the procedural cubemap content needs to be regenerated.
++ // Toggling enableIBL alone reuses the same cubemap, so it isn't a content change.
++ if (material->radianceMapSize != m_radianceMapSize || (m_dirtyFlag & (Dirty::FragmentShader | Dirty::TrackedProperty))) {
++ material->isDirty = true;
++ }
++
++ material->radianceMapSize = m_radianceMapSize;
++ material->enableIBL = m_enableIBL;
++ material->iblSampleCount = m_iblSampleCount;
++ // Convert user-facing "frames to converge" to the render-side per-frame sample budget.
++ // 1 frame = no time-slicing (mapped to 0, the render struct's "single-frame" sentinel).
++ // Otherwise ceil(sampleCount/frameCount) so accumulation always finishes within the
++ // requested frame budget, even when the division isn't exact.
++ material->iblSamplesPerFrame = (m_iblRenderFrames <= 1) ? 0 : (m_iblSampleCount + m_iblRenderFrames - 1) / m_iblRenderFrames;
++ material->iblRenderFrames = m_iblRenderFrames;
++ material->skyboxMode = static_cast<QSSGRenderSkyMaterial::SkyboxMode>(m_skyboxMode);
++
++ if (m_dirtyFlag & Dirty::FragmentShader) {
++ const QQmlContext *context = qmlContext(this);
++ material->fragmentShaderSource = !m_fragmentShaderCode.isEmpty() ? m_fragmentShaderCode.toUtf8()
++ : !m_fragmentShader.isEmpty() ? QSSGShaderUtils::resolveShader(m_fragmentShader, context, material->shaderPathKey)
++ : QByteArray();
++ material->isFragmentShaderDirty = true;
++ material->isBackgroundShaderDirty = true;
++ }
++
++ if (m_dirtyFlag & Dirty::TrackedProperty) {
++ material->propertyUniforms = extractProperties();
++ }
++
++ m_dirtyFlag = 0;
++
++ return QQuick3DObject::updateSpatialNode(material);
++ }
++
++ void QQuick3DSkyMaterial::markTrackedPropertyDirty(QMetaProperty property, DirtyPropertyHint hint)
++ {
++ Q_UNUSED(property);
++ Q_UNUSED(hint);
++ markDirty(Dirty::TrackedProperty);
++ }
++
++ void QQuick3DSkyMaterial::markDirty(Dirty v)
++ {
++ m_dirtyFlag |= v;
++ update();
++ }
++
++ void QQuick3DSkyMaterial::setRadianceMapSize(int radianceMapSize)
++ {
++ // Snap to the nearest power of two, then clamp to [8, 2048]. We store the
++ // normalized value so reading the property back reflects the resolution the
++ // renderer will actually use.
++ const int upper = qNextPowerOfTwo(qMax(1, radianceMapSize));
++ const int lower = upper >> 1;
++ radianceMapSize = (radianceMapSize - lower < upper - radianceMapSize) ? lower : upper;
++ radianceMapSize = qBound(8, radianceMapSize, 2048);
++ if (m_radianceMapSize == radianceMapSize)
++ return;
++
++ m_radianceMapSize = radianceMapSize;
++ emit radianceMapSizeChanged();
++ update();
++ }
++
++ QUrl QQuick3DSkyMaterial::fragmentShader() const
++ {
++ return m_fragmentShader;
++ }
++
++ void QQuick3DSkyMaterial::setFragmentShader(const QUrl &newFragmentShader)
++ {
++ if (m_fragmentShader == newFragmentShader)
++ return;
++ m_fragmentShader = newFragmentShader;
++ emit fragmentShaderChanged();
++ markDirty(Dirty::FragmentShader);
++ }
++
++ QString QQuick3DSkyMaterial::fragmentShaderCode() const
++ {
++ return m_fragmentShaderCode;
++ }
++
++ void QQuick3DSkyMaterial::setFragmentShaderCode(const QString &newFragmentShaderCode)
++ {
++ if (m_fragmentShaderCode == newFragmentShaderCode)
++ return;
++ m_fragmentShaderCode = newFragmentShaderCode;
++ emit fragmentShaderCodeChanged();
++ markDirty(Dirty::FragmentShader);
++ }
++
++ bool QQuick3DSkyMaterial::enableIBL() const
++ {
++ return m_enableIBL;
++ }
++
++ void QQuick3DSkyMaterial::setEnableIBL(bool newEnableIBL)
++ {
++ if (m_enableIBL == newEnableIBL)
++ return;
++ m_enableIBL = newEnableIBL;
++ emit enableIBLChanged();
++ update();
++ }
++
++ int QQuick3DSkyMaterial::iblSampleCount() const
++ {
++ return m_iblSampleCount;
++ }
++
++ void QQuick3DSkyMaterial::setIblSampleCount(int newIblSampleCount)
++ {
++ const int clamped = qBound(1, newIblSampleCount, 1024);
++ if (m_iblSampleCount == clamped)
++ return;
++ m_iblSampleCount = clamped;
++ emit iblSampleCountChanged();
++ update();
++ }
++
++ int QQuick3DSkyMaterial::iblRenderFrames() const
++ {
++ return m_iblRenderFrames;
++ }
++
++ void QQuick3DSkyMaterial::setIblRenderFrames(int newIblRenderFrames)
++ {
++ const int clamped = qMax(0, newIblRenderFrames);
++ if (m_iblRenderFrames == clamped)
++ return;
++ m_iblRenderFrames = clamped;
++ emit iblRenderFramesChanged();
++ update();
++ }
++
++ QQuick3DSkyMaterial::SkyboxMode QQuick3DSkyMaterial::skyboxMode() const
++ {
++ return m_skyboxMode;
++ }
++
++ void QQuick3DSkyMaterial::setSkyboxMode(SkyboxMode skyboxMode)
++ {
++ if (m_skyboxMode == skyboxMode)
++ return;
++ m_skyboxMode = skyboxMode;
++ emit skyboxModeChanged();
++ update();
++ }
++
++ QT_END_NAMESPACE