| 1 | // Copyright (C) 2019 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "qquick3dabstractlight_p.h" |
| 5 | #include "qquick3dobject_p.h" |
| 6 | #include "qquick3dnode_p_p.h" |
| 7 | |
| 8 | #include <QtQuick3DRuntimeRender/private/qssgrenderlight_p.h> |
| 9 | #include <QtQuick3DUtils/private/qssgutils_p.h> |
| 10 | |
| 11 | QT_BEGIN_NAMESPACE |
| 12 | |
| 13 | /*! |
| 14 | \qmltype Light |
| 15 | \inherits Node |
| 16 | \inqmlmodule QtQuick3D |
| 17 | \brief An uncreatable abstract base type for all lights. |
| 18 | |
| 19 | Light itself is an uncreatable base for all of its subtypes. The subtypes provide multiple |
| 20 | options to determine the style of the light. |
| 21 | |
| 22 | For usage examples, see \l{Qt Quick 3D - Lights Example}. |
| 23 | |
| 24 | \sa DirectionalLight, PointLight |
| 25 | */ |
| 26 | |
| 27 | /*! |
| 28 | \qmlproperty color Light::color |
| 29 | This property defines the color applied to models illuminated by this light. |
| 30 | The default value is white, rgb(255, 255, 255). |
| 31 | */ |
| 32 | |
| 33 | /*! |
| 34 | \qmlproperty color Light::ambientColor |
| 35 | The property defines the ambient color applied to materials before being lit by this light. |
| 36 | The default value is black, rgb(0, 0, 0). |
| 37 | */ |
| 38 | |
| 39 | /*! |
| 40 | \qmlproperty real Light::brightness |
| 41 | This property defines an overall multiplier for this light’s effects. |
| 42 | The default value is 1. |
| 43 | */ |
| 44 | |
| 45 | /*! |
| 46 | \qmlproperty Node Light::scope |
| 47 | |
| 48 | The property allows the selection of a Node in the scene. Only that node |
| 49 | and its children are affected by this light. By default the value is null, |
| 50 | which indicates no scope selected. |
| 51 | |
| 52 | \note Scoped lights cannot cast real-time shadows, meaning a Light with a |
| 53 | scope set should not set \l castsShadow to true. They can however generate |
| 54 | baked shadows when \l bakeMode is set to Light.BakeModeAll. |
| 55 | */ |
| 56 | |
| 57 | /*! |
| 58 | \qmlproperty bool Light::castsShadow |
| 59 | |
| 60 | When this property is enabled, the light will cast (real-time) shadows. The |
| 61 | default value is false. |
| 62 | |
| 63 | \note When \l bakeMode is set to Light.BakeModeAll, this property has no |
| 64 | effect. A fully baked light always has baked shadows, but it will never |
| 65 | participate in real-time shadow mapping. |
| 66 | */ |
| 67 | |
| 68 | /*! |
| 69 | \qmlproperty real Light::shadowBias |
| 70 | This property is used to tweak the shadowing effect when objects |
| 71 | are casting shadows on themselves. The value tries to approximate the offset |
| 72 | in world space so it needs to be tweaked depending on the size of your scene. |
| 73 | |
| 74 | The default value is \c{10} |
| 75 | */ |
| 76 | |
| 77 | /*! |
| 78 | \qmlproperty real Light::shadowFactor |
| 79 | This property determines how dark the cast shadows should be. The value range is [0, 100], where |
| 80 | 0 means no shadows and 100 means the light is fully shadowed. |
| 81 | |
| 82 | The default value is \c{75}. |
| 83 | */ |
| 84 | |
| 85 | /*! |
| 86 | \qmlproperty enumeration Light::shadowMapQuality |
| 87 | The property sets the quality of the shadow map created for shadow rendering. Lower quality uses |
| 88 | less resources, but produces lower quality shadows while higher quality uses more resources, but |
| 89 | produces better quality shadows. |
| 90 | |
| 91 | Supported quality values are: |
| 92 | \value Light.ShadowMapQualityLow Render shadowmap using 256x256 texture. |
| 93 | \value Light.ShadowMapQualityMedium Render shadowmap using 512x512 texture. |
| 94 | \value Light.ShadowMapQualityHigh Render shadowmap using 1024x1024 texture. |
| 95 | \value Light.ShadowMapQualityVeryHigh Render shadowmap using 2048x2048 texture. |
| 96 | |
| 97 | The default value is \c Light.ShadowMapQualityLow |
| 98 | */ |
| 99 | |
| 100 | /*! |
| 101 | \qmlproperty real Light::shadowMapFar |
| 102 | The property determines the maximum distance for the shadow map. Smaller |
| 103 | values improve the precision and effects of the map. |
| 104 | The default value is 5000. Unit is points in local coordinate space. |
| 105 | */ |
| 106 | |
| 107 | /*! |
| 108 | \qmlproperty real Light::shadowFilter |
| 109 | This property sets how much blur is applied to the shadows. |
| 110 | |
| 111 | The default value is 5. |
| 112 | |
| 113 | \deprecated [6.8] No longer used for anything, use \l{Light::}{pcfFactor} instead. |
| 114 | |
| 115 | \sa Light::softShadowQuality |
| 116 | */ |
| 117 | |
| 118 | /*! |
| 119 | \qmlproperty enumeration Light::bakeMode |
| 120 | The property controls if the light is active in baked lighting, such as |
| 121 | when generating lightmaps. |
| 122 | |
| 123 | \value Light.BakeModeDisabled The light is not used in baked lighting. |
| 124 | |
| 125 | \value Light.BakeModeIndirect Indirect lighting contribution (for global |
| 126 | illumination) is baked for this light. Direct lighting (diffuse, specular, |
| 127 | real-time shadow mapping) is calculated normally for the light at run time. |
| 128 | At run time, when not in baking mode, the renderer will attempt to sample |
| 129 | the lightmap to get the indirect lighting data and combine that with the |
| 130 | results of the real-time calculations. |
| 131 | |
| 132 | \value Light.BakeModeAll Both direct (diffuse, shadow) and indirect |
| 133 | lighting is baked for this light. The light will not have a specular |
| 134 | contribution and will not generate realtime shadow maps, but it will always |
| 135 | have baked shadows. At run time, when not in baking mode, the renderer will |
| 136 | attempt to sample the lightmap in place of the standard, real-time |
| 137 | calculations for diffuse lighting and shadow mapping. |
| 138 | |
| 139 | The default value is \c Light.BakeModeDisabled |
| 140 | |
| 141 | \note Just as with \l Model::usedInBakedLighting, designers and developers |
| 142 | must always evaluate on a per-light basis if the light is suitable to take |
| 143 | part in baked lighting. |
| 144 | |
| 145 | \warning Lights with dynamically changing properties, for example, animated |
| 146 | position, rotation, or other properties, are not suitable for participating |
| 147 | in baked lighting. |
| 148 | |
| 149 | This property is relevant both when baking and when using lightmaps. A |
| 150 | consistent state between the baking run and the subsequent runs that use |
| 151 | the generated data is essential. Changing to a different value will not |
| 152 | change the previously generated and persistently stored data in the |
| 153 | lightmaps, the engine's rendering behavior will however follow the |
| 154 | property's current value. |
| 155 | |
| 156 | For more information on how to bake lightmaps, see the \l {Lightmaps and |
| 157 | Global Illumination}. |
| 158 | |
| 159 | \sa Model::usedInBakedLighting, Model::bakedLightmap, Lightmapper, {Lightmaps and Global Illumination} |
| 160 | */ |
| 161 | |
| 162 | /*! |
| 163 | \qmlproperty enumeration Light::softShadowQuality |
| 164 | \since 6.8 |
| 165 | |
| 166 | The property controls the soft shadow quality. |
| 167 | |
| 168 | \value Light.Hard No soft shadows. |
| 169 | \value Light.PCF4 Percentage-closer filtering soft shadows with 4 samples. |
| 170 | \value Light.PCF8 Percentage-closer filtering soft shadows with 8 samples. |
| 171 | \value Light.PCF16 Percentage-closer filtering soft shadows with 16 samples. |
| 172 | \value Light.PCF32 Percentage-closer filtering soft shadows with 32 samples. |
| 173 | \value Light.PCF64 Percentage-closer filtering soft shadows with 64 samples. |
| 174 | |
| 175 | Default value: \c Light.PCF4 |
| 176 | |
| 177 | \sa Light::pcfFactor, Light::shadowFilter |
| 178 | */ |
| 179 | |
| 180 | /*! |
| 181 | \qmlproperty real Light::pcfFactor |
| 182 | \since 6.8 |
| 183 | |
| 184 | The property controls the PCF (percentage-closer filtering) factor. This |
| 185 | value tries to approximate the radius of a PCF filtering in world space. |
| 186 | |
| 187 | \note PCF needs to be set in \l{Light::}{softShadowQuality} for this property |
| 188 | to have an effect. |
| 189 | |
| 190 | Default value: \c{2.0} |
| 191 | |
| 192 | \sa Light::softShadowQuality |
| 193 | */ |
| 194 | |
| 195 | QQuick3DAbstractLight::QQuick3DAbstractLight(QQuick3DNodePrivate &dd, QQuick3DNode *parent) |
| 196 | : QQuick3DNode(dd, parent) |
| 197 | , m_color(Qt::white) |
| 198 | , m_ambientColor(Qt::black) {} |
| 199 | |
| 200 | QQuick3DAbstractLight::~QQuick3DAbstractLight() {} |
| 201 | |
| 202 | QColor QQuick3DAbstractLight::color() const |
| 203 | { |
| 204 | return m_color; |
| 205 | } |
| 206 | |
| 207 | QColor QQuick3DAbstractLight::ambientColor() const |
| 208 | { |
| 209 | return m_ambientColor; |
| 210 | } |
| 211 | |
| 212 | float QQuick3DAbstractLight::brightness() const |
| 213 | { |
| 214 | return m_brightness; |
| 215 | } |
| 216 | |
| 217 | QQuick3DNode *QQuick3DAbstractLight::scope() const |
| 218 | { |
| 219 | return m_scope; |
| 220 | } |
| 221 | |
| 222 | bool QQuick3DAbstractLight::castsShadow() const |
| 223 | { |
| 224 | return m_castsShadow; |
| 225 | } |
| 226 | |
| 227 | float QQuick3DAbstractLight::shadowBias() const |
| 228 | { |
| 229 | return m_shadowBias; |
| 230 | } |
| 231 | |
| 232 | float QQuick3DAbstractLight::shadowFactor() const |
| 233 | { |
| 234 | return m_shadowFactor; |
| 235 | } |
| 236 | |
| 237 | QQuick3DAbstractLight::QSSGShadowMapQuality QQuick3DAbstractLight::shadowMapQuality() const |
| 238 | { |
| 239 | return m_shadowMapQuality; |
| 240 | } |
| 241 | |
| 242 | QQuick3DAbstractLight::QSSGSoftShadowQuality QQuick3DAbstractLight::softShadowQuality() const |
| 243 | { |
| 244 | return m_softShadowQuality; |
| 245 | } |
| 246 | |
| 247 | float QQuick3DAbstractLight::shadowMapFar() const |
| 248 | { |
| 249 | return m_shadowMapFar; |
| 250 | } |
| 251 | |
| 252 | float QQuick3DAbstractLight::shadowFilter() const |
| 253 | { |
| 254 | return m_shadowFilter; |
| 255 | } |
| 256 | |
| 257 | QQuick3DAbstractLight::QSSGBakeMode QQuick3DAbstractLight::bakeMode() const |
| 258 | { |
| 259 | return m_bakeMode; |
| 260 | } |
| 261 | |
| 262 | float QQuick3DAbstractLight::pcfFactor() const |
| 263 | { |
| 264 | return m_pcfFactor; |
| 265 | } |
| 266 | |
| 267 | void QQuick3DAbstractLight::markAllDirty() |
| 268 | { |
| 269 | m_dirtyFlags = DirtyFlags(DirtyFlag::ShadowDirty) |
| 270 | | DirtyFlags(DirtyFlag::ColorDirty) |
| 271 | | DirtyFlags(DirtyFlag::BrightnessDirty) |
| 272 | | DirtyFlags(DirtyFlag::FadeDirty) |
| 273 | | DirtyFlags(DirtyFlag::AreaDirty) |
| 274 | | DirtyFlags(DirtyFlag::BakeModeDirty); |
| 275 | QQuick3DNode::markAllDirty(); |
| 276 | } |
| 277 | |
| 278 | void QQuick3DAbstractLight::setColor(const QColor &color) |
| 279 | { |
| 280 | if (m_color == color) |
| 281 | return; |
| 282 | |
| 283 | m_color = color; |
| 284 | m_dirtyFlags.setFlag(flag: DirtyFlag::ColorDirty); |
| 285 | emit colorChanged(); |
| 286 | update(); |
| 287 | } |
| 288 | |
| 289 | void QQuick3DAbstractLight::setAmbientColor(const QColor &ambientColor) |
| 290 | { |
| 291 | if (m_ambientColor == ambientColor) |
| 292 | return; |
| 293 | |
| 294 | m_ambientColor = ambientColor; |
| 295 | m_dirtyFlags.setFlag(flag: DirtyFlag::ColorDirty); |
| 296 | emit ambientColorChanged(); |
| 297 | update(); |
| 298 | } |
| 299 | |
| 300 | void QQuick3DAbstractLight::setBrightness(float brightness) |
| 301 | { |
| 302 | if (qFuzzyCompare(p1: m_brightness, p2: brightness)) |
| 303 | return; |
| 304 | |
| 305 | m_brightness = brightness; |
| 306 | m_dirtyFlags.setFlag(flag: DirtyFlag::BrightnessDirty); |
| 307 | emit brightnessChanged(); |
| 308 | update(); |
| 309 | } |
| 310 | |
| 311 | void QQuick3DAbstractLight::setScope(QQuick3DNode *scope) |
| 312 | { |
| 313 | if (m_scope == scope) |
| 314 | return; |
| 315 | |
| 316 | m_scope = scope; |
| 317 | emit scopeChanged(); |
| 318 | update(); |
| 319 | } |
| 320 | |
| 321 | void QQuick3DAbstractLight::setCastsShadow(bool castsShadow) |
| 322 | { |
| 323 | if (m_castsShadow == castsShadow) |
| 324 | return; |
| 325 | |
| 326 | m_castsShadow = castsShadow; |
| 327 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
| 328 | emit castsShadowChanged(); |
| 329 | update(); |
| 330 | } |
| 331 | |
| 332 | void QQuick3DAbstractLight::setShadowBias(float shadowBias) |
| 333 | { |
| 334 | if (qFuzzyCompare(p1: m_shadowBias, p2: shadowBias)) |
| 335 | return; |
| 336 | |
| 337 | m_shadowBias = shadowBias; |
| 338 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
| 339 | emit shadowBiasChanged(); |
| 340 | update(); |
| 341 | } |
| 342 | |
| 343 | void QQuick3DAbstractLight::setShadowFactor(float shadowFactor) |
| 344 | { |
| 345 | shadowFactor = qBound(min: 0.0f, val: shadowFactor, max: 100.0f); |
| 346 | if (qFuzzyCompare(p1: m_shadowFactor, p2: shadowFactor)) |
| 347 | return; |
| 348 | |
| 349 | m_shadowFactor = shadowFactor; |
| 350 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
| 351 | emit shadowFactorChanged(); |
| 352 | update(); |
| 353 | } |
| 354 | |
| 355 | void QQuick3DAbstractLight::setShadowMapQuality( |
| 356 | QQuick3DAbstractLight::QSSGShadowMapQuality shadowMapQuality) |
| 357 | { |
| 358 | if (m_shadowMapQuality == shadowMapQuality) |
| 359 | return; |
| 360 | |
| 361 | m_shadowMapQuality = shadowMapQuality; |
| 362 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
| 363 | emit shadowMapQualityChanged(); |
| 364 | update(); |
| 365 | } |
| 366 | |
| 367 | void QQuick3DAbstractLight::setSoftShadowQuality(QSSGSoftShadowQuality softShadowQuality) |
| 368 | { |
| 369 | if (m_softShadowQuality == softShadowQuality) |
| 370 | return; |
| 371 | |
| 372 | m_softShadowQuality = softShadowQuality; |
| 373 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
| 374 | emit softShadowQualityChanged(); |
| 375 | update(); |
| 376 | } |
| 377 | |
| 378 | void QQuick3DAbstractLight::setBakeMode(QQuick3DAbstractLight::QSSGBakeMode bakeMode) |
| 379 | { |
| 380 | if (m_bakeMode == bakeMode) |
| 381 | return; |
| 382 | |
| 383 | m_bakeMode = bakeMode; |
| 384 | m_dirtyFlags.setFlag(flag: DirtyFlag::BakeModeDirty); |
| 385 | emit bakeModeChanged(); |
| 386 | update(); |
| 387 | } |
| 388 | |
| 389 | void QQuick3DAbstractLight::setPcfFactor(float pcfFactor) |
| 390 | { |
| 391 | if (m_pcfFactor == pcfFactor) |
| 392 | return; |
| 393 | |
| 394 | m_pcfFactor = pcfFactor; |
| 395 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
| 396 | emit pcfFactorChanged(); |
| 397 | update(); |
| 398 | } |
| 399 | |
| 400 | void QQuick3DAbstractLight::setShadowMapFar(float shadowMapFar) |
| 401 | { |
| 402 | if (qFuzzyCompare(p1: m_shadowMapFar, p2: shadowMapFar)) |
| 403 | return; |
| 404 | |
| 405 | m_shadowMapFar = shadowMapFar; |
| 406 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
| 407 | emit shadowMapFarChanged(); |
| 408 | update(); |
| 409 | } |
| 410 | |
| 411 | void QQuick3DAbstractLight::setShadowFilter(float shadowFilter) |
| 412 | { |
| 413 | if (qFuzzyCompare(p1: m_shadowFilter, p2: shadowFilter)) |
| 414 | return; |
| 415 | |
| 416 | m_shadowFilter = shadowFilter; |
| 417 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
| 418 | emit shadowFilterChanged(); |
| 419 | update(); |
| 420 | } |
| 421 | |
| 422 | quint32 QQuick3DAbstractLight::mapToShadowResolution(QSSGShadowMapQuality quality) |
| 423 | { |
| 424 | switch (quality) { |
| 425 | case QSSGShadowMapQuality::ShadowMapQualityMedium: |
| 426 | return 512; |
| 427 | case QSSGShadowMapQuality::ShadowMapQualityHigh: |
| 428 | return 1024; |
| 429 | case QSSGShadowMapQuality::ShadowMapQualityVeryHigh: |
| 430 | return 2048; |
| 431 | default: |
| 432 | break; |
| 433 | } |
| 434 | return 256; |
| 435 | } |
| 436 | |
| 437 | QSSGRenderGraphObject *QQuick3DAbstractLight::updateSpatialNode(QSSGRenderGraphObject *node) |
| 438 | { |
| 439 | Q_ASSERT_X(node, __FUNCTION__, "Node must have been created in parent class." ); |
| 440 | |
| 441 | QQuick3DNode::updateSpatialNode(node); |
| 442 | |
| 443 | QSSGRenderLight *light = static_cast<QSSGRenderLight *>(node); |
| 444 | |
| 445 | if (m_dirtyFlags.toInt() != 0) // Some flag was set, so mark the light dirty! |
| 446 | light->markDirty(dirtyFlag: QSSGRenderLight::DirtyFlag::LightDirty); |
| 447 | |
| 448 | if (m_dirtyFlags.testFlag(flag: DirtyFlag::ColorDirty)) { |
| 449 | m_dirtyFlags.setFlag(flag: DirtyFlag::ColorDirty, on: false); |
| 450 | light->m_diffuseColor = QSSGUtils::color::sRGBToLinear(color: m_color).toVector3D(); |
| 451 | light->m_specularColor = light->m_diffuseColor; |
| 452 | light->m_ambientColor = QSSGUtils::color::sRGBToLinear(color: m_ambientColor).toVector3D(); |
| 453 | } |
| 454 | |
| 455 | if (m_dirtyFlags.testFlag(flag: DirtyFlag::BrightnessDirty)) { |
| 456 | m_dirtyFlags.setFlag(flag: DirtyFlag::BrightnessDirty, on: false); |
| 457 | light->m_brightness = m_brightness; |
| 458 | } |
| 459 | |
| 460 | if (m_dirtyFlags.testFlag(flag: DirtyFlag::ShadowDirty)) { |
| 461 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty, on: false); |
| 462 | light->m_castShadow = m_castsShadow; |
| 463 | light->m_shadowBias = m_shadowBias; |
| 464 | light->m_shadowFactor = m_shadowFactor; |
| 465 | light->m_shadowMapRes = mapToShadowResolution(quality: m_shadowMapQuality); |
| 466 | light->m_softShadowQuality = static_cast<QSSGRenderLight::SoftShadowQuality>(m_softShadowQuality); |
| 467 | light->m_shadowMapFar = m_shadowMapFar; |
| 468 | light->m_shadowFilter = m_shadowFilter; |
| 469 | light->m_pcfFactor = m_pcfFactor; |
| 470 | } |
| 471 | |
| 472 | if (m_dirtyFlags.testFlag(flag: DirtyFlag::BakeModeDirty)) { |
| 473 | m_dirtyFlags.setFlag(flag: DirtyFlag::BakeModeDirty, on: false); |
| 474 | light->m_bakingEnabled = m_bakeMode != QSSGBakeMode::BakeModeDisabled; |
| 475 | light->m_fullyBaked = m_bakeMode == QSSGBakeMode::BakeModeAll; |
| 476 | } |
| 477 | |
| 478 | if (m_scope) { |
| 479 | // Special case: |
| 480 | // If the 'scope' is 'this' and this is the first call, then the spatial node is the one we just created. |
| 481 | // This is not unlikely, as it can make sense to put all child nodes that should receive light under the light node... |
| 482 | if (m_scope == this) |
| 483 | light->m_scope = light; |
| 484 | else |
| 485 | light->m_scope = static_cast<QSSGRenderNode*>(QQuick3DObjectPrivate::get(item: m_scope)->spatialNode); |
| 486 | } else { |
| 487 | light->m_scope = nullptr; |
| 488 | } |
| 489 | |
| 490 | return node; |
| 491 | } |
| 492 | |
| 493 | QT_END_NAMESPACE |
| 494 | |