Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ import QtQuick
++ import QtQuick.Window
++ import QtQuick.Controls
++ import QtQuick.Layouts
++
++ import QtQuick3D
++ import QtQuick3D.Helpers
++
++ Window {
++ id: windowRoot
++ visible: true
++ width: 1280
++ height: 720
++
++ View3D {
++ id: view3D
++ anchors.fill: parent
++
++ // IBL backend: 0=Legacy CPU, 1=SkyMaterial GPU
++ property int iblMode: 1
++ property int skyMaterialKind: 1
++ property SkyMaterial activeSkyMaterial: skyMaterialKind === 0 ? basicSkyEffect : advancedSkyEffect
++ property bool animateSunAcrossHorizon: false
++ property real sunSweepSpeedDegPerSec: 20.0
++ property real sunSweepMinRot: -180
++ property real sunSweepMaxRot: 0
++ property real sunSweepDirection: 1.0
++
++ property Texture lightProbe: Texture {
++ textureData: ProceduralSkyTextureData {
++ id: procSkyTextureData
++ }
++ }
++
++ // ProceduralSkyTextureData regenerates the entire CPU texture every time a
++ // sun property changes, so only drive it when Legacy CPU IBL is actually in use.
++ Binding {
++ target: procSkyTextureData
++ property: "sunLatitude"
++ // Map DirectionalLight.eulerRotation.x (expected in [-180, 0])
++ // to a sun elevation in [0, 90]: 0 = horizon, -90 = zenith, -180 = opposite horizon.
++ value: 90 - Math.abs(mySkyLight.eulerRotation.x + 90)
++ when: view3D.iblMode === 0
++ }
++ Binding {
++ target: procSkyTextureData
++ property: "sunLongitude"
++ value: mySkyLight.eulerRotation.y
++ when: view3D.iblMode === 0
++ }
++
++ AdvancedSky {
++ id: advancedSkyEffect
++ skyLight: mySkyLight
++ }
++
++ BasicSky {
++ id: basicSkyEffect
++ skyLight: mySkyLight
++ }
++
++ environment: SceneEnvironment {
++ id: scene
++ backgroundMode: view3D.iblMode === 0 ? SceneEnvironment.SkyBox : SceneEnvironment.SkyMaterial
++ skyMaterial: view3D.activeSkyMaterial
++ // Legacy CPU probe is mode 0; mode 1 uses GPU SkyMaterial capture.
++ lightProbe: view3D.iblMode === 0 ? view3D.lightProbe : null
++ }
++
++ PerspectiveCamera {
++ id: cam
++ position: Qt.vector3d(1338.76, 251.537, -942.507)
++ eulerRotation: Qt.vector3d(-5.44442, -225.778, 0)
++ }
++ camera: cam
++
++
++ DirectionalLight {
++ id: mySkyLight
++ eulerRotation.x: -90
++ castsShadow: true
++ softShadowQuality: Light.PCF16
++ shadowMapQuality: Light.ShadowMapQualityVeryHigh
++ pcfFactor: 3
++ use32BitShadowmap: true
++ property real customBrightness: 1.0
++ brightness: eulerRotation.x > 0.0 || eulerRotation.x < -180 ? 0 : customBrightness
++ }
++
++ FrameAnimation {
++ running: view3D.animateSunAcrossHorizon
++ onTriggered: {
++ let rot = mySkyLight.eulerRotation.x
++ + view3D.sunSweepDirection * view3D.sunSweepSpeedDegPerSec * frameTime
++
++ if (rot > view3D.sunSweepMaxRot) {
++ rot = view3D.sunSweepMaxRot
++ view3D.sunSweepDirection = -1.0
++ } else if (rot < view3D.sunSweepMinRot) {
++ rot = view3D.sunSweepMinRot
++ view3D.sunSweepDirection = 1.0
++ }
++
++ mySkyLight.eulerRotation.x = rot
++ }
++ }
++
++ Model {
++ source: "#Rectangle"
++ scale: Qt.vector3d(30, 30, 30)
++ eulerRotation.x: -90
++ receivesShadows: true
++ materials: PrincipledMaterial {
++ baseColor: "lightgray"
++ roughness: 0.9
++ metalness: 0.0
++ }
++ }
++
++ Model {
++ source: "#Cube"
++ x: -375
++ y: 100
++ scale: Qt.vector3d(2, 2, 2)
++ castsShadows: true
++ receivesShadows: true
++ materials: PrincipledMaterial {
++ baseColor: "steelblue"
++ roughness: 0.25
++ metalness: 0.0
++ }
++ }
++
++ Model {
++ source: "#Sphere"
++ x: -75
++ y: 125
++ scale: Qt.vector3d(2.5, 2.5, 2.5)
++ castsShadows: true
++ receivesShadows: true
++ materials: PrincipledMaterial {
++ baseColor: "lightblue"
++ roughness: 0.05
++ metalness: 1.0
++ }
++ }
++
++ Model {
++ source: "#Sphere"
++ x: 275
++ y: 125
++ scale: Qt.vector3d(2.5, 2.5, 2.5)
++ castsShadows: true
++ receivesShadows: true
++ materials: PrincipledMaterial {
++ baseColor: "lightblue"
++ roughness: 0.3
++ metalness: 1.0
++ }
++ }
++
++ Model {
++ source: "#Sphere"
++ x: 575
++ y: 125
++ scale: Qt.vector3d(2.5, 2.5, 2.5)
++ castsShadows: true
++ receivesShadows: true
++ materials: PrincipledMaterial {
++ baseColor: "lightblue"
++ roughness: 0.55
++ metalness: 1.0
++ }
++ }
++
++ Model {
++ source: "#Sphere"
++ x: 875
++ y: 125
++ scale: Qt.vector3d(2.5, 2.5, 2.5)
++ castsShadows: true
++ receivesShadows: true
++ materials: PrincipledMaterial {
++ baseColor: "lightblue"
++ roughness: 0.8
++ metalness: 1.0
++ }
++ }
++
++ WasdController {
++ id: wasdController
++ controlledObject: cam
++ }
++ DebugView {
++ anchors.top: parent.top
++ anchors.right: parent.right
++ source: view3D
++ }
++ }
++
++
++
++ Pane {
++ width: 256
++ height: parent.height
++ anchors.top: parent.top
++ anchors.left: parent.left
++
++ ScrollView {
++ anchors.fill: parent
++ anchors.margins: 8
++
++ ColumnLayout {
++ width: parent.width
++ spacing: 6
++
++ Label {
++ Layout.fillWidth: true
++ text: "Sky Controls"
++ font.bold: true
++ }
++
++ ComboBox {
++ Layout.fillWidth: true
++ model: ["Legacy CPU", "SkyMaterial GPU"]
++ currentIndex: view3D.iblMode
++ onActivated: view3D.iblMode = currentIndex
++ }
++
++ ComboBox {
++ Layout.fillWidth: true
++ model: ["BasicSkyEffect", "AdvancedSkyEffect"]
++ currentIndex: view3D.skyMaterialKind
++ onActivated: view3D.skyMaterialKind = currentIndex
++ }
++
++ Switch {
++ Layout.fillWidth: true
++ text: "Animate Sun Across Horizon"
++ checked: view3D.animateSunAcrossHorizon
++ onToggled: view3D.animateSunAcrossHorizon = checked
++ }
++
++ Switch {
++ Layout.fillWidth: true
++ text: "Toggle IBL"
++ checked: view3D.activeSkyMaterial.enableIBL
++ onToggled: view3D.activeSkyMaterial.enableIBL = checked
++ }
++
++ // What the SkyMaterial outputs for the background: the IBL/radiance cube
++ // (cheapest for a static sky — rendered once) or a per-frame screen-space
++ // render at one of three scales. Watch the DebugView frame time change.
++ Label {
++ Layout.fillWidth: true
++ text: "Skybox mode"
++ }
++ ComboBox {
++ Layout.fillWidth: true
++ model: [
++ { value: SkyMaterial.Cubemap, text: "Cubemap (IBL size)" },
++ { value: SkyMaterial.ScreenSpaceFull, text: "Screen space — Full" },
++ { value: SkyMaterial.ScreenSpaceHalf, text: "Screen space — Half" },
++ { value: SkyMaterial.ScreenSpaceQuarter, text: "Screen space — Quarter" },
++ ]
++ textRole: "text"
++ valueRole: "value"
++ currentIndex: view3D.activeSkyMaterial.skyboxMode
++ onActivated: view3D.activeSkyMaterial.skyboxMode = currentValue
++ }
++
++ Label {
++ Layout.fillWidth: true
++ text: "Sun X rot: " + mySkyLight.eulerRotation.x.toFixed(1)
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: -180
++ to: 180
++ value: mySkyLight.eulerRotation.x
++ onMoved: mySkyLight.eulerRotation.x = value
++ stepSize: 1
++ }
++
++ Label {
++ Layout.fillWidth: true
++ text: "Sun Y rot: " + mySkyLight.eulerRotation.y.toFixed(1)
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: -180
++ to: 180
++ value: mySkyLight.eulerRotation.y
++ onMoved: mySkyLight.eulerRotation.y = value
++ }
++ Label {
++ Layout.fillWidth: true
++ text: "GPU Radiance Size: " + view3D.activeSkyMaterial.radianceMapSize
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 0
++ to: 2048
++ stepSize: 1
++ value: view3D.activeSkyMaterial.radianceMapSize
++ enabled: view3D.iblMode === 1
++ onMoved: view3D.activeSkyMaterial.radianceMapSize = Math.round(value)
++ }
++ Label {
++ Layout.fillWidth: true
++ text: "IBL Sample Count: " + view3D.activeSkyMaterial.iblSampleCount
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 1
++ to: 1024
++ stepSize: 1
++ value: view3D.activeSkyMaterial.iblSampleCount
++ enabled: view3D.iblMode === 1
++ onMoved: view3D.activeSkyMaterial.iblSampleCount = Math.round(value)
++ }
++ Label {
++ Layout.fillWidth: true
++ text: "IBL render frames: " + view3D.activeSkyMaterial.iblRenderFrames
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 1
++ to: 32
++ stepSize: 1
++ value: view3D.activeSkyMaterial.iblRenderFrames
++ enabled: view3D.iblMode === 1
++ onMoved: view3D.activeSkyMaterial.iblRenderFrames = Math.round(value)
++ }
++ Label {
++ Layout.fillWidth: true
++ text: "Sun Brightness: " + mySkyLight.customBrightness.toFixed(2)
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 0
++ to: 20
++ value: mySkyLight.customBrightness
++ onMoved: mySkyLight.customBrightness = value
++ }
++ Label {
++ Layout.fillWidth: true
++ text: "Sun Disk Inner Angle: " + view3D.activeSkyMaterial.sunDiskInnerAngle.toFixed(2)
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 0.1
++ to: 10
++ value: view3D.activeSkyMaterial.sunDiskInnerAngle
++ onMoved: view3D.activeSkyMaterial.sunDiskInnerAngle = value
++ }
++
++ Label {
++ Layout.fillWidth: true
++ text: "Sun Disk Outer Angle: " + view3D.activeSkyMaterial.sunDiskOuterAngle.toFixed(2)
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 1
++ to: 150
++ value: view3D.activeSkyMaterial.sunDiskOuterAngle
++ onMoved: view3D.activeSkyMaterial.sunDiskOuterAngle = value
++ }
++
++ Label {
++ Layout.fillWidth: true
++ text: "Sun Disk Falloff: " + view3D.activeSkyMaterial.sunDiskFalloff.toFixed(3)
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 0.001
++ to: 1
++ value: view3D.activeSkyMaterial.sunDiskFalloff
++ onMoved: view3D.activeSkyMaterial.sunDiskFalloff = value
++ }
++
++ Label {
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ text: "LUT Size Transmittance: " + advancedSkyEffect.lutSizeTransmit
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 8
++ to: 1024
++ stepSize: 8
++ value: advancedSkyEffect.lutSizeTransmit
++ enabled: view3D.skyMaterialKind === 1 && view3D.iblMode === 1
++ onMoved: advancedSkyEffect.lutSizeTransmit = Math.round(value / 8) * 8
++ }
++
++ Label {
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ text: "LUT Size: " + advancedSkyEffect.lutSize
++ }
++ Slider {
++ Layout.fillWidth: true
++ from: 8
++ to: 1024
++ stepSize: 8
++ value: advancedSkyEffect.lutSize
++ enabled: view3D.skyMaterialKind === 1 && view3D.iblMode === 1
++ onMoved: advancedSkyEffect.lutSize = Math.round(value / 8) * 8
++ }
++
++ Label {
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ text: "Aerosol type"
++ }
++ ComboBox {
++ enabled: view3D.skyMaterialKind === 1
++ id: aerosolTypeCb
++ Layout.fillWidth: true
++ // 0=Background, 1=Desert Dust, 2=Maritime Clean, 3=Maritime Mineral,
++ // 4=Polar Antarctic, 5=Polar Arctic, 6=Remote Continental, 7=Rural, 8=Urban
++ model: [
++ { value: 0, text: qsTr("Background") },
++ { value: 1, text: qsTr("Desert Dust") },
++ { value: 2, text: qsTr("Maritime Clean") },
++ { value: 3, text: qsTr("Maritime Mineral") },
++ { value: 4, text: qsTr("Polar Antarctic") },
++ { value: 5, text: qsTr("Polar Arctic") },
++ { value: 6, text: qsTr("Remote Continental") },
++ { value: 7, text: qsTr("Rural") },
++ { value: 8, text: qsTr("Urban") }
++ ]
++ currentIndex: advancedSkyEffect.aerosol_type
++ textRole: "text"
++ valueRole: "value"
++ onActivated: advancedSkyEffect.aerosol_type = currentValue
++ }
++ Label {
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ text: "Eye altitude: " + eyeAltitudeSlider.value.toFixed(2)
++ }
++ Slider {
++ id: eyeAltitudeSlider
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ from: 0.0
++ to: 1.0
++ value: advancedSkyEffect.eye_altitude
++ stepSize: 0.01
++ onMoved: advancedSkyEffect.eye_altitude = value
++ }
++ Label {
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ text: "Ozone (Dobson units): " + ozoneSlider.value.toFixed(0)
++ }
++ Slider {
++ id: ozoneSlider
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ from: 100
++ to: 600
++ value: advancedSkyEffect.ozone_mean_monthly_dobson
++ stepSize: 10
++ onMoved: advancedSkyEffect.ozone_mean_monthly_dobson = value
++ }
++ Label {
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ text: "Aerosol Turbidity: " + turbiditySlider.value.toFixed(2)
++ }
++ Slider {
++ id: turbiditySlider
++ Layout.fillWidth: true
++ enabled: view3D.skyMaterialKind === 1
++ from: 0.0
++ to: 1.0
++ value: advancedSkyEffect.aerosol_turbidity
++ stepSize: 0.1
++ onMoved: advancedSkyEffect.aerosol_turbidity = value
++ }
++ }
++ }
++ }
++ }