1 | // Copyright (C) 2019 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qquick3dmodel_p.h" |
5 | #include "qquick3dobject_p.h" |
6 | #include "qquick3dscenemanager_p.h" |
7 | #include "qquick3dnode_p_p.h" |
8 | #include "qquick3dinstancing_p.h" |
9 | |
10 | #include <QtQuick3DRuntimeRender/private/qssgrendergraphobject_p.h> |
11 | #include <QtQuick3DRuntimeRender/private/qssgrendercustommaterial_p.h> |
12 | #include <QtQuick3DRuntimeRender/private/qssgrenderdefaultmaterial_p.h> |
13 | #include <QtQuick3DRuntimeRender/private/qssgrendermodel_p.h> |
14 | |
15 | #include <QtQuick3DUtils/private/qssgutils_p.h> |
16 | |
17 | #include <QtQml/QQmlFile> |
18 | |
19 | QT_BEGIN_NAMESPACE |
20 | |
21 | /*! |
22 | \qmltype Model |
23 | \inherits Node |
24 | \inqmlmodule QtQuick3D |
25 | \brief Lets you load a 3D model data. |
26 | |
27 | The Model item makes it possible to load a mesh and modify how its shaded, by adding materials |
28 | to it. For a model to be renderable, it needs at least a mesh and a material. |
29 | |
30 | \section1 Mesh format and built-in primitives |
31 | |
32 | The model can load static meshes from storage or one of the built-in primitive types. |
33 | The mesh format used is a run-time format that's native to the engine, but additional formats are |
34 | supported through the asset import tool \l {Balsam Asset Import Tool}{Balsam}. |
35 | |
36 | The built-in primitives can be loaded by setting the \c source property to one of these values: |
37 | \c {#Rectangle, #Sphere, #Cube, #Cylinder or #Cone}. |
38 | |
39 | \qml |
40 | Model { |
41 | source: "#Sphere" |
42 | } |
43 | \endqml |
44 | |
45 | \section2 Custom geometry |
46 | |
47 | In addition to using static meshes, you can implement a |
48 | \l {QQuick3DGeometry}{custom geometry} provider that provides the model with |
49 | custom vertex data at run-time. See the |
50 | \l {Qt Quick 3D - Custom Geometry Example}{Custom Geometry Example} for an |
51 | example on how to create and use a custom material with your model. |
52 | |
53 | \section1 Materials |
54 | |
55 | A model can consist of several sub-meshes, each of which can have its own |
56 | material. The sub-mesh uses a material from the \l{materials} list, |
57 | corresponding to its index. If the number of materials is less than the |
58 | sub-meshes, the last material in the list is used for subsequent sub-meshes. |
59 | This is demonstrated in the |
60 | \l {Qt Quick 3D - Sub-mesh Example}{Sub-mesh example}. |
61 | |
62 | You can use the following materials with the model item: |
63 | \l {PrincipledMaterial}, \l {DefaultMaterial}, and \l {CustomMaterial}. |
64 | |
65 | \section1 Picking |
66 | |
67 | \e Picking is the process of sending a ray through the scene from some |
68 | starting position to find which models intersect with the ray. In |
69 | Qt Quick 3D, the ray is normally sent from the view using 2D coordinates |
70 | resulting from a touch or mouse event. If a model was hit by the ray, |
71 | \l {PickResult} will be returned with a handle to the model and information |
72 | about where the ray hit the model. For models that use |
73 | \l {QQuick3DGeometry}{custom geometry}, the picking is less accurate than |
74 | for static mesh data, as picking is only done against the model's |
75 | \l {bounds}{bounding volume}. If the ray goes through more than one model, |
76 | the closest \l {Model::pickable}{pickable} model is selected. |
77 | |
78 | Note that for models to be \l {Model::pickable}{pickable}, their |
79 | \l {Model::pickable}{pickable} property must be set to \c true. For more |
80 | information, see \l {Qt Quick 3D - Picking example}. |
81 | |
82 | */ |
83 | |
84 | /*! |
85 | \qmltype bounds |
86 | \inqmlmodule QtQuick3D |
87 | \since 5.15 |
88 | \brief Specifies the bounds of a model. |
89 | |
90 | bounds specify a bounding box with minimum and maximum points. |
91 | bounds is a readonly property of the model. |
92 | */ |
93 | |
94 | /*! |
95 | \qmlproperty vector3d bounds::minimum |
96 | |
97 | Specifies the minimum point of the model bounds. |
98 | \sa maximum |
99 | */ |
100 | |
101 | /*! |
102 | \qmlproperty vector3d bounds::maximum |
103 | |
104 | Specifies the maximum point of the model bounds. |
105 | \sa minimum |
106 | */ |
107 | |
108 | QQuick3DModel::QQuick3DModel(QQuick3DNode *parent) |
109 | : QQuick3DNode(*(new QQuick3DNodePrivate(QQuick3DNodePrivate::Type::Model)), parent) {} |
110 | |
111 | QQuick3DModel::~QQuick3DModel() |
112 | { |
113 | disconnect(m_geometryConnection); |
114 | |
115 | auto matList = materials(); |
116 | qmlClearMaterials(list: &matList); |
117 | auto morphList = morphTargets(); |
118 | qmlClearMorphTargets(list: &morphList); |
119 | } |
120 | |
121 | /*! |
122 | \qmlproperty url Model::source |
123 | |
124 | This property defines the location of the mesh file containing the geometry |
125 | of this Model or one of the built-in primitive meshes listed below |
126 | as described in \l {Mesh format and built-in primitives}. |
127 | |
128 | \list |
129 | \li "#Rectangle" |
130 | \li "#Sphere" |
131 | \li "#Cube" |
132 | \li "#Cone" |
133 | \li "#Cylinder" |
134 | \endlist |
135 | */ |
136 | |
137 | QUrl QQuick3DModel::source() const |
138 | { |
139 | return m_source; |
140 | } |
141 | |
142 | /*! |
143 | \qmlproperty List<QtQuick3D::Material> Model::materials |
144 | |
145 | This property contains a list of materials used to render the provided |
146 | geometry. To render anything, there must be at least one material. Normally |
147 | there should be one material for each sub-mesh included in the source |
148 | geometry. |
149 | |
150 | \sa {Qt Quick 3D - Sub-mesh Example} |
151 | */ |
152 | |
153 | |
154 | QQmlListProperty<QQuick3DMaterial> QQuick3DModel::materials() |
155 | { |
156 | return QQmlListProperty<QQuick3DMaterial>(this, |
157 | nullptr, |
158 | QQuick3DModel::qmlAppendMaterial, |
159 | QQuick3DModel::qmlMaterialsCount, |
160 | QQuick3DModel::qmlMaterialAt, |
161 | QQuick3DModel::qmlClearMaterials); |
162 | } |
163 | |
164 | /*! |
165 | \qmlproperty List<QtQuick3D::MorphTarget> Model::morphTargets |
166 | |
167 | This property contains a list of \l [QtQuick3D QML] {MorphTarget}{MorphTarget}s used to |
168 | render the provided geometry. Meshes should have at least one attribute |
169 | among positions, normals, tangents, binormals, texture coordinates, |
170 | and vertex colors for the morph targets. |
171 | |
172 | \sa {MorphTarget} |
173 | */ |
174 | |
175 | QQmlListProperty<QQuick3DMorphTarget> QQuick3DModel::morphTargets() |
176 | { |
177 | return QQmlListProperty<QQuick3DMorphTarget>(this, |
178 | nullptr, |
179 | QQuick3DModel::qmlAppendMorphTarget, |
180 | QQuick3DModel::qmlMorphTargetsCount, |
181 | QQuick3DModel::qmlMorphTargetAt, |
182 | QQuick3DModel::qmlClearMorphTargets); |
183 | } |
184 | |
185 | /*! |
186 | \qmlproperty QtQuick3D::Instancing Model::instancing |
187 | |
188 | If this property is set, the model will not be rendered normally. Instead, a number of |
189 | instances of the model will be rendered, as defined by the instance table. |
190 | |
191 | \sa Instancing |
192 | */ |
193 | |
194 | QQuick3DInstancing *QQuick3DModel::instancing() const |
195 | { |
196 | return m_instancing; |
197 | } |
198 | |
199 | /*! |
200 | \qmlproperty QtQuick3D::Node Model::instanceRoot |
201 | |
202 | This property defines the origin of the instance’s coordinate system. |
203 | |
204 | See the \l{Transforms and instancing}{overview documentation} for a detailed explanation. |
205 | |
206 | \sa instancing, Instancing |
207 | */ |
208 | QQuick3DNode *QQuick3DModel::instanceRoot() const |
209 | { |
210 | return m_instanceRoot; |
211 | } |
212 | |
213 | // Source URL's need a bit of translation for the engine because of the |
214 | // use of fragment syntax for specifiying primitives and sub-meshes |
215 | // So we need to check for the fragment before translating to a qmlfile |
216 | |
217 | QString QQuick3DModel::translateMeshSource(const QUrl &source, QObject *contextObject) |
218 | { |
219 | QString fragment; |
220 | if (source.hasFragment()) { |
221 | // Check if this is an index, or primitive |
222 | bool isNumber = false; |
223 | source.fragment().toInt(ok: &isNumber); |
224 | fragment = QStringLiteral("#" ) + source.fragment(); |
225 | // If it wasn't an index, then it was a primitive |
226 | if (!isNumber) |
227 | return fragment; |
228 | } |
229 | |
230 | const QQmlContext *context = qmlContext(contextObject); |
231 | const auto resolvedUrl = context ? context->resolvedUrl(source) : source; |
232 | const auto qmlSource = QQmlFile::urlToLocalFileOrQrc(resolvedUrl); |
233 | return (qmlSource.isEmpty() ? source.path() : qmlSource) + fragment; |
234 | } |
235 | |
236 | void QQuick3DModel::markAllDirty() |
237 | { |
238 | m_dirtyAttributes = 0xffffffff; |
239 | QQuick3DNode::markAllDirty(); |
240 | } |
241 | |
242 | /*! |
243 | \qmlproperty bool Model::castsShadows |
244 | |
245 | When this property is \c true, the geometry of this model is used when |
246 | rendering to the shadow maps, and is also generating shadows in baked |
247 | lighting. |
248 | |
249 | The default value is \c true. |
250 | */ |
251 | |
252 | bool QQuick3DModel::castsShadows() const |
253 | { |
254 | return m_castsShadows; |
255 | } |
256 | |
257 | /*! |
258 | \qmlproperty bool Model::receivesShadows |
259 | |
260 | When this property is set to \c true, the model's materials take shadow |
261 | contribution from shadow casting lights into account. |
262 | |
263 | \note When lightmapping is enabled for this model, fully baked lights with |
264 | Light::bakeMode set to Light.BakeModeAll will always generate (baked) |
265 | shadows on the model, regardless of the value of this property. |
266 | |
267 | The default value is \c true. |
268 | */ |
269 | |
270 | bool QQuick3DModel::receivesShadows() const |
271 | { |
272 | return m_receivesShadows; |
273 | } |
274 | |
275 | /*! |
276 | \qmlproperty bool Model::pickable |
277 | |
278 | This property controls whether the model is pickable or not. Set this property |
279 | to \c true to make the model pickable. Models are not pickable by default. |
280 | |
281 | \sa {View3D::pick} |
282 | */ |
283 | bool QQuick3DModel::pickable() const |
284 | { |
285 | return m_pickable; |
286 | } |
287 | |
288 | /*! |
289 | \qmlproperty Geometry Model::geometry |
290 | |
291 | Specify a custom geometry for the model. The Model::source must be empty when custom geometry |
292 | is used. |
293 | |
294 | */ |
295 | QQuick3DGeometry *QQuick3DModel::geometry() const |
296 | { |
297 | return m_geometry; |
298 | } |
299 | |
300 | /*! |
301 | \qmlproperty Skeleton Model::skeleton |
302 | |
303 | Contains the skeleton for the model. The Skeleton is used together with \l {inverseBindPoses} |
304 | for \l {Vertex Skinning}{skinning}. |
305 | |
306 | \note Meshes of the model must have both joints and weights attributes. |
307 | \note If this property is set, skinning animation is enabled. It means |
308 | that \l {Model} is transformed based on \l {Skeleton} ignoring Model's global |
309 | transformation. |
310 | |
311 | \sa {Model::inverseBindPoses}, {Qt Quick 3D - Simple Skinning Example} |
312 | */ |
313 | QQuick3DSkeleton *QQuick3DModel::skeleton() const |
314 | { |
315 | return m_skeleton; |
316 | } |
317 | |
318 | /*! |
319 | \qmlproperty Skin Model::skin |
320 | |
321 | Contains the skeleton for the model. The Skeleton is used for |
322 | \l {Vertex Skinning}{skinning}. |
323 | |
324 | \note Meshes of the model must have both joints and weights attributes. |
325 | \note If this property is set, skinning animation is enabled. This means |
326 | the \l {Model} will be transformed based on \l {Skin::joints} and the |
327 | Model's global transformation will be ignored. |
328 | \note If a model has both a skeleton and a skin, then the skin will be used. |
329 | |
330 | \sa {Model::skeleton} {Qt Quick 3D - Simple Skinning Example} |
331 | */ |
332 | QQuick3DSkin *QQuick3DModel::skin() const |
333 | { |
334 | return m_skin; |
335 | } |
336 | |
337 | |
338 | /*! |
339 | \qmlproperty List<matrix4x4> Model::inverseBindPoses |
340 | |
341 | This property contains a list of Inverse Bind Pose matrixes used for the |
342 | skeletal animation. Each inverseBindPose matrix means the inverse of the |
343 | global transform of the repective \l {Joint::index} in \l {skeleton}, which |
344 | will be used initially. |
345 | |
346 | \note This property is only used if the Model::skeleton is valid. |
347 | \note If some of the matrices are not set, identity values will be used. |
348 | |
349 | \sa {skeleton} {Joint::index} |
350 | */ |
351 | QList<QMatrix4x4> QQuick3DModel::inverseBindPoses() const |
352 | { |
353 | return m_inverseBindPoses; |
354 | } |
355 | |
356 | /*! |
357 | \qmlproperty Bounds Model::bounds |
358 | |
359 | The bounds of the model descibes the extents of the bounding volume around the model. |
360 | |
361 | \note The bounds might not be immediately available if the model needs to be loaded first. |
362 | |
363 | \readonly |
364 | */ |
365 | QQuick3DBounds3 QQuick3DModel::bounds() const |
366 | { |
367 | return m_bounds; |
368 | } |
369 | |
370 | /*! |
371 | \qmlproperty real Model::depthBias |
372 | |
373 | Holds the depth bias of the model. Depth bias is added to the object distance from camera when sorting |
374 | objects. This can be used to force rendering order between objects close to each other, that |
375 | might otherwise be rendered in different order in different frames. Negative values cause the |
376 | sorting value to move closer to the camera while positive values move it further from the camera. |
377 | */ |
378 | float QQuick3DModel::depthBias() const |
379 | { |
380 | return m_depthBias; |
381 | } |
382 | |
383 | /*! |
384 | \qmlproperty bool Model::receivesReflections |
385 | |
386 | When this property is set to \c true, the model's materials take reflections contribution from |
387 | a reflection probe. If the model is inside more than one reflection probe at the same time, |
388 | the nearest reflection probe is taken into account. |
389 | */ |
390 | |
391 | bool QQuick3DModel::receivesReflections() const |
392 | { |
393 | return m_receivesReflections; |
394 | } |
395 | |
396 | /*! |
397 | \qmlproperty bool Model::castsReflections |
398 | \since 6.4 |
399 | |
400 | When this property is set to \c true, the model is rendered by reflection probes and can be |
401 | seen in the reflections. |
402 | */ |
403 | bool QQuick3DModel::castsReflections() const |
404 | { |
405 | return m_castsReflections; |
406 | } |
407 | |
408 | /*! |
409 | \qmlproperty bool Model::usedInBakedLighting |
410 | |
411 | When this property is set to \c true, the model contributes to baked |
412 | lighting, such as lightmaps, for example in form of casting shadows or |
413 | indirect light. This setting is independent of controlling lightmap |
414 | generation for the model, use \l bakedLightmap for that. |
415 | |
416 | The default value is \c false. |
417 | |
418 | \note The default value is false, because designers and developers must |
419 | always evaluate on a per-model basis if the object is suitable to take part |
420 | in baked lighting. |
421 | |
422 | \warning Models with dynamically changing properties, for example, animated |
423 | position, rotation, or other properties, are not suitable for participating |
424 | in baked lighting. |
425 | |
426 | For more information on how to bake lightmaps, see the \l Lightmapper |
427 | documentation. |
428 | |
429 | This property is relevant only when baking lightmaps. It has no effect |
430 | afterwards, when using the generated lightmaps during rendering. |
431 | |
432 | \sa Light::bakeMode, bakedLightmap, Lightmapper, {Lightmaps and Global Illumination} |
433 | */ |
434 | |
435 | bool QQuick3DModel::isUsedInBakedLighting() const |
436 | { |
437 | return m_usedInBakedLighting; |
438 | } |
439 | |
440 | /*! |
441 | \qmlproperty int Model::lightmapBaseResolution |
442 | |
443 | Defines the approximate size of the lightmap for this model. The default |
444 | value is 1024, indicating 1024x1024 as the base size. The actual size of |
445 | the lightmap texture is likely to be different, often bigger, depending on |
446 | the mesh. |
447 | |
448 | For simpler, smaller meshes, or when it is known that using a bigger |
449 | lightmap is unnecessary, the value can be set to something smaller, for |
450 | example, 512 or 256. |
451 | |
452 | The minimum value is 128. |
453 | |
454 | This setting applies both to persistently stored and for intermediate, |
455 | partial lightmaps. When baking lightmaps, all models that have \l |
456 | usedInBakedLighting enabled are part of the path-traced scene. Thus all of |
457 | them need to have lightmap UV unwrapping performed and the rasterization |
458 | steps necessary to compute direct lighting which then can be taken into |
459 | account for indirect light bounces in the scene. However, for models that |
460 | just contribute to, but do not store a lightmap the default value is often |
461 | sufficient. Fine-tuning is more relevant for models that store and then use |
462 | the generated lightmaps. |
463 | |
464 | This property is relevant only when baking lightmaps. It has no effect |
465 | afterwards, when using the generated lightmaps during rendering. |
466 | |
467 | Models that have lightmap UV data pre-generated during asset import time |
468 | (e.g. via the balsam tool) will ignore this property because the lightmap |
469 | UV unwrapping and the lightmap size hint evaluation have already been done, |
470 | and will not be performed again during lightmap baking. |
471 | */ |
472 | int QQuick3DModel::lightmapBaseResolution() const |
473 | { |
474 | return m_lightmapBaseResolution; |
475 | } |
476 | |
477 | /*! |
478 | \qmlproperty BakedLightmap Model::bakedLightmap |
479 | |
480 | When this property is set to a valid, enabled BakedLightmap object, the |
481 | model will get a lightmap generated when baking lighting, the lightmap is |
482 | then stored persistently. When rendering, the model will load and use the |
483 | associated lightmap. The default value is null. |
484 | |
485 | \note When the intention is to generate a persistently stored lightmap for |
486 | a Model, both bakedLightmap and \l usedInBakedLighting must be set on it, |
487 | in order to indicate that the Model not only participates in the |
488 | lightmapped scene, but also wants a full lightmap to be baked and stored. |
489 | |
490 | For more information on how to bake lightmaps, see the \l Lightmapper |
491 | documentation. |
492 | |
493 | This property is relevant both when baking and when using lightmaps. A |
494 | consistent state between the baking run and the subsequent runs that use |
495 | the generated data is essential. For example, changing the lightmap key |
496 | will make it impossible to load the previously generated data. An exception |
497 | is \l {BakedLightmap::}{enabled}, which can be used to dynamically toggle |
498 | the usage of lightmaps (outside of the baking run), but be aware that the |
499 | rendered results will depend on the Lights' \l{Light::bakeMode}{bakeMode} |
500 | settings in the scene. |
501 | |
502 | \sa usedInBakedLighting, Lightmapper |
503 | */ |
504 | |
505 | QQuick3DBakedLightmap *QQuick3DModel::bakedLightmap() const |
506 | { |
507 | return m_bakedLightmap; |
508 | } |
509 | |
510 | /*! |
511 | \qmlproperty real Model::instancingLodMin |
512 | |
513 | Defines the minimum distance from camera that an instance of this model is shown. |
514 | Used for a level of detail implementation. |
515 | */ |
516 | float QQuick3DModel::instancingLodMin() const |
517 | { |
518 | return m_instancingLodMin; |
519 | } |
520 | |
521 | /*! |
522 | \qmlproperty real Model::instancingLodMax |
523 | |
524 | Defines the maximum distance from camera that an instance of this model is shown. |
525 | Used for a level of detail implementation. |
526 | */ |
527 | float QQuick3DModel::instancingLodMax() const |
528 | { |
529 | return m_instancingLodMax; |
530 | } |
531 | |
532 | void QQuick3DModel::setSource(const QUrl &source) |
533 | { |
534 | if (m_source == source) |
535 | return; |
536 | |
537 | m_source = source; |
538 | emit sourceChanged(); |
539 | markDirty(type: SourceDirty); |
540 | if (QQuick3DObjectPrivate::get(item: this)->sceneManager) |
541 | QQuick3DObjectPrivate::get(item: this)->sceneManager->dirtyBoundingBoxList.append(t: this); |
542 | } |
543 | |
544 | void QQuick3DModel::setCastsShadows(bool castsShadows) |
545 | { |
546 | if (m_castsShadows == castsShadows) |
547 | return; |
548 | |
549 | m_castsShadows = castsShadows; |
550 | emit castsShadowsChanged(); |
551 | markDirty(type: ShadowsDirty); |
552 | } |
553 | |
554 | void QQuick3DModel::setReceivesShadows(bool receivesShadows) |
555 | { |
556 | if (m_receivesShadows == receivesShadows) |
557 | return; |
558 | |
559 | m_receivesShadows = receivesShadows; |
560 | emit receivesShadowsChanged(); |
561 | markDirty(type: ShadowsDirty); |
562 | } |
563 | |
564 | void QQuick3DModel::setPickable(bool isPickable) |
565 | { |
566 | if (m_pickable == isPickable) |
567 | return; |
568 | |
569 | m_pickable = isPickable; |
570 | emit pickableChanged(); |
571 | markDirty(type: PickingDirty); |
572 | } |
573 | |
574 | void QQuick3DModel::setGeometry(QQuick3DGeometry *geometry) |
575 | { |
576 | if (geometry == m_geometry) |
577 | return; |
578 | |
579 | // Make sure to disconnect if the geometry gets deleted out from under us |
580 | QQuick3DObjectPrivate::attachWatcher(context: this, setter: &QQuick3DModel::setGeometry, newO: geometry, oldO: m_geometry); |
581 | |
582 | if (m_geometry) |
583 | QObject::disconnect(m_geometryConnection); |
584 | m_geometry = geometry; |
585 | |
586 | if (m_geometry) { |
587 | m_geometryConnection |
588 | = QObject::connect(sender: m_geometry, signal: &QQuick3DGeometry::geometryNodeDirty, slot: [this]() { |
589 | markDirty(type: GeometryDirty); |
590 | }); |
591 | } |
592 | emit geometryChanged(); |
593 | markDirty(type: GeometryDirty); |
594 | } |
595 | |
596 | void QQuick3DModel::setSkeleton(QQuick3DSkeleton *skeleton) |
597 | { |
598 | if (skeleton == m_skeleton) |
599 | return; |
600 | |
601 | // Make sure to disconnect if the skeleton gets deleted out from under us |
602 | QQuick3DObjectPrivate::attachWatcher(context: this, setter: &QQuick3DModel::setSkeleton, newO: skeleton, oldO: m_skeleton); |
603 | |
604 | m_skeleton = skeleton; |
605 | |
606 | emit skeletonChanged(); |
607 | markDirty(type: SkeletonDirty); |
608 | } |
609 | |
610 | void QQuick3DModel::setSkin(QQuick3DSkin *skin) |
611 | { |
612 | if (skin == m_skin) |
613 | return; |
614 | |
615 | // Make sure to disconnect if the skin gets deleted out from under us |
616 | QQuick3DObjectPrivate::attachWatcher(context: this, setter: &QQuick3DModel::setSkin, newO: skin, oldO: m_skin); |
617 | |
618 | m_skin = skin; |
619 | emit skinChanged(); |
620 | markDirty(type: SkinDirty); |
621 | } |
622 | |
623 | void QQuick3DModel::setInverseBindPoses(const QList<QMatrix4x4> &poses) |
624 | { |
625 | if (m_inverseBindPoses == poses) |
626 | return; |
627 | |
628 | m_inverseBindPoses = poses; |
629 | emit inverseBindPosesChanged(); |
630 | markDirty(type: PoseDirty); |
631 | } |
632 | |
633 | |
634 | void QQuick3DModel::setBounds(const QVector3D &min, const QVector3D &max) |
635 | { |
636 | if (!qFuzzyCompare(v1: m_bounds.maximum(), v2: max) |
637 | || !qFuzzyCompare(v1: m_bounds.minimum(), v2: min)) { |
638 | m_bounds.bounds = QSSGBounds3 { min, max }; |
639 | emit boundsChanged(); |
640 | } |
641 | } |
642 | |
643 | void QQuick3DModel::setInstancing(QQuick3DInstancing *instancing) |
644 | { |
645 | if (m_instancing == instancing) |
646 | return; |
647 | |
648 | // Make sure to disconnect if the instance table gets deleted out from under us |
649 | QQuick3DObjectPrivate::attachWatcher(context: this, setter: &QQuick3DModel::setInstancing, newO: instancing, oldO: m_instancing); |
650 | if (m_instancing) |
651 | QObject::disconnect(m_instancingConnection); |
652 | m_instancing = instancing; |
653 | if (m_instancing) { |
654 | m_instancingConnection = QObject::connect |
655 | (sender: m_instancing, signal: &QQuick3DInstancing::instanceNodeDirty, |
656 | context: this, slot: [this]{ markDirty(type: InstancesDirty);}); |
657 | } |
658 | markDirty(type: InstancesDirty); |
659 | emit instancingChanged(); |
660 | } |
661 | |
662 | void QQuick3DModel::setInstanceRoot(QQuick3DNode *instanceRoot) |
663 | { |
664 | if (m_instanceRoot == instanceRoot) |
665 | return; |
666 | |
667 | QQuick3DObjectPrivate::attachWatcher(context: this, setter: &QQuick3DModel::setInstanceRoot, newO: instanceRoot, oldO: m_instanceRoot); |
668 | |
669 | m_instanceRoot = instanceRoot; |
670 | markDirty(type: InstanceRootDirty); |
671 | emit instanceRootChanged(); |
672 | } |
673 | |
674 | void QQuick3DModel::setDepthBias(float bias) |
675 | { |
676 | if (qFuzzyCompare(p1: bias, p2: m_depthBias)) |
677 | return; |
678 | |
679 | m_depthBias = bias; |
680 | markDirty(type: PropertyDirty); |
681 | emit depthBiasChanged(); |
682 | } |
683 | |
684 | void QQuick3DModel::setReceivesReflections(bool receivesReflections) |
685 | { |
686 | if (m_receivesReflections == receivesReflections) |
687 | return; |
688 | |
689 | m_receivesReflections = receivesReflections; |
690 | emit receivesReflectionsChanged(); |
691 | markDirty(type: ReflectionDirty); |
692 | } |
693 | |
694 | void QQuick3DModel::setCastsReflections(bool castsReflections) |
695 | { |
696 | if (m_castsReflections == castsReflections) |
697 | return; |
698 | m_castsReflections = castsReflections; |
699 | emit castsReflectionsChanged(); |
700 | markDirty(type: ReflectionDirty); |
701 | } |
702 | |
703 | void QQuick3DModel::setUsedInBakedLighting(bool enable) |
704 | { |
705 | if (m_usedInBakedLighting == enable) |
706 | return; |
707 | |
708 | m_usedInBakedLighting = enable; |
709 | emit usedInBakedLightingChanged(); |
710 | markDirty(type: PropertyDirty); |
711 | } |
712 | |
713 | void QQuick3DModel::setLightmapBaseResolution(int resolution) |
714 | { |
715 | resolution = qMax(a: 128, b: resolution); |
716 | if (m_lightmapBaseResolution == resolution) |
717 | return; |
718 | |
719 | m_lightmapBaseResolution = resolution; |
720 | emit lightmapBaseResolutionChanged(); |
721 | markDirty(type: PropertyDirty); |
722 | } |
723 | |
724 | void QQuick3DModel::setBakedLightmap(QQuick3DBakedLightmap *bakedLightmap) |
725 | { |
726 | if (m_bakedLightmap == bakedLightmap) |
727 | return; |
728 | |
729 | if (m_bakedLightmap) |
730 | m_bakedLightmap->disconnect(m_bakedLightmapSignalConnection); |
731 | |
732 | m_bakedLightmap = bakedLightmap; |
733 | |
734 | m_bakedLightmapSignalConnection = QObject::connect(sender: m_bakedLightmap, signal: &QQuick3DBakedLightmap::changed, context: this, |
735 | slot: [this] { markDirty(type: PropertyDirty); }); |
736 | |
737 | QObject::connect(sender: m_bakedLightmap, signal: &QObject::destroyed, context: this, |
738 | slot: [this] |
739 | { |
740 | m_bakedLightmap = nullptr; |
741 | markDirty(type: PropertyDirty); |
742 | }); |
743 | |
744 | emit bakedLightmapChanged(); |
745 | markDirty(type: PropertyDirty); |
746 | } |
747 | |
748 | void QQuick3DModel::itemChange(ItemChange change, const ItemChangeData &value) |
749 | { |
750 | if (change == QQuick3DObject::ItemSceneChange) |
751 | updateSceneManager(sceneManager: value.sceneManager); |
752 | } |
753 | |
754 | QSSGRenderGraphObject *QQuick3DModel::updateSpatialNode(QSSGRenderGraphObject *node) |
755 | { |
756 | if (!node) { |
757 | markAllDirty(); |
758 | node = new QSSGRenderModel(); |
759 | } |
760 | |
761 | QQuick3DNode::updateSpatialNode(node); |
762 | int dirtyAttribute = 0; |
763 | |
764 | auto modelNode = static_cast<QSSGRenderModel *>(node); |
765 | if (m_dirtyAttributes & SourceDirty) |
766 | modelNode->meshPath = QSSGRenderPath(translateMeshSource(source: m_source, contextObject: this)); |
767 | if (m_dirtyAttributes & PickingDirty) |
768 | modelNode->setState(state: QSSGRenderModel::LocalState::Pickable, on: m_pickable); |
769 | |
770 | if (m_dirtyAttributes & ShadowsDirty) { |
771 | modelNode->castsShadows = m_castsShadows; |
772 | modelNode->receivesShadows = m_receivesShadows; |
773 | } |
774 | |
775 | if (m_dirtyAttributes & MaterialsDirty) { |
776 | if (!m_materials.isEmpty()) { |
777 | if (modelNode->materials.isEmpty()) { |
778 | // Easy mode, just add each material |
779 | for (const Material &material : m_materials) { |
780 | QSSGRenderGraphObject *graphObject = QQuick3DObjectPrivate::get(item: material.material)->spatialNode; |
781 | if (graphObject) |
782 | modelNode->materials.append(t: graphObject); |
783 | else |
784 | dirtyAttribute |= MaterialsDirty; // We still got dirty materials |
785 | } |
786 | } else { |
787 | // Hard mode, go through each material and see if they match |
788 | if (modelNode->materials.size() != m_materials.size()) |
789 | modelNode->materials.resize(size: m_materials.size()); |
790 | for (int i = 0; i < m_materials.size(); ++i) { |
791 | QSSGRenderGraphObject *graphObject = QQuick3DObjectPrivate::get(item: m_materials[i].material)->spatialNode; |
792 | if (modelNode->materials[i] != graphObject) |
793 | modelNode->materials[i] = graphObject; |
794 | } |
795 | } |
796 | } else { |
797 | // No materials |
798 | modelNode->materials.clear(); |
799 | } |
800 | } |
801 | |
802 | if (m_dirtyAttributes & MorphTargetsDirty) { |
803 | if (!m_morphTargets.isEmpty()) { |
804 | const int numMorphTarget = m_morphTargets.size(); |
805 | if (modelNode->morphTargets.isEmpty()) { |
806 | // Easy mode, just add each morphTarget |
807 | for (const auto morphTarget : std::as_const(t&: m_morphTargets)) { |
808 | QSSGRenderGraphObject *graphObject = QQuick3DObjectPrivate::get(item: morphTarget)->spatialNode; |
809 | if (graphObject) |
810 | modelNode->morphTargets.append(t: graphObject); |
811 | else |
812 | dirtyAttribute |= MorphTargetsDirty; // We still got dirty morphTargets |
813 | } |
814 | modelNode->morphWeights.resize(size: numMorphTarget); |
815 | modelNode->morphAttributes.resize(size: numMorphTarget); |
816 | } else { |
817 | // Hard mode, go through each morphTarget and see if they match |
818 | if (modelNode->morphTargets.size() != numMorphTarget) { |
819 | modelNode->morphTargets.resize(size: numMorphTarget); |
820 | modelNode->morphWeights.resize(size: numMorphTarget); |
821 | modelNode->morphAttributes.resize(size: numMorphTarget); |
822 | } |
823 | for (int i = 0; i < numMorphTarget; ++i) |
824 | modelNode->morphTargets[i] = QQuick3DObjectPrivate::get(item: m_morphTargets.at(i))->spatialNode; |
825 | } |
826 | } else { |
827 | // No morphTargets |
828 | modelNode->morphTargets.clear(); |
829 | } |
830 | } |
831 | |
832 | if (m_dirtyAttributes & quint32(InstancesDirty | InstanceRootDirty)) { |
833 | // If we have an instance root set we have lower priority and the instance root node should already |
834 | // have been created. |
835 | QSSGRenderNode *instanceRootNode = nullptr; |
836 | if (m_instanceRoot) { |
837 | if (m_instanceRoot == this) |
838 | instanceRootNode = modelNode; |
839 | else |
840 | instanceRootNode = static_cast<QSSGRenderNode *>(QQuick3DObjectPrivate::get(item: m_instanceRoot)->spatialNode); |
841 | } |
842 | if (instanceRootNode != modelNode->instanceRoot) { |
843 | modelNode->instanceRoot = instanceRootNode; |
844 | modelNode->markDirty(dirtyFlag: QSSGRenderNode::DirtyFlag::TransformDirty); |
845 | } |
846 | |
847 | if (m_instancing) { |
848 | modelNode->instanceTable = static_cast<QSSGRenderInstanceTable *>(QQuick3DObjectPrivate::get(item: m_instancing)->spatialNode); |
849 | } else { |
850 | modelNode->instanceTable = nullptr; |
851 | } |
852 | } |
853 | |
854 | if (m_dirtyAttributes & GeometryDirty) { |
855 | if (m_geometry) { |
856 | modelNode->geometry = static_cast<QSSGRenderGeometry *>(QQuick3DObjectPrivate::get(item: m_geometry)->spatialNode); |
857 | setBounds(min: m_geometry->boundsMin(), max: m_geometry->boundsMax()); |
858 | } else { |
859 | modelNode->geometry = nullptr; |
860 | setBounds(min: QVector3D(), max: QVector3D()); |
861 | } |
862 | } |
863 | |
864 | if (m_dirtyAttributes & SkeletonDirty) { |
865 | if (m_skeleton) { |
866 | modelNode->skeleton = static_cast<QSSGRenderSkeleton *>(QQuick3DObjectPrivate::get(item: m_skeleton)->spatialNode); |
867 | if (modelNode->skeleton) |
868 | modelNode->skeleton->skinningDirty = true; |
869 | } else { |
870 | modelNode->skeleton = nullptr; |
871 | } |
872 | } |
873 | |
874 | if (m_dirtyAttributes & SkinDirty) { |
875 | if (m_skin) |
876 | modelNode->skin = static_cast<QSSGRenderSkin *>(QQuick3DObjectPrivate::get(item: m_skin)->spatialNode); |
877 | else |
878 | modelNode->skin = nullptr; |
879 | } |
880 | |
881 | if (m_dirtyAttributes & LodDirty) { |
882 | modelNode->instancingLodMin = m_instancingLodMin; |
883 | modelNode->instancingLodMax = m_instancingLodMax; |
884 | } |
885 | |
886 | if (m_dirtyAttributes & PoseDirty) { |
887 | modelNode->inverseBindPoses = m_inverseBindPoses.toVector(); |
888 | if (modelNode->skeleton) |
889 | modelNode->skeleton->skinningDirty = true; |
890 | } |
891 | |
892 | if (m_dirtyAttributes & PropertyDirty) { |
893 | modelNode->m_depthBiasSq = QSSGRenderModel::signedSquared(val: m_depthBias); |
894 | modelNode->usedInBakedLighting = m_usedInBakedLighting; |
895 | modelNode->lightmapBaseResolution = uint(m_lightmapBaseResolution); |
896 | if (m_bakedLightmap && m_bakedLightmap->isEnabled()) { |
897 | modelNode->lightmapKey = m_bakedLightmap->key(); |
898 | const QString srcPrefix = m_bakedLightmap->loadPrefix(); |
899 | const QString srcPath = srcPrefix.isEmpty() ? QStringLiteral("." ) : srcPrefix; |
900 | const QQmlContext *context = qmlContext(m_bakedLightmap); |
901 | const QUrl resolvedUrl = context ? context->resolvedUrl(srcPath) : srcPath; |
902 | modelNode->lightmapLoadPath = QQmlFile::urlToLocalFileOrQrc(resolvedUrl); |
903 | } else { |
904 | modelNode->lightmapKey.clear(); |
905 | modelNode->lightmapLoadPath.clear(); |
906 | } |
907 | modelNode->levelOfDetailBias = m_levelOfDetailBias; |
908 | } |
909 | |
910 | if (m_dirtyAttributes & ReflectionDirty) { |
911 | modelNode->receivesReflections = m_receivesReflections; |
912 | modelNode->castsReflections = m_castsReflections; |
913 | } |
914 | |
915 | m_dirtyAttributes = dirtyAttribute; |
916 | |
917 | return modelNode; |
918 | } |
919 | |
920 | void QQuick3DModel::markDirty(QQuick3DModel::QSSGModelDirtyType type) |
921 | { |
922 | if (InstanceRootDirty & quint32(type)) |
923 | QQuick3DObjectPrivate::get(item: this)->dirty(QQuick3DObjectPrivate::InstanceRootChanged); |
924 | |
925 | if (!(m_dirtyAttributes & quint32(type))) { |
926 | m_dirtyAttributes |= quint32(type); |
927 | update(); |
928 | } |
929 | } |
930 | |
931 | void QQuick3DModel::updateSceneManager(QQuick3DSceneManager *sceneManager) |
932 | { |
933 | if (sceneManager) { |
934 | sceneManager->dirtyBoundingBoxList.append(t: this); |
935 | QQuick3DObjectPrivate::refSceneManager(obj: m_skeleton, mgr&: *sceneManager); |
936 | QQuick3DObjectPrivate::refSceneManager(obj: m_skin, mgr&: *sceneManager); |
937 | QQuick3DObjectPrivate::refSceneManager(obj: m_geometry, mgr&: *sceneManager); |
938 | QQuick3DObjectPrivate::refSceneManager(obj: m_instancing, mgr&: *sceneManager); |
939 | for (Material &mat : m_materials) { |
940 | if (!mat.material->parentItem() && !QQuick3DObjectPrivate::get(item: mat.material)->sceneManager) { |
941 | if (!mat.refed) { |
942 | QQuick3DObjectPrivate::refSceneManager(obj: mat.material, mgr&: *sceneManager); |
943 | mat.refed = true; |
944 | } |
945 | } |
946 | } |
947 | } else { |
948 | QQuick3DObjectPrivate::derefSceneManager(obj: m_skeleton); |
949 | QQuick3DObjectPrivate::derefSceneManager(obj: m_skin); |
950 | QQuick3DObjectPrivate::derefSceneManager(obj: m_geometry); |
951 | QQuick3DObjectPrivate::derefSceneManager(obj: m_instancing); |
952 | for (Material &mat : m_materials) { |
953 | if (mat.refed) { |
954 | QQuick3DObjectPrivate::derefSceneManager(obj: mat.material); |
955 | mat.refed = false; |
956 | } |
957 | } |
958 | } |
959 | } |
960 | |
961 | void QQuick3DModel::onMaterialDestroyed(QObject *object) |
962 | { |
963 | bool found = false; |
964 | for (int i = 0; i < m_materials.size(); ++i) { |
965 | if (m_materials[i].material == object) { |
966 | m_materials.removeAt(i: i--); |
967 | found = true; |
968 | } |
969 | } |
970 | if (found) |
971 | markDirty(type: QQuick3DModel::MaterialsDirty); |
972 | } |
973 | |
974 | void QQuick3DModel::qmlAppendMaterial(QQmlListProperty<QQuick3DMaterial> *list, QQuick3DMaterial *material) |
975 | { |
976 | if (material == nullptr) |
977 | return; |
978 | QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object); |
979 | self->m_materials.push_back(t: { .material: material, .refed: false }); |
980 | self->markDirty(type: QQuick3DModel::MaterialsDirty); |
981 | |
982 | if (material->parentItem() == nullptr) { |
983 | // If the material has no parent, check if it has a hierarchical parent that's a QQuick3DObject |
984 | // and re-parent it to that, e.g., inline materials |
985 | QQuick3DObject *parentItem = qobject_cast<QQuick3DObject *>(object: material->parent()); |
986 | if (parentItem) { |
987 | material->setParentItem(parentItem); |
988 | } else { // If no valid parent was found, make sure the material refs our scene manager |
989 | const auto &sceneManager = QQuick3DObjectPrivate::get(item: self)->sceneManager; |
990 | if (sceneManager) { |
991 | QQuick3DObjectPrivate::get(item: material)->refSceneManager(*sceneManager); |
992 | // Have to keep track if we called refSceneManager because we |
993 | // can end up in double deref attempts when a model is going |
994 | // away, due to updateSceneManager() being called on |
995 | // ItemSceneChanged (and also doing deref). We must ensure that |
996 | // is one deref for each ref. |
997 | self->m_materials.last().refed = true; |
998 | } |
999 | // else: If there's no scene manager, defer until one is set, see itemChange() |
1000 | } |
1001 | } |
1002 | |
1003 | // Make sure materials are removed when destroyed |
1004 | connect(sender: material, signal: &QQuick3DMaterial::destroyed, context: self, slot: &QQuick3DModel::onMaterialDestroyed); |
1005 | } |
1006 | |
1007 | QQuick3DMaterial *QQuick3DModel::qmlMaterialAt(QQmlListProperty<QQuick3DMaterial> *list, qsizetype index) |
1008 | { |
1009 | QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object); |
1010 | return self->m_materials.at(i: index).material; |
1011 | } |
1012 | |
1013 | qsizetype QQuick3DModel::qmlMaterialsCount(QQmlListProperty<QQuick3DMaterial> *list) |
1014 | { |
1015 | QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object); |
1016 | return self->m_materials.size(); |
1017 | } |
1018 | |
1019 | void QQuick3DModel::qmlClearMaterials(QQmlListProperty<QQuick3DMaterial> *list) |
1020 | { |
1021 | QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object); |
1022 | for (Material &mat : self->m_materials) { |
1023 | if (mat.material->parentItem() == nullptr) { |
1024 | if (mat.refed) { |
1025 | QQuick3DObjectPrivate::get(item: mat.material)->derefSceneManager(); |
1026 | mat.refed = false; |
1027 | } |
1028 | } |
1029 | mat.material->disconnect(receiver: self, SLOT(onMaterialDestroyed(QObject*))); |
1030 | } |
1031 | self->m_materials.clear(); |
1032 | self->markDirty(type: QQuick3DModel::MaterialsDirty); |
1033 | } |
1034 | |
1035 | void QQuick3DModel::onMorphTargetDestroyed(QObject *object) |
1036 | { |
1037 | bool found = false; |
1038 | for (int i = 0; i < m_morphTargets.size(); ++i) { |
1039 | if (m_morphTargets.at(i) == object) { |
1040 | m_morphTargets.removeAt(i: i--); |
1041 | found = true; |
1042 | } |
1043 | } |
1044 | if (found) { |
1045 | markDirty(type: QQuick3DModel::MorphTargetsDirty); |
1046 | m_numMorphAttribs = 0; |
1047 | } |
1048 | } |
1049 | |
1050 | void QQuick3DModel::qmlAppendMorphTarget(QQmlListProperty<QQuick3DMorphTarget> *list, QQuick3DMorphTarget *morphTarget) |
1051 | { |
1052 | if (morphTarget == nullptr) |
1053 | return; |
1054 | QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object); |
1055 | if (self->m_numMorphAttribs >= 8) { |
1056 | qWarning(msg: "The number of morph attributes exceeds 8. This morph target will be ignored." ); |
1057 | return; |
1058 | } |
1059 | self->m_morphTargets.push_back(t: morphTarget); |
1060 | self->m_numMorphAttribs += morphTarget->numAttribs(); |
1061 | if (self->m_numMorphAttribs > 8) |
1062 | qWarning(msg: "The number of morph attributes exceeds 8. This morph target will be supported partially." ); |
1063 | |
1064 | self->markDirty(type: QQuick3DModel::MorphTargetsDirty); |
1065 | |
1066 | if (morphTarget->parentItem() == nullptr) { |
1067 | // If the morphTarget has no parent, check if it has a hierarchical parent that's a QQuick3DObject |
1068 | // and re-parent it to that, e.g., inline morphTargets |
1069 | QQuick3DObject *parentItem = qobject_cast<QQuick3DObject *>(object: morphTarget->parent()); |
1070 | if (parentItem) { |
1071 | morphTarget->setParentItem(parentItem); |
1072 | } else { // If no valid parent was found, make sure the morphTarget refs our scene manager |
1073 | const auto &scenManager = QQuick3DObjectPrivate::get(item: self)->sceneManager; |
1074 | if (scenManager) |
1075 | QQuick3DObjectPrivate::get(item: morphTarget)->refSceneManager(*scenManager); |
1076 | // else: If there's no scene manager, defer until one is set, see itemChange() |
1077 | } |
1078 | } |
1079 | |
1080 | // Make sure morphTargets are removed when destroyed |
1081 | connect(sender: morphTarget, signal: &QQuick3DMorphTarget::destroyed, context: self, slot: &QQuick3DModel::onMorphTargetDestroyed); |
1082 | } |
1083 | |
1084 | QQuick3DMorphTarget *QQuick3DModel::qmlMorphTargetAt(QQmlListProperty<QQuick3DMorphTarget> *list, qsizetype index) |
1085 | { |
1086 | QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object); |
1087 | if (index >= self->m_morphTargets.size()) { |
1088 | qWarning(msg: "The index exceeds the range of valid morph targets." ); |
1089 | return nullptr; |
1090 | } |
1091 | return self->m_morphTargets.at(i: index); |
1092 | } |
1093 | |
1094 | qsizetype QQuick3DModel::qmlMorphTargetsCount(QQmlListProperty<QQuick3DMorphTarget> *list) |
1095 | { |
1096 | QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object); |
1097 | return self->m_morphTargets.size(); |
1098 | } |
1099 | |
1100 | void QQuick3DModel::qmlClearMorphTargets(QQmlListProperty<QQuick3DMorphTarget> *list) |
1101 | { |
1102 | QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object); |
1103 | for (const auto &morph : std::as_const(t&: self->m_morphTargets)) { |
1104 | if (morph->parentItem() == nullptr) |
1105 | QQuick3DObjectPrivate::get(item: morph)->derefSceneManager(); |
1106 | morph->disconnect(receiver: self, SLOT(onMorphTargetDestroyed(QObject*))); |
1107 | } |
1108 | self->m_morphTargets.clear(); |
1109 | self->m_numMorphAttribs = 0; |
1110 | self->markDirty(type: QQuick3DModel::MorphTargetsDirty); |
1111 | } |
1112 | |
1113 | void QQuick3DModel::setInstancingLodMin(float minDistance) |
1114 | { |
1115 | if (qFuzzyCompare(p1: m_instancingLodMin, p2: minDistance)) |
1116 | return; |
1117 | m_instancingLodMin = minDistance; |
1118 | emit instancingLodMinChanged(); |
1119 | markDirty(type: LodDirty); |
1120 | } |
1121 | |
1122 | void QQuick3DModel::setInstancingLodMax(float maxDistance) |
1123 | { |
1124 | if (qFuzzyCompare(p1: m_instancingLodMax, p2: maxDistance)) |
1125 | return; |
1126 | m_instancingLodMax = maxDistance; |
1127 | emit instancingLodMaxChanged(); |
1128 | markDirty(type: LodDirty); |
1129 | } |
1130 | |
1131 | /*! |
1132 | \qmlproperty real Model::levelOfDetailBias |
1133 | \since 6.5 |
1134 | |
1135 | This property changes the size the model needs to be when rendered before the |
1136 | automatic level of detail meshes are used. Each generated level of detail |
1137 | mesh contains an ideal size value that each level should be shown, which is |
1138 | a ratio of how much of the rendered scene will be that mesh. A model that |
1139 | represents only a few pixels on screen will not require the full geometry |
1140 | to look correct, so a lower level of detail mesh will be used instead in |
1141 | this case. This value is a bias to the ideal value such that a value smaller |
1142 | than \c 1.0 will require an even smaller rendered size before switching to |
1143 | a lesser level of detail. Values above \c 1.0 will lead to lower levels of detail |
1144 | being used sooner. A value of \c 0.0 will disable the usage of levels of detail |
1145 | completely. |
1146 | |
1147 | The default value is \c 1.0 |
1148 | |
1149 | \note This property will only have an effect when the Model's geometry contains |
1150 | levels of detail. |
1151 | |
1152 | \sa Camera::levelOfDetailBias |
1153 | */ |
1154 | |
1155 | float QQuick3DModel::levelOfDetailBias() const |
1156 | { |
1157 | return m_levelOfDetailBias; |
1158 | } |
1159 | |
1160 | void QQuick3DModel::setLevelOfDetailBias(float newLevelOfDetailBias) |
1161 | { |
1162 | if (qFuzzyCompare(p1: m_levelOfDetailBias, p2: newLevelOfDetailBias)) |
1163 | return; |
1164 | m_levelOfDetailBias = newLevelOfDetailBias; |
1165 | emit levelOfDetailBiasChanged(); |
1166 | markDirty(type: QQuick3DModel::PropertyDirty); |
1167 | } |
1168 | |
1169 | QT_END_NAMESPACE |
1170 | |