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 range is [-1.0, 1.0]. Generally, a value |
72 | inside [-0.1, 0.1] is sufficient. |
73 | The default value is 0. |
74 | */ |
75 | |
76 | /*! |
77 | \qmlproperty real Light::shadowFactor |
78 | This property determines how dark the cast shadows should be. The value range is [0, 100], where |
79 | 0 means no shadows and 100 means the light is fully shadowed. |
80 | The default value is 5. |
81 | */ |
82 | |
83 | /*! |
84 | \qmlproperty enumeration Light::shadowMapQuality |
85 | The property sets the quality of the shadow map created for shadow rendering. Lower quality uses |
86 | less resources, but produces lower quality shadows while higher quality uses more resources, but |
87 | produces better quality shadows. |
88 | |
89 | Supported quality values are: |
90 | \value Light.ShadowMapQualityLow Render shadowmap using 256x256 texture. |
91 | \value Light.ShadowMapQualityMedium Render shadowmap using 512x512 texture. |
92 | \value Light.ShadowMapQualityHigh Render shadowmap using 1024x1024 texture. |
93 | \value Light.ShadowMapQualityVeryHigh Render shadowmap using 2048x2048 texture. |
94 | |
95 | The default value is \c Light.ShadowMapQualityLow |
96 | */ |
97 | |
98 | /*! |
99 | \qmlproperty real Light::shadowMapFar |
100 | The property determines the maximum distance for the shadow map. Smaller |
101 | values improve the precision and effects of the map. |
102 | The default value is 5000. Unit is points in local coordinate space. |
103 | */ |
104 | |
105 | /*! |
106 | \qmlproperty real Light::shadowFilter |
107 | This property sets how much blur is applied to the shadows. |
108 | The default value is 5. |
109 | */ |
110 | |
111 | /*! |
112 | \qmlproperty enumeration Light::bakeMode |
113 | The property controls if the light is active in baked lighting, such as |
114 | when generating lightmaps. |
115 | |
116 | \value Light.BakeModeDisabled The light is not used in baked lighting. |
117 | |
118 | \value Light.BakeModeIndirect Indirect lighting contribution (for global |
119 | illumination) is baked for this light. Direct lighting (diffuse, specular, |
120 | real-time shadow mapping) is calculated normally for the light at run time. |
121 | At run time, when not in baking mode, the renderer will attempt to sample |
122 | the lightmap to get the indirect lighting data and combine that with the |
123 | results of the real-time calculations. |
124 | |
125 | \value Light.BakeModeAll Both direct (diffuse, shadow) and indirect |
126 | lighting is baked for this light. The light will not have a specular |
127 | contribution and will not generate realtime shadow maps, but it will always |
128 | have baked shadows. At run time, when not in baking mode, the renderer will |
129 | attempt to sample the lightmap in place of the standard, real-time |
130 | calculations for diffuse lighting and shadow mapping. |
131 | |
132 | The default value is \c Light.BakeModeDisabled |
133 | |
134 | \note Just as with \l Model::usedInBakedLighting, designers and developers |
135 | must always evaluate on a per-light basis if the light is suitable to take |
136 | part in baked lighting. |
137 | |
138 | \warning Lights with dynamically changing properties, for example, animated |
139 | position, rotation, or other properties, are not suitable for participating |
140 | in baked lighting. |
141 | |
142 | This property is relevant both when baking and when using lightmaps. A |
143 | consistent state between the baking run and the subsequent runs that use |
144 | the generated data is essential. Changing to a different value will not |
145 | change the previously generated and persistently stored data in the |
146 | lightmaps, the engine's rendering behavior will however follow the |
147 | property's current value. |
148 | |
149 | For more information on how to bake lightmaps, see the \l {Lightmaps and |
150 | Global Illumination}. |
151 | |
152 | \sa Model::usedInBakedLighting, Model::bakedLightmap, Lightmapper, {Lightmaps and Global Illumination} |
153 | */ |
154 | |
155 | QQuick3DAbstractLight::QQuick3DAbstractLight(QQuick3DNodePrivate &dd, QQuick3DNode *parent) |
156 | : QQuick3DNode(dd, parent) |
157 | , m_color(Qt::white) |
158 | , m_ambientColor(Qt::black) {} |
159 | |
160 | QQuick3DAbstractLight::~QQuick3DAbstractLight() {} |
161 | |
162 | QColor QQuick3DAbstractLight::color() const |
163 | { |
164 | return m_color; |
165 | } |
166 | |
167 | QColor QQuick3DAbstractLight::ambientColor() const |
168 | { |
169 | return m_ambientColor; |
170 | } |
171 | |
172 | float QQuick3DAbstractLight::brightness() const |
173 | { |
174 | return m_brightness; |
175 | } |
176 | |
177 | QQuick3DNode *QQuick3DAbstractLight::scope() const |
178 | { |
179 | return m_scope; |
180 | } |
181 | |
182 | bool QQuick3DAbstractLight::castsShadow() const |
183 | { |
184 | return m_castsShadow; |
185 | } |
186 | |
187 | float QQuick3DAbstractLight::shadowBias() const |
188 | { |
189 | return m_shadowBias; |
190 | } |
191 | |
192 | float QQuick3DAbstractLight::shadowFactor() const |
193 | { |
194 | return m_shadowFactor; |
195 | } |
196 | |
197 | QQuick3DAbstractLight::QSSGShadowMapQuality QQuick3DAbstractLight::shadowMapQuality() const |
198 | { |
199 | return m_shadowMapQuality; |
200 | } |
201 | |
202 | float QQuick3DAbstractLight::shadowMapFar() const |
203 | { |
204 | return m_shadowMapFar; |
205 | } |
206 | |
207 | float QQuick3DAbstractLight::shadowFilter() const |
208 | { |
209 | return m_shadowFilter; |
210 | } |
211 | |
212 | QQuick3DAbstractLight::QSSGBakeMode QQuick3DAbstractLight::bakeMode() const |
213 | { |
214 | return m_bakeMode; |
215 | } |
216 | |
217 | void QQuick3DAbstractLight::markAllDirty() |
218 | { |
219 | m_dirtyFlags = DirtyFlags(DirtyFlag::ShadowDirty) |
220 | | DirtyFlags(DirtyFlag::ColorDirty) |
221 | | DirtyFlags(DirtyFlag::BrightnessDirty) |
222 | | DirtyFlags(DirtyFlag::FadeDirty) |
223 | | DirtyFlags(DirtyFlag::AreaDirty) |
224 | | DirtyFlags(DirtyFlag::BakeModeDirty); |
225 | QQuick3DNode::markAllDirty(); |
226 | } |
227 | |
228 | void QQuick3DAbstractLight::setColor(const QColor &color) |
229 | { |
230 | if (m_color == color) |
231 | return; |
232 | |
233 | m_color = color; |
234 | m_dirtyFlags.setFlag(flag: DirtyFlag::ColorDirty); |
235 | emit colorChanged(); |
236 | update(); |
237 | } |
238 | |
239 | void QQuick3DAbstractLight::setAmbientColor(const QColor &ambientColor) |
240 | { |
241 | if (m_ambientColor == ambientColor) |
242 | return; |
243 | |
244 | m_ambientColor = ambientColor; |
245 | m_dirtyFlags.setFlag(flag: DirtyFlag::ColorDirty); |
246 | emit ambientColorChanged(); |
247 | update(); |
248 | } |
249 | |
250 | void QQuick3DAbstractLight::setBrightness(float brightness) |
251 | { |
252 | if (qFuzzyCompare(p1: m_brightness, p2: brightness)) |
253 | return; |
254 | |
255 | m_brightness = brightness; |
256 | m_dirtyFlags.setFlag(flag: DirtyFlag::BrightnessDirty); |
257 | emit brightnessChanged(); |
258 | update(); |
259 | } |
260 | |
261 | void QQuick3DAbstractLight::setScope(QQuick3DNode *scope) |
262 | { |
263 | if (m_scope == scope) |
264 | return; |
265 | |
266 | m_scope = scope; |
267 | emit scopeChanged(); |
268 | update(); |
269 | } |
270 | |
271 | void QQuick3DAbstractLight::setCastsShadow(bool castsShadow) |
272 | { |
273 | if (m_castsShadow == castsShadow) |
274 | return; |
275 | |
276 | m_castsShadow = castsShadow; |
277 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
278 | emit castsShadowChanged(); |
279 | update(); |
280 | } |
281 | |
282 | void QQuick3DAbstractLight::setShadowBias(float shadowBias) |
283 | { |
284 | shadowBias = qBound(min: -1.0f, val: shadowBias, max: 1.0f); |
285 | if (qFuzzyCompare(p1: m_shadowBias, p2: shadowBias)) |
286 | return; |
287 | |
288 | m_shadowBias = shadowBias; |
289 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
290 | emit shadowBiasChanged(); |
291 | update(); |
292 | } |
293 | |
294 | void QQuick3DAbstractLight::setShadowFactor(float shadowFactor) |
295 | { |
296 | shadowFactor = qBound(min: 0.0f, val: shadowFactor, max: 100.0f); |
297 | if (qFuzzyCompare(p1: m_shadowFactor, p2: shadowFactor)) |
298 | return; |
299 | |
300 | m_shadowFactor = shadowFactor; |
301 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
302 | emit shadowFactorChanged(); |
303 | update(); |
304 | } |
305 | |
306 | void QQuick3DAbstractLight::setShadowMapQuality( |
307 | QQuick3DAbstractLight::QSSGShadowMapQuality shadowMapQuality) |
308 | { |
309 | if (m_shadowMapQuality == shadowMapQuality) |
310 | return; |
311 | |
312 | m_shadowMapQuality = shadowMapQuality; |
313 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
314 | emit shadowMapQualityChanged(); |
315 | update(); |
316 | } |
317 | |
318 | void QQuick3DAbstractLight::setBakeMode(QQuick3DAbstractLight::QSSGBakeMode bakeMode) |
319 | { |
320 | if (m_bakeMode == bakeMode) |
321 | return; |
322 | |
323 | m_bakeMode = bakeMode; |
324 | m_dirtyFlags.setFlag(flag: DirtyFlag::BakeModeDirty); |
325 | emit bakeModeChanged(); |
326 | update(); |
327 | } |
328 | |
329 | void QQuick3DAbstractLight::setShadowMapFar(float shadowMapFar) |
330 | { |
331 | if (qFuzzyCompare(p1: m_shadowMapFar, p2: shadowMapFar)) |
332 | return; |
333 | |
334 | m_shadowMapFar = shadowMapFar; |
335 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
336 | emit shadowMapFarChanged(); |
337 | update(); |
338 | } |
339 | |
340 | void QQuick3DAbstractLight::setShadowFilter(float shadowFilter) |
341 | { |
342 | if (qFuzzyCompare(p1: m_shadowFilter, p2: shadowFilter)) |
343 | return; |
344 | |
345 | m_shadowFilter = shadowFilter; |
346 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty); |
347 | emit shadowFilterChanged(); |
348 | update(); |
349 | } |
350 | |
351 | quint32 QQuick3DAbstractLight::mapToShadowResolution(QSSGShadowMapQuality quality) |
352 | { |
353 | switch (quality) { |
354 | case QSSGShadowMapQuality::ShadowMapQualityMedium: |
355 | return 9; |
356 | case QSSGShadowMapQuality::ShadowMapQualityHigh: |
357 | return 10; |
358 | case QSSGShadowMapQuality::ShadowMapQualityVeryHigh: |
359 | return 11; |
360 | default: |
361 | break; |
362 | } |
363 | return 8; |
364 | } |
365 | |
366 | QSSGRenderGraphObject *QQuick3DAbstractLight::updateSpatialNode(QSSGRenderGraphObject *node) |
367 | { |
368 | Q_ASSERT_X(node, __FUNCTION__, "Node must have been created in parent class." ); |
369 | |
370 | QQuick3DNode::updateSpatialNode(node); |
371 | |
372 | QSSGRenderLight *light = static_cast<QSSGRenderLight *>(node); |
373 | |
374 | if (m_dirtyFlags.toInt() != 0) // Some flag was set, so mark the light dirty! |
375 | light->markDirty(dirtyFlag: QSSGRenderLight::DirtyFlag::LightDirty); |
376 | |
377 | if (m_dirtyFlags.testFlag(flag: DirtyFlag::ColorDirty)) { |
378 | m_dirtyFlags.setFlag(flag: DirtyFlag::ColorDirty, on: false); |
379 | light->m_diffuseColor = QSSGUtils::color::sRGBToLinear(color: m_color).toVector3D(); |
380 | light->m_specularColor = light->m_diffuseColor; |
381 | light->m_ambientColor = QSSGUtils::color::sRGBToLinear(color: m_ambientColor).toVector3D(); |
382 | } |
383 | |
384 | if (m_dirtyFlags.testFlag(flag: DirtyFlag::BrightnessDirty)) { |
385 | m_dirtyFlags.setFlag(flag: DirtyFlag::BrightnessDirty, on: false); |
386 | light->m_brightness = m_brightness; |
387 | } |
388 | |
389 | if (m_dirtyFlags.testFlag(flag: DirtyFlag::ShadowDirty)) { |
390 | m_dirtyFlags.setFlag(flag: DirtyFlag::ShadowDirty, on: false); |
391 | light->m_castShadow = m_castsShadow; |
392 | light->m_shadowBias = m_shadowBias; |
393 | light->m_shadowFactor = m_shadowFactor; |
394 | light->m_shadowMapRes = mapToShadowResolution(quality: m_shadowMapQuality); |
395 | light->m_shadowMapFar = m_shadowMapFar; |
396 | light->m_shadowFilter = m_shadowFilter; |
397 | } |
398 | |
399 | if (m_dirtyFlags.testFlag(flag: DirtyFlag::BakeModeDirty)) { |
400 | m_dirtyFlags.setFlag(flag: DirtyFlag::BakeModeDirty, on: false); |
401 | light->m_bakingEnabled = m_bakeMode != QSSGBakeMode::BakeModeDisabled; |
402 | light->m_fullyBaked = m_bakeMode == QSSGBakeMode::BakeModeAll; |
403 | } |
404 | |
405 | if (m_scope) { |
406 | // Special case: |
407 | // If the 'scope' is 'this' and this is the first call, then the spatial node is the one we just created. |
408 | // This is not unlikely, as it can make sense to put all child nodes that should receive light under the light node... |
409 | if (m_scope == this) |
410 | light->m_scope = light; |
411 | else |
412 | light->m_scope = static_cast<QSSGRenderNode*>(QQuick3DObjectPrivate::get(item: m_scope)->spatialNode); |
413 | } else { |
414 | light->m_scope = nullptr; |
415 | } |
416 | |
417 | return node; |
418 | } |
419 | |
420 | QT_END_NAMESPACE |
421 | |