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
19QT_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
108QQuick3DModel::QQuick3DModel(QQuick3DNode *parent)
109 : QQuick3DNode(*(new QQuick3DNodePrivate(QQuick3DNodePrivate::Type::Model)), parent) {}
110
111QQuick3DModel::~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
137QUrl 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
154QQmlListProperty<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
175QQmlListProperty<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
194QQuick3DInstancing *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*/
208QQuick3DNode *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
217QString 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
236void 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
252bool 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
270bool 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*/
283bool 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*/
295QQuick3DGeometry *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*/
313QQuick3DSkeleton *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*/
332QQuick3DSkin *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*/
351QList<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*/
365QQuick3DBounds3 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*/
378float 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
391bool 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*/
403bool 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
435bool 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 */
472int 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
505QQuick3DBakedLightmap *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*/
516float 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*/
527float QQuick3DModel::instancingLodMax() const
528{
529 return m_instancingLodMax;
530}
531
532void 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
544void 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
554void 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
564void 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
574void 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
596void 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
610void 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
623void 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
634void 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
643void 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
662void 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
674void 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
684void 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
694void 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
703void 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
713void 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
724void 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
748void QQuick3DModel::itemChange(ItemChange change, const ItemChangeData &value)
749{
750 if (change == QQuick3DObject::ItemSceneChange)
751 updateSceneManager(sceneManager: value.sceneManager);
752}
753
754QSSGRenderGraphObject *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
920void 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
931void 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
961void 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
974void 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
1007QQuick3DMaterial *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
1013qsizetype QQuick3DModel::qmlMaterialsCount(QQmlListProperty<QQuick3DMaterial> *list)
1014{
1015 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1016 return self->m_materials.size();
1017}
1018
1019void 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
1035void 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
1050void 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
1084QQuick3DMorphTarget *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
1094qsizetype QQuick3DModel::qmlMorphTargetsCount(QQmlListProperty<QQuick3DMorphTarget> *list)
1095{
1096 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1097 return self->m_morphTargets.size();
1098}
1099
1100void 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
1113void 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
1122void 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
1155float QQuick3DModel::levelOfDetailBias() const
1156{
1157 return m_levelOfDetailBias;
1158}
1159
1160void 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
1169QT_END_NAMESPACE
1170

source code of qtquick3d/src/quick3d/qquick3dmodel.cpp