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

source code of qtquick3d/src/quick3d/qquick3dinstancing.cpp