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