On this page

Qt Quick 3D - Sky Material Example

Demonstrates procedural sky rendering and image-based lighting with SkyMaterial.

Procedural atmospheric sky with PBR spheres reflecting the environment

This example shows how to use SkyMaterial to render a procedural sky directly from a fragment shader and use the same procedural cubemap as the light probe for image-based lighting (IBL) on the scene's PBR materials.

Two SkyMaterial subclasses are provided side by side:

  • BasicSky — a small shader that produces a gradient sky with a sun disk, suitable as a starting point for stylized or game-style skies.
  • AdvancedSky — a physically-based atmospheric scattering model following Hillaire's "A Scalable and Production Ready Sky and Atmosphere Rendering Technique" (2020), with configurable aerosols, ozone, and eye altitude.

Setting Up the Scene

The application is a standard Window with a View3D. The relevant QtQuick3D imports are:

import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts

import QtQuick3D
import QtQuick3D.Helpers

Defining the Sky Materials

Both sky variants are declared inside the View3D so they share the same skyLight Node (the sun). The active material is selected by a property that the UI swaps between the two:

// Two SkyMaterial subclasses are instantiated up front. The toggle in
// the side panel swaps which one is assigned to SceneEnvironment.skyMaterial.
BasicSky {
    id: basicSky
    iblRenderFrames: view3D.iblRenderFrames
    iblSampleCount: view3D.iblSampleCount
    enableIBL: view3D.enableIBL
    radianceMapSize: view3D.radianceMapSize
    skyLight: sun
}

AdvancedSky {
    id: advancedSky
    iblRenderFrames: view3D.iblRenderFrames
    iblSampleCount: view3D.iblSampleCount
    enableIBL: view3D.enableIBL
    radianceMapSize: view3D.radianceMapSize
    skyLight: sun
}

Each helper file is a SkyMaterial subclass with its own fragment shader and an exported set of QML properties; the properties are automatically forwarded as uniforms to the shader. See BasicSky.qml and AdvancedSky.qml.

Image-Based Lighting

Assigning the active SkyMaterial to SceneEnvironment::skyMaterial uses the procedurally rendered cubemap both as the visible background and as the source for image-based lighting on the PBR materials in the scene:

// SkyMaterial produces both the visible sky (rendered as the background)
// and the image-based lighting cubemap used for reflections and ambient
// light on the PBR materials in the scene.
environment: SceneEnvironment {
    backgroundMode: SceneEnvironment.SkyMaterial
    skyMaterial: view3D.activeSky
}

The smoothest sphere in the demo mirrors the procedural sky almost perfectly; the rougher spheres show progressively blurred reflections, produced by SkyMaterial's GGX prefilter chain over the procedural cubemap.

Time-Sliced IBL Convergence

Generating the prefiltered IBL cubemap is the most expensive part of a procedural sky. SkyMaterial exposes iblRenderFrames so that the cost can be spread across several frames: the same total iblSampleCount is integrated incrementally, with the accumulator advancing each frame until convergence.

With the default value of 1 the prefilter runs in a single frame. Setting it higher amortizes the cost, which is useful when the sky changes infrequently relative to the frame rate, e.g. an animated sun-sweep that should not stall the main thread on every step.

Animating the Sun

A FrameAnimation drives the sun back and forth across the horizon when the "Animate sun" toggle is on. Time-slicing pairs naturally with this kind of slow drift: every sun position invalidates the accumulator, but the convergence catches up over the next few frames without any single frame paying the full prefilter cost.

// Ping-pongs the sun back and forth across the horizon while
// view3D.animateSun is true. With time-sliced IBL the cubemap
// progressively re-converges as the sun moves.
FrameAnimation {
    id: sunAnimator
    running: view3D.animateSun

    property real t: 0
    property real lastT: 0
    property int direction: 1

    onTriggered: {
        const min = view3D.sunSweepMinRot
        const max = view3D.sunSweepMaxRot
        lastT = t
        t += direction * frameTime * view3D.sunSweepSpeedDegPerSec * 0.01
        sun.eulerRotation.x = min + (Math.sin(t) * 0.5 + 0.5) * (max - min)
    }

    function updatePhaseFromSun() {
        const min = view3D.sunSweepMinRot
        const max = view3D.sunSweepMaxRot
        let norm = Math.max(0.0, Math.min(1.0, (sun.eulerRotation.x - min) / (max - min)))
        t = Math.asin(norm * 2.0 - 1.0)
        direction = lastT < t ? 1 : -1
    }
}

Controls

  • Sky variant — selects which SkyMaterial subclass is active.
  • Animate sun — runs the sun sweep across the horizon.
  • Sun elevation — manual sun position; enabled when the sweep animation is off.
  • IBL Render frames — how many frames the prefilter takes to converge. 1 means single-frame (no time-slicing); higher values spread the cost.
  • IBL sample count — number of GGX importance samples integrated per output texel of the prefiltered radiance map. Higher values reduce shimmering on high-frequency features (e.g. the sun disk) at the cost of more work per cycle.
  • Radiance map size — resolution of the procedural environment cubemap. Higher values sharpen reflections on glossy materials but increase memory and rendering cost. Internally clamped to the nearest power-of-two.

The camera responds to standard WasdController navigation — W/A/S/D plus mouse drag to look around.

Example project @ code.qt.io

See also SkyMaterial and SceneEnvironment.

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.