1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4/*
5 Based on "sky.cpp" from the Godot engine v3
6 Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.
7 Copyright (c) 2014-2022 Godot Engine contributors.
8*/
9
10#include "proceduralskytexturedata_p.h"
11#include <QtQuick3DRuntimeRender/private/qssgrendertexturedata_p.h>
12#include <QtQuick3DRuntimeRender/private/qssgrenderimage_p.h>
13
14QT_BEGIN_NAMESPACE
15
16/*!
17 \qmltype ProceduralSkyTextureData
18 \inqmlmodule QtQuick3D.Helpers
19 \inherits TextureData
20 \brief Generates an HDR skybox cubemap.
21
22 This helper type provides an easy way to generate a lightprobe/skybox texture in HDR format. Note that
23 generating a lightprobe is an expensive process that can take significant time on embedded hardware.
24
25 The generated cubemap consists of three elements: the sky, the ground, and the sun. The sky and the
26 ground cover the top and bottom hemispheres. The position of the sun can be specified by setting
27 \l sunLatitude and \l sunLongitude.
28
29 \qml
30 View3D {
31 environment: SceneEnvironment {
32 backgroundMode: SceneEnvironment.SkyBox
33 lightProbe: Texture {
34 textureData: ProceduralSkyTextureData {
35 }
36 }
37 }
38 }
39 \endqml
40
41 \image sceneenvironment_lightprobe_proceduralsky.jpg
42
43 \sa SceneEnvironment
44*/
45
46/*! \qmlproperty color ProceduralSkyTextureData::skyTopColor
47 Specifies the sky color at the top of the skybox. The top half of the skybox has a gradient from \l skyHorizonColor to \c skyTopColor.
48 */
49
50/*! \qmlproperty color ProceduralSkyTextureData::skyHorizonColor
51 Specifies the sky color at the horizon. The top half of the skybox has a gradient from \c skyHorizonColor to \l skyTopColor.
52 */
53
54/*! \qmlproperty real ProceduralSkyTextureData::skyCurve
55 Modifies the curve of the sky gradient.
56 */
57
58/*! \qmlproperty real ProceduralSkyTextureData::skyEnergy
59 Specifies the intensity of the top half of the skybox. The sky gradient is multiplied with this factor.
60*/
61
62/*! \qmlproperty color ProceduralSkyTextureData::groundBottomColor
63 Specifies the ground color at the bottom of the skybox. The bottom half of the skybox has
64 a gradient from \l groundHorizonColor to \c groundBottomColor.
65*/
66
67/*! \qmlproperty color ProceduralSkyTextureData::groundHorizonColor
68 Specifies the ground color at the horizon. The bottom half of the skybox has
69 a gradient from \c groundHorizonColor to \l groundBottomColor.
70*/
71
72/*! \qmlproperty real ProceduralSkyTextureData::groundCurve
73 Modifies the curve of the ground gradient.
74*/
75
76/*! \qmlproperty real ProceduralSkyTextureData::groundEnergy
77 Specifies the intensity of the bottom half of the skybox. The ground gradient is multiplied with this factor.
78*/
79
80/*! \qmlproperty color ProceduralSkyTextureData::sunColor
81 Specifies the color of the sun.
82*/
83
84/*! \qmlproperty real ProceduralSkyTextureData::sunLatitude
85 Specifies the angle between the horizon and the sun position.
86 */
87
88/*! \qmlproperty real ProceduralSkyTextureData::sunLongitude
89 Specifies the angle between the forward direction and the sun position.
90*/
91
92/*! \qmlproperty real ProceduralSkyTextureData::sunAngleMin
93 Specifies the angle from the center of the sun to where it starts to fade.
94*/
95
96/*! \qmlproperty real ProceduralSkyTextureData::sunAngleMax
97 Specifies the angle from the center of the sun to where it fades out completely.
98*/
99
100/*! \qmlproperty real ProceduralSkyTextureData::sunCurve
101 Modifies the curve of the sun gradient.
102*/
103
104/*! \qmlproperty float ProceduralSkyTextureData::sunEnergy
105 Specifies the intensity of the sun.
106*/
107
108/*! \qmlproperty SkyTextureQuality ProceduralSkyTextureData::textureQuality
109 This property sets the quality of the sky texture. Supported values are:
110
111 \value ProceduralSkyTextureData.SkyTextureQualityLow Generate a 512x512 texture
112 \value ProceduralSkyTextureData.SkyTextureQualityMedium Generate a 1024x1024 texture
113 \value ProceduralSkyTextureData.SkyTextureQualityHigh Generate a 2048x2048 texture
114 \value ProceduralSkyTextureData.SkyTextureQualityVeryHigh Generate a 4096x4096 texture
115*/
116
117ProceduralSkyTextureData::ProceduralSkyTextureData()
118{
119 scheduleTextureUpdate();
120}
121
122ProceduralSkyTextureData::~ProceduralSkyTextureData()
123{
124}
125
126QColor ProceduralSkyTextureData::skyTopColor() const
127{
128 return m_skyTopColor;
129}
130
131QColor ProceduralSkyTextureData::skyHorizonColor() const
132{
133 return m_skyHorizonColor;
134}
135
136float ProceduralSkyTextureData::skyCurve() const
137{
138 return m_skyCurve;
139}
140
141float ProceduralSkyTextureData::skyEnergy() const
142{
143 return m_skyEnergy;
144}
145
146QColor ProceduralSkyTextureData::groundBottomColor() const
147{
148 return m_groundBottomColor;
149}
150
151QColor ProceduralSkyTextureData::groundHorizonColor() const
152{
153 return m_groundHorizonColor;
154}
155
156float ProceduralSkyTextureData::groundCurve() const
157{
158 return m_groundCurve;
159}
160
161float ProceduralSkyTextureData::groundEnergy() const
162{
163 return m_groundEnergy;
164}
165
166QColor ProceduralSkyTextureData::sunColor() const
167{
168 return m_sunColor;
169}
170
171float ProceduralSkyTextureData::sunLatitude() const
172{
173 return m_sunLatitude;
174}
175
176float ProceduralSkyTextureData::sunLongitude() const
177{
178 return m_sunLongitude;
179}
180
181float ProceduralSkyTextureData::sunAngleMin() const
182{
183 return m_sunAngleMin;
184}
185
186float ProceduralSkyTextureData::sunAngleMax() const
187{
188 return m_sunAngleMax;
189}
190
191float ProceduralSkyTextureData::sunCurve() const
192{
193 return m_sunCurve;
194}
195
196float ProceduralSkyTextureData::sunEnergy() const
197{
198 return m_sunEnergy;
199}
200
201ProceduralSkyTextureData::SkyTextureQuality ProceduralSkyTextureData::textureQuality() const
202{
203 return m_textureQuality;
204}
205
206void ProceduralSkyTextureData::setSkyTopColor(QColor skyTopColor)
207{
208 if (m_skyTopColor == skyTopColor)
209 return;
210
211 m_skyTopColor = skyTopColor;
212 emit skyTopColorChanged(skyTopColor: m_skyTopColor);
213 scheduleTextureUpdate();
214}
215
216void ProceduralSkyTextureData::setSkyHorizonColor(QColor skyHorizonColor)
217{
218 if (m_skyHorizonColor == skyHorizonColor)
219 return;
220
221 m_skyHorizonColor = skyHorizonColor;
222 emit skyHorizonColorChanged(skyHorizonColor: m_skyHorizonColor);
223 scheduleTextureUpdate();
224}
225
226void ProceduralSkyTextureData::setSkyCurve(float skyCurve)
227{
228 if (qFuzzyCompare(p1: m_skyCurve, p2: skyCurve))
229 return;
230
231 m_skyCurve = skyCurve;
232 emit skyCurveChanged(skyCurve: m_skyCurve);
233 scheduleTextureUpdate();
234}
235
236void ProceduralSkyTextureData::setSkyEnergy(float skyEnergy)
237{
238 if (qFuzzyCompare(p1: m_skyEnergy, p2: skyEnergy))
239 return;
240
241 m_skyEnergy = skyEnergy;
242 emit skyEnergyChanged(skyEnergy: m_skyEnergy);
243 scheduleTextureUpdate();
244}
245
246void ProceduralSkyTextureData::setGroundBottomColor(QColor groundBottomColor)
247{
248 if (m_groundBottomColor == groundBottomColor)
249 return;
250
251 m_groundBottomColor = groundBottomColor;
252 emit groundBottomColorChanged(groundBottomColor: m_groundBottomColor);
253 scheduleTextureUpdate();
254}
255
256void ProceduralSkyTextureData::setGroundHorizonColor(QColor groundHorizonColor)
257{
258 if (m_groundHorizonColor == groundHorizonColor)
259 return;
260
261 m_groundHorizonColor = groundHorizonColor;
262 emit groundHorizonColorChanged(groundHorizonColor: m_groundHorizonColor);
263 scheduleTextureUpdate();
264}
265
266void ProceduralSkyTextureData::setGroundCurve(float groundCurve)
267{
268 if (qFuzzyCompare(p1: m_groundCurve, p2: groundCurve))
269 return;
270
271 m_groundCurve = groundCurve;
272 emit groundCurveChanged(groundCurve: m_groundCurve);
273 scheduleTextureUpdate();
274}
275
276void ProceduralSkyTextureData::setGroundEnergy(float groundEnergy)
277{
278 if (qFuzzyCompare(p1: m_groundEnergy, p2: groundEnergy))
279 return;
280
281 m_groundEnergy = groundEnergy;
282 emit groundEnergyChanged(groundEnergy: m_groundEnergy);
283 scheduleTextureUpdate();
284}
285
286void ProceduralSkyTextureData::setSunColor(QColor sunColor)
287{
288 if (m_sunColor == sunColor)
289 return;
290
291 m_sunColor = sunColor;
292 emit sunColorChanged(sunColor: m_sunColor);
293 scheduleTextureUpdate();
294}
295
296void ProceduralSkyTextureData::setSunLatitude(float sunLatitude)
297{
298 if (qFuzzyCompare(p1: m_sunLatitude, p2: sunLatitude))
299 return;
300
301 m_sunLatitude = sunLatitude;
302 emit sunLatitudeChanged(sunLatitude: m_sunLatitude);
303 scheduleTextureUpdate();
304}
305
306void ProceduralSkyTextureData::setSunLongitude(float sunLongitude)
307{
308 if (qFuzzyCompare(p1: m_sunLongitude, p2: sunLongitude))
309 return;
310
311 m_sunLongitude = sunLongitude;
312 emit sunLongitudeChanged(sunLongitude: m_sunLongitude);
313 scheduleTextureUpdate();
314}
315
316void ProceduralSkyTextureData::setSunAngleMin(float sunAngleMin)
317{
318 if (qFuzzyCompare(p1: m_sunAngleMin, p2: sunAngleMin))
319 return;
320
321 m_sunAngleMin = sunAngleMin;
322 emit sunAngleMinChanged(sunAngleMin: m_sunAngleMin);
323 scheduleTextureUpdate();
324}
325
326void ProceduralSkyTextureData::setSunAngleMax(float sunAngleMax)
327{
328 if (qFuzzyCompare(p1: m_sunAngleMax, p2: sunAngleMax))
329 return;
330
331 m_sunAngleMax = sunAngleMax;
332 emit sunAngleMaxChanged(sunAngleMax: m_sunAngleMax);
333 scheduleTextureUpdate();
334}
335
336void ProceduralSkyTextureData::setSunCurve(float sunCurve)
337{
338 if (qFuzzyCompare(p1: m_sunCurve, p2: sunCurve))
339 return;
340
341 m_sunCurve = sunCurve;
342 emit sunCurveChanged(sunCurve: m_sunCurve);
343 scheduleTextureUpdate();
344}
345
346void ProceduralSkyTextureData::setSunEnergy(float sunEnergy)
347{
348 if (qFuzzyCompare(p1: m_sunEnergy, p2: sunEnergy))
349 return;
350
351 m_sunEnergy = sunEnergy;
352 emit sunEnergyChanged(sunEnergy: m_sunEnergy);
353 scheduleTextureUpdate();
354}
355
356void ProceduralSkyTextureData::setTextureQuality(ProceduralSkyTextureData::SkyTextureQuality textureQuality)
357{
358 if (m_textureQuality == textureQuality)
359 return;
360
361 m_textureQuality = textureQuality;
362 emit textureQualityChanged(textureQuality: m_textureQuality);
363 scheduleTextureUpdate();
364}
365
366void ProceduralSkyTextureData::generateRGBA16FTexture()
367{
368 int size = 0;
369 switch (m_textureQuality) {
370 case SkyTextureQuality::SkyTextureQualityLow:
371 size = 512;
372 break;
373 case SkyTextureQuality::SkyTextureQualityMedium:
374 size = 1024;
375 break;
376 case SkyTextureQuality::SkyTextureQualityHigh:
377 size = 2048;
378 break;
379 case SkyTextureQuality::SkyTextureQualityVeryHigh:
380 size = 4096;
381 break;
382 }
383
384 const int width = size;
385 const int height = width / 2;
386 setSize(QSize(width, height));
387 setFormat(Format::RGBA16F);
388 setHasTransparency(false);
389 const int dataSize = width * height * 4 * 2; // 2 bytes per channel
390 QByteArray imageData;
391 imageData.resize(size: dataSize);
392 generateSkyTexture(width, height, imageData, isRGBE: false);
393 setTextureData(imageData);
394}
395
396QByteArray ProceduralSkyTextureData::generateSkyTexture(int width, int height, QByteArray &imageData, bool isRGBE) const
397{
398 quint32 *data = reinterpret_cast<quint32 *>(imageData.data());
399
400 LinearColor skyTopLinear(m_skyTopColor);
401 LinearColor skyHorizonLinear(m_skyHorizonColor);
402 LinearColor groundBottomLinear(m_groundBottomColor);
403 LinearColor groundHorizonLinear(m_groundHorizonColor);
404 LinearColor sunLinear(m_sunColor);
405 sunLinear.r *= m_sunEnergy;
406 sunLinear.g *= m_sunEnergy;
407 sunLinear.b *= m_sunEnergy;
408
409 QVector3D sun(0, 0, -1);
410
411 sun = QQuaternion::fromAxisAndAngle(axis: QVector3D(1, 0, 0), angle: m_sunLatitude) * sun;
412 sun = QQuaternion::fromAxisAndAngle(axis: QVector3D(0, 1, 0), angle: m_sunLongitude) * sun;
413 sun.normalize();
414
415 auto clamp = [](float value, float min, float max) {
416 if (value < min)
417 return min;
418 else if (value > max)
419 return max;
420 return value;
421 };
422
423 auto ease = [](float x, float c) {
424 if (x < 0.0f)
425 x = 0.0f;
426 else if (x > 1.0f)
427 x = 1.0f;
428 if (c > 0.0f) {
429 if (c < 1.0f) {
430 return 1.0f - qPow(x: 1.0f - x, y: 1.0f / c);
431 } else {
432 return qPow(x, y: c);
433 }
434 } else if (c < 0.0f) {
435 if (x < 0.5f) {
436 return qPow(x: x * 2.0f, y: -c) * 0.5f;
437 } else {
438 return (1.0f - qPow(x: 1.0f - (x - 0.5f) * 2.0f, y: -c)) * 0.5f + 0.5f;
439 }
440 } else
441 return 0.0f;
442 };
443
444 for (int i = 0; i < width; i++) {
445
446 float u = float(i) / (width - 1);
447 float phi = u * 2.0 * M_PI;
448
449 for (int j = 0; j < height; j++) {
450 float v = float(j) / (height - 1);
451 float theta = v * M_PI;
452
453 QVector3D normal(qSin(v: phi) * qSin(v: theta) * -1.0,
454 qCos(v: theta),
455 qCos(v: phi) * qSin(v: theta) * -1.0);
456 normal.normalize();
457 float vAngle = qAcos(v: clamp(normal.y(), -1.0, 1.0));
458 LinearColor color;
459
460 if (normal.y() < 0) {
461 // Ground color
462 float c = (vAngle - (M_PI * 0.5f)) / (M_PI * 0.5f);
463 color = groundHorizonLinear.interpolate(color: groundBottomLinear, value: ease(c, m_groundCurve));
464 color.r *= m_groundEnergy;
465 color.g *= m_groundEnergy;
466 color.b *= m_groundEnergy;
467 } else {
468 // Sky color
469 float c = vAngle / (M_PI * 0.5f);
470 color = skyHorizonLinear.interpolate(color: skyTopLinear, value: ease(1.0 - c, m_skyCurve));
471 color.r *= m_skyEnergy;
472 color.g *= m_skyEnergy;
473 color.b *= m_skyEnergy;
474
475 float sunAngle = qRadiansToDegrees(radians: qAcos(v: clamp(QVector3D::dotProduct(v1: sun, v2: normal), -1.0f, 1.0f)));
476 if (sunAngle < m_sunAngleMin) {
477 color = color.blend(color: sunLinear);
478 } else if (sunAngle < m_sunAngleMax) {
479 float c2 = (sunAngle - m_sunAngleMin) / (m_sunAngleMax - m_sunAngleMin);
480 c2 = ease(c2, m_sunCurve);
481 color = color.blend(color: sunLinear).interpolate(color, value: c2);
482 }
483 }
484
485 // Write from bottom to top
486 if (isRGBE) {
487 data[(height - j - 1) * width + i] = color.toRGBE8();
488 } else {
489 // RGBA16F
490 const int offset = ((height - j - 1) * width + i) * 2;
491 qfloat16 *fData = reinterpret_cast<qfloat16 *>(data + offset);
492 float pixel[4] = {color.r, color.g, color.b, color.a };
493 qFloatToFloat16(fData, pixel, length: 4);
494 }
495 }
496 }
497
498 return imageData;
499}
500
501void ProceduralSkyTextureData::scheduleTextureUpdate()
502{
503 generateRGBA16FTexture();
504}
505
506ProceduralSkyTextureData::LinearColor::LinearColor(const QColor &color)
507{
508 const float red = color.redF();
509 const float green = color.greenF();
510 const float blue = color.blueF();
511 const float alpha = color.alphaF();
512
513 r = red < 0.04045 ? red * (1.0 / 12.92) : qPow(x: (red + 0.055) * (1.0 / (1 + 0.055)), y: 2.4),
514 g = green < 0.04045 ? green * (1.0 / 12.92) : qPow(x: (green + 0.055) * (1.0 / (1 + 0.055)), y: 2.4),
515 b = blue < 0.04045 ? blue * (1.0 / 12.92) : qPow(x: (blue + 0.055) * (1.0 / (1 + 0.055)), y: 2.4),
516 a = alpha;
517}
518
519ProceduralSkyTextureData::LinearColor ProceduralSkyTextureData::LinearColor::interpolate(const ProceduralSkyTextureData::LinearColor &color, float value) const
520{
521 LinearColor copy = *this;
522
523 copy.r += (value * (color.r - r));
524 copy.g += (value * (color.g - g));
525 copy.b += (value * (color.b - b));
526 copy.a += (value * (color.a - a));
527
528 return copy;
529}
530
531ProceduralSkyTextureData::LinearColor ProceduralSkyTextureData::LinearColor::blend(const ProceduralSkyTextureData::LinearColor &color) const
532{
533 LinearColor copy;
534 float sa = 1.0 - color.a;
535 copy.a = a * sa + color.a;
536 if (copy.a == 0) {
537 return LinearColor();
538 } else {
539 copy.r = (r * a * sa + color.r * color.a) / copy.a;
540 copy.g = (g * a * sa + color.g * color.a) / copy.a;
541 copy.b = (b * a * sa + color.b * color.a) / copy.a;
542 }
543 return copy;
544}
545
546quint32 ProceduralSkyTextureData::LinearColor::toRGBA8() const
547{
548 return (quint32(lrintf(x: r)) & 0xFF) |
549 ((quint32(lrintf(x: g)) & 0xFF) << 8) |
550 ((quint32(lrintf(x: b)) & 0xFF) << 16) |
551 ((quint32(lrintf(x: a)) & 0xFF) << 24);
552}
553
554quint32 ProceduralSkyTextureData::LinearColor::toRGBE8() const
555{
556 float v = 0.0f;
557 int exp = 0;
558
559 v = r;
560 if (g > v)
561 v = g;
562 if (b > v)
563 v = b;
564
565 v = frexp(x: v, exponent: &exp) * 256.0f / v;
566 quint32 result = 0;
567 quint8 *components = reinterpret_cast<quint8*>(&result);
568 components[0] = quint8(r * v);
569 components[1] = quint8(g * v);
570 components[2] = quint8(b * v);
571 components[3] = quint8(exp + 128);
572 return result;
573}
574
575quint32 ProceduralSkyTextureData::LinearColor::toRGBE9995() const
576{
577 const float pow2to9 = 512.0f;
578 const float B = 15.0f;
579 const float N = 9.0f;
580
581 float sharedExp = 65408.000f;
582
583 float cRed = qMax(a: 0.0f, b: qMin(a: sharedExp, b: r));
584 float cGreen = qMax(a: 0.0f, b: qMin(a: sharedExp, b: g));
585 float cBlue = qMax(a: 0.0f, b: qMin(a: sharedExp, b));
586
587 float cMax = qMax(a: cRed, b: qMax(a: cGreen, b: cBlue));
588
589 float expp = qMax(a: -B - 1.0f, b: floor(x: std::log(x: cMax) / M_LN2)) + 1.0f + B;
590
591 float sMax = (float)floor(x: (cMax / qPow(x: 2.0f, y: expp - B - N)) + 0.5f);
592
593 float exps = expp + 1.0f;
594
595 if (0.0 <= sMax && sMax < pow2to9) {
596 exps = expp;
597 }
598
599 float sRed = qFloor(v: (cRed / pow(x: 2.0f, y: exps - B - N)) + 0.5f);
600 float sGreen = qFloor(v: (cGreen / pow(x: 2.0f, y: exps - B - N)) + 0.5f);
601 float sBlue = qFloor(v: (cBlue / pow(x: 2.0f, y: exps - B - N)) + 0.5f);
602
603 return (quint32(lrintf(x: sRed)) & 0x1FF) |
604 ((quint32(lrintf(x: sGreen)) & 0x1FF) << 9) |
605 ((quint32(lrintf(x: sBlue)) & 0x1FF) << 18) |
606 ((quint32(lrintf(x: exps)) & 0x1F) << 27);
607}
608
609QT_END_NAMESPACE
610

source code of qtquick3d/src/helpers/proceduralskytexturedata.cpp