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
qquick3dinstancing.cpp
Go to the documentation of this file.
1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
7#include <QtQuick3DRuntimeRender/private/qssgrenderinstancetable_p.h>
8#include <QtQuick3DUtils/private/qssgutils_p.h>
9#include <QXmlStreamReader>
10#include <QtQml/QQmlFile>
11
12QT_BEGIN_NAMESPACE
13
14/*!
15 \qmltype Instancing
16 \inherits Object3D
17 \inqmlmodule QtQuick3D
18 \nativetype QQuick3DInstancing
19 \since 6.2
20 \brief Base type for instance tables.
21
22 \l {Instanced Rendering}{Instanced rendering} allows duplicating a model with variations.
23
24 The Instancing type defines a table that specifies how each instance is modified relative to the
25 base model. The table has an entry for each index, containing a transform matrix, a color, and
26 generic data for use by custom materials. To use instancing, set a model's
27 \l{Model::instancing}{instancing} property to reference an Instancing object.
28
29 An application can define an Instancing object in C++ by subclassing QQuick3DInstancing,
30 or it can use one of the pre-defined QML types: InstanceList FileInstancing, or RandomInstancing.
31 In addition, it is possible to use a \l {ParticleSystem3D}{particle system} to define an
32 instancing table by using the \l{ModelParticle3D::instanceTable}{ModelParticle3D.instanceTable}
33 property.
34*/
35
36/*!
37 \qmlproperty int Instancing::instanceCountOverride
38
39 Set this property to limit the number of instances without regenerating or re-uploading the instance table.
40 This allows very inexpensive animation of the number of instances rendered.
41*/
42
43/*!
44 \property QQuick3DInstancing::instanceCountOverride
45
46 Set this property to limit the number of instances without regenerating or re-uploading the instance table.
47 This allows very inexpensive animation of the number of instances rendered.
48*/
49
50/*!
51 \qmlproperty bool Instancing::hasTransparency
52
53 Set this property to true if the instancing table contains alpha values that should be used when
54 rendering the model. This property only makes a difference if the model is opaque: If the model has a
55 transparent \l{Model::materials}{material}, or an \l{Node::opacity}{opacity} less than one, the
56 alpha value from the table will be used regardless.
57
58 \note Enabling alpha blending may cause rendering issues when instances overlap. See the
59 \l{Alpha-blending and instancing}{alpha blending and instancing} documentation for details.
60*/
61
62/*!
63 \property QQuick3DInstancing::hasTransparency
64
65 Set this property to true if the instancing table contains alpha values that should be used when
66 rendering the model. This property only makes a difference if the model is opaque: If the model has a
67 transparent \l{Model::materials}{material}, or an \l{Node::opacity}{opacity} less than one, the
68 alpha value from the table will be used regardless.
69
70 \note Enabling alpha blending may cause rendering issues when instances overlap. See the
71 \l{Alpha-blending and instancing}{alpha blending and instancing} documentation for details.
72*/
73
74/*!
75 \qmlproperty bool Instancing::depthSortingEnabled
76
77 Holds the depth sorting enabled value for the instance table. When enabled, instances are sorted
78 and rendered from the furthest instance from the camera to the nearest i.e. back-to-front.
79 If disabled, which is the default, instances are rendered in the order they are specified in
80 the instance table.
81
82 \note The instances are only sorted against each other. Instances are not sorted against other
83 objects in the scene.
84 \note The sorting increases the frame preparation time especially with large instance counts.
85*/
86
87/*!
88 \property QQuick3DInstancing::depthSortingEnabled
89
90 Holds the depth sorting enabled value for the instance table. When enabled, instances are sorted
91 and rendered from the furthest instance from the camera to the nearest i.e. back-to-front.
92 If disabled, which is the default, instances are rendered in the order they are specified in
93 the instance table.
94
95 \note The instances are only sorted against each other. Instances are not sorted against other
96 objects in the scene.
97 \note The sorting increases the frame preparation time especially with large instance counts.
98*/
99
100/*!
101 \property QQuick3DInstancing::shadowBoundsMinimum
102 \since 6.9
103
104 Sets the minimum bounds used when calculating the shadow map bounds of the
105 models in the instance table.
106
107 Default value: \c{(1, 1, 1)}
108
109 \note This property is only enabled when the respective components of
110 shadowBoundsMinimum are smaller than those in shadowBoundsMaximum.
111 Otherwise the bounds are calculated automatically.
112
113 \sa shadowBoundsMaximum
114*/
115/*!
116 \qmlproperty vector3d Instancing::shadowBoundsMinimum
117
118 Sets the minimum bounds used when calculating the shadow map bounds of the models in the
119 instance table.
120
121 Default value: \c{(1, 1, 1)}
122
123 \note This property is only enabled when the respective components of
124 Instancing::shadowBoundsMinimum are smaller than those in
125 Instancing::shadowBoundsMaximum. Otherwise the bounds are calculated automatically.
126 \sa shadowBoundsMaximum
127*/
128
129/*!
130 \property QQuick3DInstancing::shadowBoundsMaximum
131 \since 6.9
132
133 Sets the maximum bounds used when calculating the shadow map bounds of the
134 models in the instance table.
135
136 Default value: \c{(-1, -1, -1)}
137
138 \note This property is only enabled when the respective components of
139 shadowBoundsMinimum are smaller than those in shadowBoundsMaximum.
140 Otherwise the bounds are calculated automatically.
141
142 \sa shadowBoundsMinimum
143*/
144/*!
145 \qmlproperty vector3d Instancing::shadowBoundsMaximum
146
147 Sets the maximum bounds used when calculating the shadow map bounds of the models in the
148 instance table.
149
150 Default value: \c{(-1, -1, -1)}
151
152 \note This property is only enabled when the respective components of
153 Instancing::shadowBoundsMinimum are smaller than those in
154 Instancing::shadowBoundsMaximum. Otherwise the bounds are calculated automatically.
155 \sa shadowBoundsMinimum
156*/
157
158/*!
159 \class QQuick3DInstancing
160 \inmodule QtQuick3D
161 \inherits QQuick3DObject
162 \since 6.2
163 \brief Base class for defining instance tables.
164
165 The QQuick3DInstancing class can be inherited to specify a custom instance table
166 for a Model in the Qt Quick 3D scene.
167
168 This class is abstract: To use it, create a subclass and implement \l getInstanceBuffer().
169*/
170
171/*!
172 \fn QByteArray QQuick3DInstancing::getInstanceBuffer(int *instanceCount)
173
174 Implement this function to return the contents of the instance table. The number of instances should be
175 returned in \a instanceCount. The subclass is responsible for caching the result if necessary. If the
176 instance table changes, the subclass should call markDirty().
177 */
178
179QQuick3DInstancingPrivate::QQuick3DInstancingPrivate()
180 : QQuick3DObjectPrivate(QQuick3DObjectPrivate::Type::ModelInstance)
181{
182}
183
184QQuick3DInstancing::QQuick3DInstancing(QQuick3DObject *parent)
185 : QQuick3DObject(*new QQuick3DInstancingPrivate, parent)
186{
187}
188
189QQuick3DInstancing::~QQuick3DInstancing()
190{
191}
192
193/*!
194 \internal
195 Returns the content of the instancing table for testing purposes.
196*/
197QByteArray QQuick3DInstancing::instanceBuffer(int *instanceCount)
198{
199 Q_D(QQuick3DInstancing);
200 QByteArray retval = getInstanceBuffer(instanceCount);
201 if (instanceCount && d->m_instanceCountOverride >= 0)
202 *instanceCount = qMin(d->m_instanceCountOverride, *instanceCount);
203 return retval;
204}
205
206int QQuick3DInstancing::instanceCountOverride() const
207{
208 Q_D(const QQuick3DInstancing);
209 return d->m_instanceCountOverride;
210}
211
212bool QQuick3DInstancing::hasTransparency() const
213{
214 Q_D(const QQuick3DInstancing);
215 return d->m_hasTransparency;
216}
217
218bool QQuick3DInstancing::depthSortingEnabled() const
219{
220 Q_D(const QQuick3DInstancing);
221 return d->m_depthSortingEnabled;
222}
223
224QVector3D QQuick3DInstancing::shadowBoundsMinimum() const
225{
226 Q_D(const QQuick3DInstancing);
227 return d->m_shadowBoundsMinimum;
228}
229
230QVector3D QQuick3DInstancing::shadowBoundsMaximum() const
231{
232 Q_D(const QQuick3DInstancing);
233 return d->m_shadowBoundsMaximum;
234}
235
236const QQuick3DInstancing::InstanceTableEntry *QQuick3DInstancing::getInstanceEntry(int index)
237{
238 const QByteArray data = getInstanceBuffer(nullptr);
239 if (index >= int(data.size() / sizeof(InstanceTableEntry)))
240 return nullptr;
241 return reinterpret_cast<const QQuick3DInstancing::InstanceTableEntry*>(data.constData()) + index;
242}
243
244QVector3D QQuick3DInstancing::InstanceTableEntry::getPosition() const
245{
246 return QVector3D{ row0[3], row1[3], row2[3] };
247}
248
249QVector3D QQuick3DInstancing::InstanceTableEntry::getScale() const
250{
251 const QVector3D col0{row0[0], row1[0], row2[0]};
252 const QVector3D col1{row0[1], row1[1], row2[1]};
253 const QVector3D col2{row0[2], row1[2], row2[2]};
254 const float scaleX = col0.length();
255 const float scaleY = col1.length();
256 const float scaleZ = col2.length();
257 return QVector3D(scaleX, scaleY, scaleZ);
258}
259
260QQuaternion QQuick3DInstancing::InstanceTableEntry::getRotation() const
261{
262 const QVector3D col0 = QVector3D(row0[0], row1[0], row2[0]).normalized();
263 const QVector3D col1 = QVector3D(row0[1], row1[1], row2[1]).normalized();
264 const QVector3D col2 = QVector3D(row0[2], row1[2], row2[2]).normalized();
265
266 const float data3x3[3*3] { // row-major order
267 col0[0], col1[0], col2[0],
268 col0[1], col1[1], col2[1],
269 col0[2], col1[2], col2[2],
270 };
271 QMatrix3x3 rot(data3x3);
272 return QQuaternion::fromRotationMatrix(rot).normalized();
273}
274
275QColor QQuick3DInstancing::InstanceTableEntry::getColor() const
276{
277 return QColor::fromRgbF(color[0], color[1], color[2], color[3]);
278}
279
280/*!
281 \qmlmethod vector3d QtQuick3D::Instancing::instancePosition(int index)
282 \since 6.3
283
284 Returns the position of the instance at \a index
285
286 \sa instanceScale, instanceRotation, instanceColor, instanceCustomData
287*/
288
289QVector3D QQuick3DInstancing::instancePosition(int index)
290{
291 auto *entry = getInstanceEntry(index);
292 if (!entry)
293 return {};
294
295 return QVector3D{ entry->row0[3], entry->row1[3], entry->row2[3] };
296}
297
298/*!
299 \qmlmethod vector3d QtQuick3D::Instancing::instanceScale(int index)
300 \since 6.3
301
302 Returns the scale of the instance at \a index
303
304 \sa instancePosition, instanceScale, instanceRotation, instanceColor, instanceCustomData
305*/
306
307QVector3D QQuick3DInstancing::instanceScale(int index)
308{
309 auto *entry = getInstanceEntry(index);
310 if (!entry)
311 return {};
312 return entry->getScale();
313}
314
315/*!
316 \qmlmethod quaternion QtQuick3D::Instancing::instanceRotation(int index)
317 \since 6.3
318
319 Returns a quaternion representing the rotation of the instance at \a index
320
321 \sa instancePosition, instanceScale, instanceRotation, instanceColor, instanceCustomData
322*/
323
324QQuaternion QQuick3DInstancing::instanceRotation(int index)
325{
326 const auto *entry = getInstanceEntry(index);
327 if (!entry)
328 return {};
329 return entry->getRotation();
330}
331
332/*!
333 \qmlmethod color QtQuick3D::Instancing::instanceColor(int index)
334 \since 6.3
335
336 Returns the color of the instance at \a index
337
338 \sa instancePosition, instanceScale, instanceRotation, instanceColor, instanceCustomData
339*/
340
341QColor QQuick3DInstancing::instanceColor(int index)
342{
343 const auto *entry = getInstanceEntry(index);
344 if (!entry)
345 return {};
346 return entry->getColor();
347}
348
349/*!
350 \qmlmethod vector3d QtQuick3D::Instancing::instanceCustomData(int index)
351 \since 6.3
352
353 Returns the custom data for the instance at \a index
354
355 \sa instancePosition, instanceScale, instanceRotation, instanceColor, instanceCustomData
356*/
357
358QVector4D QQuick3DInstancing::instanceCustomData(int index)
359{
360 const auto *entry = getInstanceEntry(index);
361 if (!entry)
362 return {};
363 return entry->instanceData;
364}
365
366void QQuick3DInstancing::setInstanceCountOverride(int instanceCountOverride)
367{
368 Q_D(QQuick3DInstancing);
369 if (d->m_instanceCountOverride == instanceCountOverride)
370 return;
371
372 d->m_instanceCountOverride = instanceCountOverride;
373 d->m_instanceCountOverrideChanged = true;
374 d->dirty(QQuick3DObjectPrivate::DirtyType::Content);
375 emit instanceCountOverrideChanged();
376}
377
378void QQuick3DInstancing::setHasTransparency(bool hasTransparency)
379{
380 Q_D(QQuick3DInstancing);
381 if (d->m_hasTransparency == hasTransparency)
382 return;
383
384 d->m_hasTransparency = hasTransparency;
385 d->dirty(QQuick3DObjectPrivate::DirtyType::Content);
386 emit hasTransparencyChanged();
387}
388
389void QQuick3DInstancing::setDepthSortingEnabled(bool enabled)
390{
391 Q_D(QQuick3DInstancing);
392 if (d->m_depthSortingEnabled == enabled)
393 return;
394
395 d->m_depthSortingEnabled = enabled;
396 d->dirty(QQuick3DObjectPrivate::DirtyType::Content);
397 emit depthSortingEnabledChanged();
398}
399
400void QQuick3DInstancing::setShadowBoundsMinimum(const QVector3D &newShadowBoundsMinimum)
401{
402 Q_D(QQuick3DInstancing);
403 if (d->m_shadowBoundsMinimum == newShadowBoundsMinimum)
404 return;
405
406 d->m_shadowBoundsMinimum = newShadowBoundsMinimum;
407 d->dirty(QQuick3DObjectPrivate::DirtyType::Content);
408 emit shadowBoundsMinimumChanged();
409}
410
411void QQuick3DInstancing::setShadowBoundsMaximum(const QVector3D &newShadowBoundsMaximum)
412{
413 Q_D(QQuick3DInstancing);
414 if (d->m_shadowBoundsMinimum == newShadowBoundsMaximum)
415 return;
416
417 d->m_shadowBoundsMaximum = newShadowBoundsMaximum;
418 d->dirty(QQuick3DObjectPrivate::DirtyType::Content);
419 emit shadowBoundsMaximumChanged();
420}
421
422/*!
423 Mark that the instance data has changed and must be uploaded again.
424
425 \sa getInstanceBuffer, instanceCountOverride
426 */
427
428void QQuick3DInstancing::markDirty()
429{
430 Q_D(QQuick3DInstancing);
431 d->dirty(QQuick3DObjectPrivate::DirtyType::Content);
432 d->m_instanceDataChanged = true;
433 emit instanceTableChanged();
434}
435
436QSSGRenderGraphObject *QQuick3DInstancing::updateSpatialNode(QSSGRenderGraphObject *node)
437{
438 Q_D(QQuick3DInstancing);
439 if (!node) {
440 markAllDirty();
441 node = new QSSGRenderInstanceTable();
442 emit instanceNodeDirty();
443 d->m_instanceDataChanged = true;
444 }
445 QQuick3DObject::updateSpatialNode(node);
446 auto effectiveInstanceCount = [d]() {
447 if (d->m_instanceCountOverride >= 0)
448 return qMin(d->m_instanceCount, d->m_instanceCountOverride);
449 return d->m_instanceCount;
450 };
451 auto *instanceTable = static_cast<QSSGRenderInstanceTable *>(node);
452 if (d->m_instanceDataChanged) {
453 QByteArray buffer = getInstanceBuffer(&d->m_instanceCount);
454 instanceTable->setData(buffer, effectiveInstanceCount(), sizeof(InstanceTableEntry));
455 d->m_instanceDataChanged = false;
456 } else if (d->m_instanceCountOverrideChanged) {
457 instanceTable->setInstanceCountOverride(effectiveInstanceCount());
458 }
459 d->m_instanceCountOverrideChanged = false;
460 instanceTable->setHasTransparency(d->m_hasTransparency);
461 instanceTable->setDepthSorting(d->m_depthSortingEnabled);
462 instanceTable->setShadowBoundsMinimum(d->m_shadowBoundsMinimum);
463 instanceTable->setShadowBoundsMaximum(d->m_shadowBoundsMaximum);
464 return node;
465}
466
467static inline QQuick3DInstancing::InstanceTableEntry calculate(const QVector3D &position, const QVector3D &scale,
468 const QVector3D &eulerRotation, const QColor &color,
469 const QVector4D &customData)
470{
471 QMatrix4x4 xform;
472
473 xform(0, 0) = scale[0];
474 xform(1, 1) = scale[1];
475 xform(2, 2) = scale[2];
476
477 QQuaternion quaternion = QQuaternion::fromEulerAngles(eulerRotation);
478 xform = QMatrix4x4(quaternion.toRotationMatrix()) * xform;
479
480 xform(0, 3) += position[0];
481 xform(1, 3) += position[1];
482 xform(2, 3) += position[2];
483
484 auto linearColor = QSSGUtils::color::sRGBToLinear(color);
485
486 return {
487 xform.row(0),
488 xform.row(1),
489 xform.row(2),
490 linearColor,
491 customData
492 };
493}
494
495/*!
496 Converts the
497 \a position
498 \a scale
499 \a eulerRotation
500 \a color
501 and
502 \a customData
503 to the instance table format expected by the standard vertex shaders. Typical pattern:
504
505 \code
506 QByteArray MyInstanceTable::getInstanceBuffer(int *instanceCount)
507 {
508 QByteArray instanceData;
509
510 ...
511
512 auto entry = calculateTableEntry({xPos, yPos, zPos}, {xScale, yScale, zScale}, {xRot, yRot, zRot}, color, {});
513 instanceData.append(reinterpret_cast<const char *>(&entry), sizeof(entry));
514 \endcode
515
516 \sa calculateTableEntryFromQuaternion
517 */
518QQuick3DInstancing::InstanceTableEntry QQuick3DInstancing::calculateTableEntry(const QVector3D &position, const QVector3D &scale,
519 const QVector3D &eulerRotation, const QColor &color, const QVector4D &customData)
520{
521 return calculate(position, scale, eulerRotation, color, customData);
522}
523
524/*!
525 Converts the
526 \a position
527 \a scale
528 \a rotation
529 \a color
530 and
531 \a customData
532 to the instance table format expected by the standard vertex shaders.
533
534 This is the same as calculateTableEntry(), except for using a quaternion to specify the rotation.
535 */
536QQuick3DInstancing::InstanceTableEntry QQuick3DInstancing::calculateTableEntryFromQuaternion(const QVector3D &position, const QVector3D &scale, const QQuaternion &rotation, const QColor &color, const QVector4D &customData)
537{
538 QMatrix4x4 xform;
539
540 xform(0, 0) = scale[0];
541 xform(1, 1) = scale[1];
542 xform(2, 2) = scale[2];
543
544 xform = QMatrix4x4(rotation.toRotationMatrix()) * xform;
545
546 xform(0, 3) += position[0];
547 xform(1, 3) += position[1];
548 xform(2, 3) += position[2];
549
550 auto linearColor = QSSGUtils::color::sRGBToLinear(color);
551
552 return {
553 xform.row(0),
554 xform.row(1),
555 xform.row(2),
556 linearColor,
557 customData
558 };
559}
560
561/*!
562 \qmltype InstanceList
563 \inherits Instancing
564 \inqmlmodule QtQuick3D
565 \brief Allows manually specifying instancing in QML.
566
567 The InstanceList type makes it possible to define an instance table manually in QML.
568
569 The following example creates an instance table with two items:
570 \qml
571 InstanceList {
572 id: manualInstancing
573 instances: [
574 InstanceListEntry {
575 position: Qt.vector3d(0, 0, -60)
576 eulerRotation: Qt.vector3d(-10, 0, 30)
577 color: "red"
578 },
579 InstanceListEntry {
580 position: Qt.vector3d(50, 10, 100)
581 eulerRotation: Qt.vector3d(0, 180, 0)
582 color: "green"
583 }
584 ]
585 }
586 \endqml
587
588 It is also possible to populate the instances property by just adding children to the
589 InstanceList. The following example is equivalent to the previous one:
590 \qml
591 InstanceList {
592 id: manualInstancing
593 InstanceListEntry {
594 position: Qt.vector3d(0, 0, -60)
595 eulerRotation: Qt.vector3d(-10, 0, 30)
596 color: "red"
597 }
598 InstanceListEntry {
599 position: Qt.vector3d(50, 10, 100)
600 eulerRotation: Qt.vector3d(0, 180, 0)
601 color: "green"
602 }
603 }
604 \endqml
605
606 Each InstanceListEntry is an object that can have property bindings and animations. This gives
607 great flexibility, but also causes memory overhead. Therefore, it is not recommended to use
608 InstanceList for procedurally generated tables containing thousands (or millions) of
609 instances. Also, any property change to an entry will cause the entire instance table to be
610 recalculated and uploaded to the GPU.
611
612 \sa RandomInstancing, QQuick3DInstancing
613*/
614
615/*!
616 \qmlproperty List<QtQuick3D::InstanceListEntry> InstanceList::instances
617 \qmldefault
618
619 This property contains the list of instance definitions. Modifying this list, or any of its elements, will cause the instance table to be updated.
620*/
621
622QQuick3DInstanceList::QQuick3DInstanceList(QQuick3DObject *parent) : QQuick3DInstancing(parent) {}
623
624QQuick3DInstanceList::~QQuick3DInstanceList() {}
625
626QByteArray QQuick3DInstanceList::getInstanceBuffer(int *instanceCount)
627{
628 if (m_dirty)
629 generateInstanceData();
630 if (instanceCount)
631 *instanceCount = m_instances.size();
632 return m_instanceData;
633}
634
635QQmlListProperty<QQuick3DInstanceListEntry> QQuick3DInstanceList::instances()
636{
637
638 return QQmlListProperty<QQuick3DInstanceListEntry>(this,
639 nullptr,
640 qmlAppendInstanceListEntry,
641 qmlInstanceListEntriesCount,
642 qmlInstanceListEntryAt,
643 qmlClearInstanceListEntries);
644}
645
646/*!
647 \qmlproperty int QtQuick3D::InstanceList::instanceCount
648 \since 6.3
649
650 This read-only property contains the number of instances in the list.
651*/
652
653int QQuick3DInstanceList::instanceCount() const
654{
655 return m_instances.size();
656}
657
658void QQuick3DInstanceList::onInstanceDestroyed(QObject *object)
659{
660 if (m_instances.removeAll(object))
661 handleInstanceChange();
662}
663
664void QQuick3DInstanceList::qmlAppendInstanceListEntry(QQmlListProperty<QQuick3DInstanceListEntry> *list, QQuick3DInstanceListEntry *instance)
665{
666 if (instance == nullptr)
667 return;
668 auto *self = static_cast<QQuick3DInstanceList *>(list->object);
669 self->m_instances.push_back(instance);
670
671 if (instance->parentItem() == nullptr)
672 instance->setParentItem(self);
673 connect(instance, &QQuick3DInstanceListEntry::changed, self, &QQuick3DInstanceList::handleInstanceChange);
674 connect(instance, &QObject::destroyed, self, &QQuick3DInstanceList::onInstanceDestroyed);
675 self->handleInstanceChange();
676}
677
678QQuick3DInstanceListEntry *QQuick3DInstanceList::qmlInstanceListEntryAt(QQmlListProperty<QQuick3DInstanceListEntry> *list, qsizetype index)
679{
680 auto *self = static_cast<QQuick3DInstanceList *>(list->object);
681 return self->m_instances.at(index);
682}
683
684qsizetype QQuick3DInstanceList::qmlInstanceListEntriesCount(QQmlListProperty<QQuick3DInstanceListEntry> *list)
685{
686 auto *self = static_cast<QQuick3DInstanceList *>(list->object);
687 return self->m_instances.size();
688}
689
690void QQuick3DInstanceList::qmlClearInstanceListEntries(QQmlListProperty<QQuick3DInstanceListEntry> *list)
691{
692 auto *self = static_cast<QQuick3DInstanceList *>(list->object);
693 for (auto *instance : std::as_const(self->m_instances)) {
694 disconnect(instance, &QObject::destroyed, self, &QQuick3DInstanceList::onInstanceDestroyed);
695 disconnect(instance, &QQuick3DInstanceListEntry::changed, self, &QQuick3DInstanceList::handleInstanceChange);
696 }
697 self->m_instances.clear();
698 self->handleInstanceChange();
699}
700
701void QQuick3DInstanceList::handleInstanceChange()
702{
703 m_dirty = true;
704 markDirty();
705 emit instanceCountChanged();
706}
707
708void QQuick3DInstanceList::generateInstanceData()
709{
710 m_dirty = false;
711 const int count = m_instances.size();
712
713 qsizetype tableSize = count * sizeof(InstanceTableEntry);
714 m_instanceData.resize(tableSize);
715 auto *array = reinterpret_cast<InstanceTableEntry*>(m_instanceData.data());
716 for (int i = 0; i < count; ++i) {
717 const auto *inst = m_instances.at(i);
718 if (inst->m_useEulerRotation)
719 array[i] = calculateTableEntry(inst->position(), inst->scale(), inst->eulerRotation(), inst->color(), inst->customData());
720 else
721 array[i] = calculateTableEntryFromQuaternion(inst->position(), inst->scale(), inst->rotation(), inst->color(), inst->customData());
722 }
723}
724
725/*!
726 \qmltype InstanceListEntry
727 \inherits Object3D
728 \inqmlmodule QtQuick3D
729 \since 6.2
730 \brief Specifies an instance in an InstanceList.
731
732 The InstanceListEntry QML type is used to specify one instance in an instance list.
733
734 All the properties can have bindings and animation. Changing a property will cause the entire
735 instance table to be recalculated and uploaded to the GPU, so this can be expensive for instance
736 lists with many members.
737*/
738
739QQuick3DInstanceListEntry::QQuick3DInstanceListEntry(QQuick3DObject *parent)
740 : QQuick3DObject(parent)
741{
742}
743
744/*!
745 \qmlproperty vector3d QtQuick3D::InstanceListEntry::position
746
747 This property specifies the position for the instance.
748*/
749void QQuick3DInstanceListEntry::setPosition(QVector3D position)
750{
751 if (m_position == position)
752 return;
753
754 m_position = position;
755 emit positionChanged();
756 emit changed();
757}
758
759/*!
760 \qmlproperty vector3d QtQuick3D::InstanceListEntry::scale
761
762 This property specifies the scale for the instance as a vector containing the scale factor along the x, y and z axes.
763*/
764void QQuick3DInstanceListEntry::setScale(QVector3D scale)
765{
766 if (m_scale == scale)
767 return;
768
769 m_scale = scale;
770 emit scaleChanged();
771 emit changed();
772}
773
774/*!
775 \qmlproperty vector3d QtQuick3D::InstanceListEntry::eulerRotation
776
777 This property specifies the rotation for the instance as an Euler vector, that
778 is a vector containing the rotation in degrees around the x, y and z axes.
779*/
780void QQuick3DInstanceListEntry::setEulerRotation(QVector3D eulerRotation)
781{
782 if (m_useEulerRotation && m_eulerRotation == eulerRotation)
783 return;
784 m_eulerRotation = eulerRotation;
785 m_useEulerRotation = true;
786 emit eulerRotationChanged();
787 emit changed();
788}
789
790/*!
791 \qmlproperty quaternion QtQuick3D::InstanceListEntry::rotation
792
793 This property specifies the rotation for the instance as a quaternion.
794*/
795void QQuick3DInstanceListEntry::setRotation(QQuaternion rotation)
796{
797 if (!m_useEulerRotation && m_rotation == rotation)
798 return;
799
800 m_rotation = rotation;
801 m_useEulerRotation = false;
802 emit rotationChanged();
803 emit changed();
804}
805
806/*!
807 \qmlproperty color QtQuick3D::InstanceListEntry::color
808
809 This property specifies the color for the instance.
810*/
811void QQuick3DInstanceListEntry::setColor(QColor color)
812{
813 if (m_color == color)
814 return;
815
816 m_color = color;
817 emit colorChanged();
818 emit changed();
819}
820
821/*!
822 \qmlproperty vector4d QtQuick3D::InstanceListEntry::customData
823
824 This property specifies the custom data for the instance. This is not used by default,
825 but is made available to the vertex shader of custom materials as \c INSTANCE_DATA.
826*/
827void QQuick3DInstanceListEntry::setCustomData(QVector4D customData)
828{
829 if (m_customData == customData)
830 return;
831
832 m_customData = customData;
833 emit customDataChanged();
834 emit changed();
835}
836
837/*!
838 \qmltype FileInstancing
839 \inherits Instancing
840 \inqmlmodule QtQuick3D
841 \since 6.2
842 \brief Allows reading instance tables from file.
843
844 The FileInstancing type makes it possible to read instance tables from files.
845
846 There are two supported file formats: XML, and a Qt-specific binary format. The
847 binary file format uses the same layout as the table that is uploaded to the GPU,
848 so it can be directly mapped to memory. The \l{Instancer Tool}{instancer} tool converts
849 from XML to the binary format.
850
851 This is an example of the XML file format:
852 \badcode
853 <?xml version="1.0" encoding="UTF-8" ?>
854 <InstanceTable>
855 <Instance position="0 200 0" scale="0.75 0.75 0.75" custom="20 20" color="#ffcf7f"/>
856 <Instance position="0 -100 0" scale="0.5 0.5 0.5" color="red"/>
857 <Instance position="0 -200 0" eulerRotation="0 0 60" color="darkred" custom="10 40 0 0"/>
858 </InstanceTable>
859 \endcode
860
861 In order to be valid, the XML file must have a top-level \c{InstanceTable} element. Each
862 instance is represented by an \c{Instance} element inside the \c{InstanceTable}. Unknown
863 elements are silently ignored.
864
865 An \c{Instance} element can have a number of attributes. \c{color} attributes are specified by the normal Qt
866 SVG color names, or by hexadecimal notation. \c{vector3d} and {vector4d} attributes are specified by
867 a string of space-separated numbers, where missing trailing numbers indicate zeroes. The following
868 attributes are supported:
869 \table
870 \header
871 \li name
872 \li type
873 \row
874 \li \c position
875 \li \c vector3d
876 \row
877 \li \c scale
878 \li \c vector3d
879 \row
880 \li \c eulerRotation
881 \li \c vector3d
882 \row
883 \li \c quaternion
884 \li \c vector4d
885 \row
886 \li \c custom
887 \li \c vector4d
888 \row
889 \li \c color
890 \li \c color
891 \endtable
892 Unknown attributes are silently ignored.
893*/
894
895/*!
896 \qmlproperty url QtQuick3D::FileInstancing::source
897
898 This property holds the location of an XML or binary file containing the instance data.
899
900 If the file name has a ".bin" extension, it is assumed to refer to a binary file.
901 Otherwise it is assumed to refer to an XML file. If an XML file \e{foo.xml} is specified, and
902 the file \e{foo.xml.bin} exists, the binary file \e{foo.xml.bin} will be loaded instead.
903*/
904
905/*!
906 \qmlproperty int QtQuick3D::FileInstancing::instanceCount
907 \since 6.3
908
909 This read-only property contains the number of instances in the instance table.
910*/
911
912static constexpr quint16 currentMajorVersion = 1;
913
923
924/*!
925 \internal
926 Writes \a instanceData, an array of \a instanceCount InstanceTableEntry
927 elements, to \a out in the binary instance table format that
928 FileInstancing reads.
929*/
930bool QQuick3DFileInstancing::writeInstanceTable(QIODevice *out, const QByteArray &instanceData, int instanceCount)
931{
932 QQuick3DInstancingBinaryFileHeader header;
933
934 header.offset = sizeof(header);
935 header.count = instanceCount;
936
937 if (instanceData.size() != qsizetype(header.stride) * instanceCount) {
938 qWarning() << "inconsistent data";
939 return false;
940 }
941
942 // Ignoring endianness: Assume we always create on little-endian, and then special-case reading if we need to.
943
944 out->write(reinterpret_cast<const char *>(&header), sizeof(header));
945 out->write(instanceData.constData(), instanceData.size());
946 return true;
947}
948
949
950bool QQuick3DFileInstancing::loadFromBinaryFile(const QString &filename)
951{
952 auto binaryFile = std::make_unique<QFile>(filename);
953 if (!binaryFile->open(QFile::ReadOnly))
954 return false;
955
956 constexpr auto headerSize = sizeof(QQuick3DInstancingBinaryFileHeader);
957 const quint64 fileSize = binaryFile->size();
958 if (fileSize < headerSize) {
959 qWarning() << "data file too small";
960 return false;
961 }
962 const char *data = reinterpret_cast<const char *>(binaryFile->map(0, fileSize));
963 const auto *header = reinterpret_cast<const QQuick3DInstancingBinaryFileHeader *>(data);
964
965 if (header->majorVersion > currentMajorVersion) {
966 qWarning() << "Version" << header->majorVersion << "is too new";
967 return false;
968 }
969
970 if (fileSize != headerSize + header->count * header->stride) {
971 qWarning() << "wrong data size";
972 return false;
973 }
974
975 delete m_dataFile;
976
977 // In order to use fromRawData safely, the file has to stay open so that the mmap stays valid
978 m_dataFile = binaryFile.release();
979
980 m_instanceData = QByteArray::fromRawData(data + header->offset, header->count * header->stride);
981 m_instanceCount = header->count;
982
983 return true;
984}
985
986bool QQuick3DFileInstancing::loadFromXmlFile(const QString &filename)
987{
988 QFile f(filename);
989 if (!f.open(QFile::ReadOnly))
990 return false;
991
992 bool valid = false;
993 QXmlStreamReader reader(&f);
994 int instances = 0;
995
996 //### Why is there not a QTextStream constructor that takes a QStringView
997 const auto toVector3D = [](const QStringView &str) {
998 float x, y, z;
999 QTextStream(str.toLocal8Bit()) >> x >> y >> z;
1000 return QVector3D { x, y, z };
1001 };
1002 const auto toVector4D = [](const QStringView &str) {
1003 float x, y, z, w;
1004 QTextStream(str.toLocal8Bit()) >> x >> y >> z >> w;
1005 return QVector4D { x, y, z, w };
1006 };
1007
1008 QByteArray instanceData;
1009
1010 while (reader.readNextStartElement()) {
1011
1012 if (reader.name() == QLatin1String("InstanceTable")) {
1013 valid = true;
1014 while (reader.readNextStartElement()) {
1015 if (reader.name() == QLatin1String("Instance")) {
1016 QColor color = Qt::white;
1017 QVector3D position;
1018 QVector3D eulerRotation;
1019 QQuaternion quaternion;
1020 bool useQuaternion = false;
1021 QVector4D custom;
1022 QVector3D scale { 1, 1, 1 };
1023 for (auto &attr : reader.attributes()) {
1024 if (attr.name() == QLatin1String("color")) {
1025 color = QColor::fromString(attr.value());
1026 } else if (attr.name() == QLatin1String("position")) {
1027 position = toVector3D(attr.value());
1028 } else if (attr.name() == QLatin1String("eulerRotation")) {
1029 eulerRotation = toVector3D(attr.value());
1030 } else if (attr.name() == QLatin1String("scale")) {
1031 scale = toVector3D(attr.value());
1032 } else if (attr.name() == QLatin1String("quaternion")) {
1033 quaternion = QQuaternion(toVector4D(attr.value()));
1034 useQuaternion = true;
1035 } else if (attr.name() == QLatin1String("custom")) {
1036 custom = toVector4D(attr.value());
1037 }
1038 }
1039 auto entry = useQuaternion ? calculateTableEntryFromQuaternion(position, scale, quaternion, color, custom)
1040 : calculateTableEntry(position, scale, eulerRotation, color, custom);
1041 instanceData.append(reinterpret_cast<const char *>(&entry), sizeof(entry));
1042 instances++;
1043 }
1044 reader.skipCurrentElement();
1045 }
1046 } else {
1047 reader.skipCurrentElement();
1048 }
1049 }
1050
1051 if (valid) {
1052 m_instanceCount = instances;
1053 m_instanceData = instanceData;
1054 }
1055
1056 f.close();
1057 return valid;
1058}
1059
1060int QQuick3DFileInstancing::writeToBinaryFile(QIODevice *out)
1061{
1062 bool success = writeInstanceTable(out, m_instanceData, m_instanceCount);
1063 return success ? m_instanceCount : -1;
1064}
1065
1066int QQuick3DFileInstancing::instanceCount() const
1067{
1068 return m_instanceCount;
1069}
1070
1071bool QQuick3DFileInstancing::loadFromFile(const QUrl &source)
1072{
1073 const QQmlContext *context = qmlContext(this);
1074
1075 const QString filePath = QQmlFile::urlToLocalFileOrQrc(context ? context->resolvedUrl(source) : source);
1076
1077 if (filePath.endsWith(QStringLiteral(".bin")))
1078 return loadFromBinaryFile(filePath);
1079
1080 const QString binaryFilePath = filePath + QStringLiteral(".bin");
1081
1082 int oldCount = m_instanceCount;
1083 bool success = loadFromBinaryFile(binaryFilePath) || loadFromXmlFile(filePath);
1084 if (m_instanceCount != oldCount)
1085 emit instanceCountChanged();
1086
1087 return success;
1088}
1089
1090QQuick3DFileInstancing::QQuick3DFileInstancing(QQuick3DObject *parent) : QQuick3DInstancing(parent) { }
1091
1092QQuick3DFileInstancing::~QQuick3DFileInstancing()
1093{
1094 delete m_dataFile;
1095}
1096
1097const QUrl &QQuick3DFileInstancing::source() const
1098{
1099 return m_source;
1100}
1101
1102void QQuick3DFileInstancing::setSource(const QUrl &newSource)
1103{
1104 if (m_source == newSource)
1105 return;
1106 m_source = newSource;
1107 m_dirty = true;
1108 markDirty();
1109
1110 emit sourceChanged();
1111}
1112
1113QByteArray QQuick3DFileInstancing::getInstanceBuffer(int *instanceCount)
1114{
1115 if (m_dirty) {
1116 if (!loadFromFile(m_source)) {
1117 qWarning() << Q_FUNC_INFO << "could not load" << m_source;
1118 m_instanceData = {};
1119 m_instanceCount = 0;
1120 }
1121 m_dirty = false;
1122 }
1123
1124 if (instanceCount)
1125 *instanceCount = m_instanceCount;
1126 return m_instanceData;
1127}
1128
1129static_assert(sizeof(QQuick3DInstancing::InstanceTableEntry) == sizeof(QSSGRenderInstanceTableEntry)
1130 && alignof(QQuick3DInstancing::InstanceTableEntry) == alignof(QSSGRenderInstanceTableEntry),
1131 "QSSGRenderInstanceTableEntry and QQuick3DInstancing::InstanceTableEntry do not match");
1132
1133QT_END_NAMESPACE
static constexpr quint16 currentMajorVersion
\qmltype FileInstancing \inherits Instancing \inqmlmodule QtQuick3D
static QQuick3DInstancing::InstanceTableEntry calculate(const QVector3D &position, const QVector3D &scale, const QVector3D &eulerRotation, const QColor &color, const QVector4D &customData)