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