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 : std::as_const(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->instancingLodFactor = m_instancingLodFactor;
918 modelNode->motionVectorEnabled = m_motionVectorEnabled;
919 modelNode->motionVectorScale = m_motionVectorScale;
920 }
921
922 if (m_dirtyAttributes & ReflectionDirty) {
923 modelNode->receivesReflections = m_receivesReflections;
924 modelNode->castsReflections = m_castsReflections;
925 }
926
927 m_dirtyAttributes = dirtyAttribute;
928
929 return modelNode;
930}
931
932void QQuick3DModel::markDirty(QQuick3DModel::QSSGModelDirtyType type)
933{
934 if (InstanceRootDirty & quint32(type))
935 QQuick3DObjectPrivate::get(this)->dirty(QQuick3DObjectPrivate::InstanceRootChanged);
936
937 if (!(m_dirtyAttributes & quint32(type))) {
938 m_dirtyAttributes |= quint32(type);
939 update();
940 }
941}
942
943void QQuick3DModel::updateSceneManager(QQuick3DSceneManager *sceneManager)
944{
945 if (sceneManager) {
946 sceneManager->dirtyBoundingBoxList.append(this);
947 QQuick3DObjectPrivate::refSceneManager(m_skeleton, *sceneManager);
948 QQuick3DObjectPrivate::refSceneManager(m_skin, *sceneManager);
949 QQuick3DObjectPrivate::refSceneManager(m_geometry, *sceneManager);
950 QQuick3DObjectPrivate::refSceneManager(m_instancing, *sceneManager);
951 for (Material &mat : m_materials) {
952 if (!mat.material->parentItem() && !QQuick3DObjectPrivate::get(mat.material)->sceneManager) {
953 if (!mat.refed) {
954 QQuick3DObjectPrivate::refSceneManager(mat.material, *sceneManager);
955 mat.refed = true;
956 }
957 }
958 }
959 } else {
960 QQuick3DObjectPrivate::derefSceneManager(m_skeleton);
961 QQuick3DObjectPrivate::derefSceneManager(m_skin);
962 QQuick3DObjectPrivate::derefSceneManager(m_geometry);
963 QQuick3DObjectPrivate::derefSceneManager(m_instancing);
964 for (Material &mat : m_materials) {
965 if (mat.refed) {
966 QQuick3DObjectPrivate::derefSceneManager(mat.material);
967 mat.refed = false;
968 }
969 }
970 }
971}
972
973void QQuick3DModel::onMaterialDestroyed(QObject *object)
974{
975 bool found = false;
976 for (int i = 0; i < m_materials.size(); ++i) {
977 if (m_materials[i].material == object) {
978 m_materials.removeAt(i--);
979 found = true;
980 }
981 }
982 if (found)
983 markDirty(QQuick3DModel::MaterialsDirty);
984}
985
986void QQuick3DModel::qmlAppendMaterial(QQmlListProperty<QQuick3DMaterial> *list, QQuick3DMaterial *material)
987{
988 if (material == nullptr)
989 return;
990 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
991 self->m_materials.push_back({ material, false });
992 self->markDirty(QQuick3DModel::MaterialsDirty);
993
994 if (material->parentItem() == nullptr) {
995 // If the material has no parent, check if it has a hierarchical parent that's a QQuick3DObject
996 // and re-parent it to that, e.g., inline materials
997 QQuick3DObject *parentItem = qobject_cast<QQuick3DObject *>(material->parent());
998 if (parentItem) {
999 material->setParentItem(parentItem);
1000 } else { // If no valid parent was found, make sure the material refs our scene manager
1001 const auto &sceneManager = QQuick3DObjectPrivate::get(self)->sceneManager;
1002 if (sceneManager) {
1003 QQuick3DObjectPrivate::get(material)->refSceneManager(*sceneManager);
1004 // Have to keep track if we called refSceneManager because we
1005 // can end up in double deref attempts when a model is going
1006 // away, due to updateSceneManager() being called on
1007 // ItemSceneChanged (and also doing deref). We must ensure that
1008 // is one deref for each ref.
1009 self->m_materials.last().refed = true;
1010 }
1011 // else: If there's no scene manager, defer until one is set, see itemChange()
1012 }
1013 }
1014
1015 // Make sure materials are removed when destroyed
1016 connect(material, &QQuick3DMaterial::destroyed, self, &QQuick3DModel::onMaterialDestroyed);
1017}
1018
1019QQuick3DMaterial *QQuick3DModel::qmlMaterialAt(QQmlListProperty<QQuick3DMaterial> *list, qsizetype index)
1020{
1021 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1022 return self->m_materials.at(index).material;
1023}
1024
1025qsizetype QQuick3DModel::qmlMaterialsCount(QQmlListProperty<QQuick3DMaterial> *list)
1026{
1027 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1028 return self->m_materials.size();
1029}
1030
1031void QQuick3DModel::qmlClearMaterials(QQmlListProperty<QQuick3DMaterial> *list)
1032{
1033 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1034 for (Material &mat : self->m_materials) {
1035 if (mat.material->parentItem() == nullptr) {
1036 if (mat.refed) {
1037 QQuick3DObjectPrivate::get(mat.material)->derefSceneManager();
1038 mat.refed = false;
1039 }
1040 }
1041 disconnect(mat.material, &QQuick3DMaterial::destroyed, self, &QQuick3DModel::onMaterialDestroyed);
1042 }
1043 self->m_materials.clear();
1044 self->markDirty(QQuick3DModel::MaterialsDirty);
1045}
1046
1047void QQuick3DModel::onMorphTargetDestroyed(QObject *object)
1048{
1049 bool found = false;
1050 for (int i = 0; i < m_morphTargets.size(); ++i) {
1051 if (m_morphTargets.at(i) == object) {
1052 m_morphTargets.removeAt(i--);
1053 found = true;
1054 }
1055 }
1056 if (found) {
1057 markDirty(QQuick3DModel::MorphTargetsDirty);
1058 }
1059}
1060
1061void QQuick3DModel::qmlAppendMorphTarget(QQmlListProperty<QQuick3DMorphTarget> *list, QQuick3DMorphTarget *morphTarget)
1062{
1063 if (morphTarget == nullptr)
1064 return;
1065 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1066 self->m_morphTargets.push_back(morphTarget);
1067
1068 self->markDirty(QQuick3DModel::MorphTargetsDirty);
1069
1070 if (morphTarget->parentItem() == nullptr) {
1071 // If the morphTarget has no parent, check if it has a hierarchical parent that's a QQuick3DObject
1072 // and re-parent it to that, e.g., inline morphTargets
1073 QQuick3DObject *parentItem = qobject_cast<QQuick3DObject *>(morphTarget->parent());
1074 if (parentItem) {
1075 morphTarget->setParentItem(parentItem);
1076 } else { // If no valid parent was found, make sure the morphTarget refs our scene manager
1077 const auto &scenManager = QQuick3DObjectPrivate::get(self)->sceneManager;
1078 if (scenManager)
1079 QQuick3DObjectPrivate::get(morphTarget)->refSceneManager(*scenManager);
1080 // else: If there's no scene manager, defer until one is set, see itemChange()
1081 }
1082 }
1083
1084 // Make sure morphTargets are removed when destroyed
1085 connect(morphTarget, &QQuick3DMorphTarget::destroyed, self, &QQuick3DModel::onMorphTargetDestroyed);
1086}
1087
1088QQuick3DMorphTarget *QQuick3DModel::qmlMorphTargetAt(QQmlListProperty<QQuick3DMorphTarget> *list, qsizetype index)
1089{
1090 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1091 if (index >= self->m_morphTargets.size()) {
1092 qWarning("The index exceeds the range of valid morph targets.");
1093 return nullptr;
1094 }
1095 return self->m_morphTargets.at(index);
1096}
1097
1098qsizetype QQuick3DModel::qmlMorphTargetsCount(QQmlListProperty<QQuick3DMorphTarget> *list)
1099{
1100 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1101 return self->m_morphTargets.size();
1102}
1103
1104void QQuick3DModel::qmlClearMorphTargets(QQmlListProperty<QQuick3DMorphTarget> *list)
1105{
1106 QQuick3DModel *self = static_cast<QQuick3DModel *>(list->object);
1107 for (const auto &morph : std::as_const(self->m_morphTargets)) {
1108 if (morph->parentItem() == nullptr)
1109 QQuick3DObjectPrivate::get(morph)->derefSceneManager();
1110 disconnect(morph, &QQuick3DMorphTarget::destroyed, self, &QQuick3DModel::onMorphTargetDestroyed);
1111 }
1112 self->m_morphTargets.clear();
1113 self->markDirty(QQuick3DModel::MorphTargetsDirty);
1114}
1115
1116void QQuick3DModel::setInstancingLodMin(float minDistance)
1117{
1118 if (qFuzzyCompare(m_instancingLodMin, minDistance))
1119 return;
1120 m_instancingLodMin = minDistance;
1121 emit instancingLodMinChanged();
1122 markDirty(LodDirty);
1123}
1124
1125void QQuick3DModel::setInstancingLodMax(float maxDistance)
1126{
1127 if (qFuzzyCompare(m_instancingLodMax, maxDistance))
1128 return;
1129 m_instancingLodMax = maxDistance;
1130 emit instancingLodMaxChanged();
1131 markDirty(LodDirty);
1132}
1133
1134/*!
1135 \qmlproperty real Model::instancingLodFactor
1136 \since 6.12
1137 \default 0.0
1138
1139 This property allows manual determination of the level of detail (LOD) for
1140 specific levels of instanced models.
1141
1142 In situations where a scene contains multiple LOD levels for large instance
1143 tables, it is necessary to manually select the most appropriate detail level.
1144 This is required because automatic LOD management cannot correctly calculate
1145 the detail for individual instances, as their transformation data only becomes
1146 available during the GPU rendering stage.
1147
1148 To reduce geometry overhead at specific LOD levels, you can manually specify
1149 \c instancingLodFactor. The closer the value is to \c 1.0, the less geometry
1150 will be rendered for the object. A value of \c 0.0 means the entire geometry
1151 will be offloaded/rendered at full detail.
1152
1153 \note This property is specifically used to override automatic LOD calculation
1154 when the proximity of individual instances to the camera is not predetermined.
1155
1156 \sa levelOfDetailBias
1157*/
1158
1159float QQuick3DModel::instancingLodFactor() const
1160{
1161 return m_instancingLodFactor;
1162}
1163
1164void QQuick3DModel::setInstancingLodFactor(float newInstancingLodFactor)
1165{
1166 if (qFuzzyCompare(m_instancingLodFactor, newInstancingLodFactor))
1167 return;
1168
1169 m_instancingLodFactor = newInstancingLodFactor;
1170 emit instancingLodFactorChanged();
1171 markDirty(QQuick3DModel::PropertyDirty);
1172}
1173
1174/*!
1175 \qmlproperty real Model::levelOfDetailBias
1176 \since 6.5
1177
1178 This property changes the size the model needs to be when rendered before the
1179 automatic level of detail meshes are used. Each generated level of detail
1180 mesh contains an ideal size value that each level should be shown, which is
1181 a ratio of how much of the rendered scene will be that mesh. A model that
1182 represents only a few pixels on screen will not require the full geometry
1183 to look correct, so a lower level of detail mesh will be used instead in
1184 this case. This value is a bias to the ideal value such that a value smaller
1185 than \c 1.0 will require an even smaller rendered size before switching to
1186 a lesser level of detail. Values above \c 1.0 will lead to lower levels of detail
1187 being used sooner. A value of \c 0.0 will disable the usage of levels of detail
1188 completely.
1189
1190 The default value is \c 1.0
1191
1192 \note This property will only have an effect when the Model's geometry contains
1193 levels of detail.
1194
1195 \sa Camera::levelOfDetailBias
1196*/
1197
1198float QQuick3DModel::levelOfDetailBias() const
1199{
1200 return m_levelOfDetailBias;
1201}
1202
1203void QQuick3DModel::setLevelOfDetailBias(float newLevelOfDetailBias)
1204{
1205 if (qFuzzyCompare(m_levelOfDetailBias, newLevelOfDetailBias))
1206 return;
1207 m_levelOfDetailBias = newLevelOfDetailBias;
1208 emit levelOfDetailBiasChanged();
1209 markDirty(QQuick3DModel::PropertyDirty);
1210}
1211
1212/*!
1213 \qmlproperty real Model::texelsPerUnit
1214 \since 6.10
1215 \default 0.0
1216
1217 A value greater than zero means this value will override the
1218 \l {Lightmapper::texelsPerUnit} value for this specific model during
1219 lightmap baking.
1220
1221 \sa Lightmapper::texelsPerUnit
1222*/
1223
1224float QQuick3DModel::texelsPerUnit() const
1225{
1226 return m_texelsPerUnit;
1227}
1228
1229void QQuick3DModel::setTexelsPerUnit(float newTexelsPerUnit)
1230{
1231 if (qFuzzyCompare(m_texelsPerUnit, newTexelsPerUnit))
1232 return;
1233 m_texelsPerUnit = newTexelsPerUnit;
1234 emit texelsPerUnitChanged();
1235 markDirty(PropertyDirty);
1236}
1237
1238/*!
1239 \qmlproperty bool Model::motionVectorEnabled
1240 \since 6.11
1241 \default true
1242
1243 Specifies whether the model participates in the motion vector pass.
1244
1245 When this property is set to \c true, the model contributes its
1246 motion information to the motion vector buffer. This is used by
1247 temporal anti-aliasing (TAA) and other effects that rely on
1248 per-pixel motion data.
1249
1250 When set to \c false, the model is excluded from motion vector
1251 generation.
1252
1253 \sa Model::motionVectorScale
1254*/
1255
1256bool QQuick3DModel::motionVectorEnabled() const
1257{
1258 return m_motionVectorEnabled;
1259}
1260
1261void QQuick3DModel::setMotionVectorEnabled(bool newMotionVectorEnabled)
1262{
1263 if (m_motionVectorEnabled == newMotionVectorEnabled)
1264 return;
1265 m_motionVectorEnabled = newMotionVectorEnabled;
1266 emit motionVectorEnabledChanged();
1267 markDirty(QQuick3DModel::PropertyDirty);
1268}
1269
1270/*!
1271 \qmlproperty real Model::motionVectorScale
1272 \since 6.11
1273 \default 1.0
1274
1275 Specifies a scaling factor applied to the model's generated motion
1276 vectors.
1277
1278 A value of \c 1.0 preserves the model's actual motion, while values
1279 greater than \c 1.0 exaggerate the motion vectors and values lower
1280 than \c 1.0 reduce them. This can be useful for stylized effects or
1281 fine-tuning visual behavior in effects that rely on the motion
1282 vector buffer.
1283
1284 \sa Model::motionVectorEnabled
1285*/
1286
1287float QQuick3DModel::motionVectorScale() const
1288{
1289 return m_motionVectorScale;
1290}
1291
1292void QQuick3DModel::setMotionVectorScale(float newMotionVectorScale)
1293{
1294 if (qFuzzyCompare(m_motionVectorScale, newMotionVectorScale))
1295 return;
1296 m_motionVectorScale = newMotionVectorScale;
1297 emit motionVectorScaleChanged();
1298 markDirty(QQuick3DModel::PropertyDirty);
1299}
1300
1301QT_END_NAMESPACE
Combined button and popup list for selecting options.