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 : 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
924static bool writeInstanceTable(QIODevice *out, const QByteArray &instanceData, int instanceCount)
925{
927
928 header.offset = sizeof(header);
929 header.count = instanceCount;
930
931 if (instanceData.size() != qsizetype(header.stride) * instanceCount) {
932 qWarning() << "inconsistent data";
933 return false;
934 }
935
936 // Ignoring endianness: Assume we always create on little-endian, and then special-case reading if we need to.
937
938 out->write(reinterpret_cast<const char *>(&header), sizeof(header));
939 out->write(instanceData.constData(), instanceData.size());
940 return true;
941}
942
943
944bool QQuick3DFileInstancing::loadFromBinaryFile(const QString &filename)
945{
946 auto binaryFile = std::make_unique<QFile>(filename);
947 if (!binaryFile->open(QFile::ReadOnly))
948 return false;
949
950 constexpr auto headerSize = sizeof(QQuick3DInstancingBinaryFileHeader);
951 const quint64 fileSize = binaryFile->size();
952 if (fileSize < headerSize) {
953 qWarning() << "data file too small";
954 return false;
955 }
956 const char *data = reinterpret_cast<const char *>(binaryFile->map(0, fileSize));
957 const auto *header = reinterpret_cast<const QQuick3DInstancingBinaryFileHeader *>(data);
958
959 if (header->majorVersion > currentMajorVersion) {
960 qWarning() << "Version" << header->majorVersion << "is too new";
961 return false;
962 }
963
964 if (fileSize != headerSize + header->count * header->stride) {
965 qWarning() << "wrong data size";
966 return false;
967 }
968
969 delete m_dataFile;
970
971 // In order to use fromRawData safely, the file has to stay open so that the mmap stays valid
972 m_dataFile = binaryFile.release();
973
974 m_instanceData = QByteArray::fromRawData(data + header->offset, header->count * header->stride);
975 m_instanceCount = header->count;
976
977 return true;
978}
979
980bool QQuick3DFileInstancing::loadFromXmlFile(const QString &filename)
981{
982 QFile f(filename);
983 if (!f.open(QFile::ReadOnly))
984 return false;
985
986 bool valid = false;
987 QXmlStreamReader reader(&f);
988 int instances = 0;
989
990 //### Why is there not a QTextStream constructor that takes a QStringView
991 const auto toVector3D = [](const QStringView &str) {
992 float x, y, z;
993 QTextStream(str.toLocal8Bit()) >> x >> y >> z;
994 return QVector3D { x, y, z };
995 };
996 const auto toVector4D = [](const QStringView &str) {
997 float x, y, z, w;
998 QTextStream(str.toLocal8Bit()) >> x >> y >> z >> w;
999 return QVector4D { x, y, z, w };
1000 };
1001
1002 QByteArray instanceData;
1003
1004 while (reader.readNextStartElement()) {
1005
1006 if (reader.name() == QLatin1String("InstanceTable")) {
1007 valid = true;
1008 while (reader.readNextStartElement()) {
1009 if (reader.name() == QLatin1String("Instance")) {
1010 QColor color = Qt::white;
1011 QVector3D position;
1012 QVector3D eulerRotation;
1013 QQuaternion quaternion;
1014 bool useQuaternion = false;
1015 QVector4D custom;
1016 QVector3D scale { 1, 1, 1 };
1017 for (auto &attr : reader.attributes()) {
1018 if (attr.name() == QLatin1String("color")) {
1019 color = QColor::fromString(attr.value());
1020 } else if (attr.name() == QLatin1String("position")) {
1021 position = toVector3D(attr.value());
1022 } else if (attr.name() == QLatin1String("eulerRotation")) {
1023 eulerRotation = toVector3D(attr.value());
1024 } else if (attr.name() == QLatin1String("scale")) {
1025 scale = toVector3D(attr.value());
1026 } else if (attr.name() == QLatin1String("quaternion")) {
1027 quaternion = QQuaternion(toVector4D(attr.value()));
1028 useQuaternion = true;
1029 } else if (attr.name() == QLatin1String("custom")) {
1030 custom = toVector4D(attr.value());
1031 }
1032 }
1033 auto entry = useQuaternion ? calculateTableEntryFromQuaternion(position, scale, quaternion, color, custom)
1034 : calculateTableEntry(position, scale, eulerRotation, color, custom);
1035 instanceData.append(reinterpret_cast<const char *>(&entry), sizeof(entry));
1036 instances++;
1037 }
1038 reader.skipCurrentElement();
1039 }
1040 } else {
1041 reader.skipCurrentElement();
1042 }
1043 }
1044
1045 if (valid) {
1046 m_instanceCount = instances;
1047 m_instanceData = instanceData;
1048 }
1049
1050 f.close();
1051 return valid;
1052}
1053
1054int QQuick3DFileInstancing::writeToBinaryFile(QIODevice *out)
1055{
1056 bool success = writeInstanceTable(out, m_instanceData, m_instanceCount);
1057 return success ? m_instanceCount : -1;
1058}
1059
1060int QQuick3DFileInstancing::instanceCount() const
1061{
1062 return m_instanceCount;
1063}
1064
1065bool QQuick3DFileInstancing::loadFromFile(const QUrl &source)
1066{
1067 const QQmlContext *context = qmlContext(this);
1068
1069 const QString filePath = QQmlFile::urlToLocalFileOrQrc(context ? context->resolvedUrl(source) : source);
1070
1071 if (filePath.endsWith(QStringLiteral(".bin")))
1072 return loadFromBinaryFile(filePath);
1073
1074 const QString binaryFilePath = filePath + QStringLiteral(".bin");
1075
1076 int oldCount = m_instanceCount;
1077 bool success = loadFromBinaryFile(binaryFilePath) || loadFromXmlFile(filePath);
1078 if (m_instanceCount != oldCount)
1079 emit instanceCountChanged();
1080
1081 return success;
1082}
1083
1084QQuick3DFileInstancing::QQuick3DFileInstancing(QQuick3DObject *parent) : QQuick3DInstancing(parent) { }
1085
1086QQuick3DFileInstancing::~QQuick3DFileInstancing()
1087{
1088 delete m_dataFile;
1089}
1090
1091const QUrl &QQuick3DFileInstancing::source() const
1092{
1093 return m_source;
1094}
1095
1096void QQuick3DFileInstancing::setSource(const QUrl &newSource)
1097{
1098 if (m_source == newSource)
1099 return;
1100 m_source = newSource;
1101 m_dirty = true;
1102 markDirty();
1103
1104 emit sourceChanged();
1105}
1106
1107QByteArray QQuick3DFileInstancing::getInstanceBuffer(int *instanceCount)
1108{
1109 if (m_dirty) {
1110 if (!loadFromFile(m_source)) {
1111 qWarning() << Q_FUNC_INFO << "could not load" << m_source;
1112 m_instanceData = {};
1113 m_instanceCount = 0;
1114 }
1115 m_dirty = false;
1116 }
1117
1118 if (instanceCount)
1119 *instanceCount = m_instanceCount;
1120 return m_instanceData;
1121}
1122
1123static_assert(sizeof(QQuick3DInstancing::InstanceTableEntry) == sizeof(QSSGRenderInstanceTableEntry)
1124 && alignof(QQuick3DInstancing::InstanceTableEntry) == alignof(QSSGRenderInstanceTableEntry),
1125 "QSSGRenderInstanceTableEntry and QQuick3DInstancing::InstanceTableEntry do not match");
1126
1127QT_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)
static bool writeInstanceTable(QIODevice *out, const QByteArray &instanceData, int instanceCount)