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 \nativetype 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 It is also possible to populate the instances property by just adding children to the
493 InstanceList. The following example is equivalent to the previous one:
494 \qml
495 InstanceList {
496 id: manualInstancing
497 InstanceListEntry {
498 position: Qt.vector3d(0, 0, -60)
499 eulerRotation: Qt.vector3d(-10, 0, 30)
500 color: "red"
501 }
502 InstanceListEntry {
503 position: Qt.vector3d(50, 10, 100)
504 eulerRotation: Qt.vector3d(0, 180, 0)
505 color: "green"
506 }
507 }
508 \endqml
509
510 Each InstanceListEntry is an object that can have property bindings and animations. This gives
511 great flexibility, but also causes memory overhead. Therefore, it is not recommended to use
512 InstanceList for procedurally generated tables containing thousands (or millions) of
513 instances. Also, any property change to an entry will cause the entire instance table to be
514 recalculated and uploaded to the GPU.
515
516 \sa RandomInstancing, QQuick3DInstancing
517*/
518
519/*!
520 \qmlproperty List<QtQuick3D::InstanceListEntry> InstanceList::instances
521 \qmldefault
522
523 This property contains the list of instance definitions. Modifying this list, or any of its elements, will cause the instance table to be updated.
524*/
525
526QQuick3DInstanceList::QQuick3DInstanceList(QQuick3DObject *parent) : QQuick3DInstancing(parent) {}
527
528QQuick3DInstanceList::~QQuick3DInstanceList() {}
529
530QByteArray QQuick3DInstanceList::getInstanceBuffer(int *instanceCount)
531{
532 if (m_dirty)
533 generateInstanceData();
534 if (instanceCount)
535 *instanceCount = m_instances.size();
536 return m_instanceData;
537}
538
539QQmlListProperty<QQuick3DInstanceListEntry> QQuick3DInstanceList::instances()
540{
541
542 return QQmlListProperty<QQuick3DInstanceListEntry>(this,
543 nullptr,
544 qmlAppendInstanceListEntry,
545 qmlInstanceListEntriesCount,
546 qmlInstanceListEntryAt,
547 qmlClearInstanceListEntries);
548}
549
550/*!
551 \qmlproperty int QtQuick3D::InstanceList::instanceCount
552 \since 6.3
553
554 This read-only property contains the number of instances in the list.
555*/
556
557int QQuick3DInstanceList::instanceCount() const
558{
559 return m_instances.size();
560}
561
562void QQuick3DInstanceList::onInstanceDestroyed(QObject *object)
563{
564 if (m_instances.removeAll(t: object))
565 handleInstanceChange();
566}
567
568void QQuick3DInstanceList::qmlAppendInstanceListEntry(QQmlListProperty<QQuick3DInstanceListEntry> *list, QQuick3DInstanceListEntry *instance)
569{
570 if (instance == nullptr)
571 return;
572 auto *self = static_cast<QQuick3DInstanceList *>(list->object);
573 self->m_instances.push_back(t: instance);
574
575 if (instance->parentItem() == nullptr)
576 instance->setParentItem(self);
577 connect(sender: instance, signal: &QQuick3DInstanceListEntry::changed, context: self, slot: &QQuick3DInstanceList::handleInstanceChange);
578 connect(sender: instance, signal: &QObject::destroyed, context: self, slot: &QQuick3DInstanceList::onInstanceDestroyed);
579 self->handleInstanceChange();
580}
581
582QQuick3DInstanceListEntry *QQuick3DInstanceList::qmlInstanceListEntryAt(QQmlListProperty<QQuick3DInstanceListEntry> *list, qsizetype index)
583{
584 auto *self = static_cast<QQuick3DInstanceList *>(list->object);
585 return self->m_instances.at(i: index);
586}
587
588qsizetype QQuick3DInstanceList::qmlInstanceListEntriesCount(QQmlListProperty<QQuick3DInstanceListEntry> *list)
589{
590 auto *self = static_cast<QQuick3DInstanceList *>(list->object);
591 return self->m_instances.size();
592}
593
594void QQuick3DInstanceList::qmlClearInstanceListEntries(QQmlListProperty<QQuick3DInstanceListEntry> *list)
595{
596 auto *self = static_cast<QQuick3DInstanceList *>(list->object);
597 for (auto *instance : self->m_instances) {
598 disconnect(sender: instance, signal: &QObject::destroyed, receiver: self, slot: &QQuick3DInstanceList::onInstanceDestroyed);
599 disconnect(sender: instance, signal: &QQuick3DInstanceListEntry::changed, receiver: self, slot: &QQuick3DInstanceList::handleInstanceChange);
600 }
601 self->m_instances.clear();
602 self->handleInstanceChange();
603}
604
605void QQuick3DInstanceList::handleInstanceChange()
606{
607 m_dirty = true;
608 markDirty();
609 emit instanceCountChanged();
610}
611
612void QQuick3DInstanceList::generateInstanceData()
613{
614 m_dirty = false;
615 const int count = m_instances.size();
616
617 qsizetype tableSize = count * sizeof(InstanceTableEntry);
618 m_instanceData.resize(size: tableSize);
619 auto *array = reinterpret_cast<InstanceTableEntry*>(m_instanceData.data());
620 for (int i = 0; i < count; ++i) {
621 const auto *inst = m_instances.at(i);
622 if (inst->m_useEulerRotation)
623 array[i] = calculateTableEntry(position: inst->position(), scale: inst->scale(), eulerRotation: inst->eulerRotation(), color: inst->color(), customData: inst->customData());
624 else
625 array[i] = calculateTableEntryFromQuaternion(position: inst->position(), scale: inst->scale(), rotation: inst->rotation(), color: inst->color(), customData: inst->customData());
626 }
627}
628
629/*!
630 \qmltype InstanceListEntry
631 \inherits Object3D
632 \inqmlmodule QtQuick3D
633 \since 6.2
634 \brief Specifies an instance in an InstanceList.
635
636 The InstanceListEntry QML type is used to specify one instance in an instance list.
637
638 All the properties can have bindings and animation. Changing a property will cause the entire
639 instance table to be recalculated and uploaded to the GPU, so this can be expensive for instance
640 lists with many members.
641*/
642
643QQuick3DInstanceListEntry::QQuick3DInstanceListEntry(QQuick3DObject *parent)
644 : QQuick3DObject(parent)
645{
646}
647
648/*!
649 \qmlproperty vector3d QtQuick3D::InstanceListEntry::position
650
651 This property specifies the position for the instance.
652*/
653void QQuick3DInstanceListEntry::setPosition(QVector3D position)
654{
655 if (m_position == position)
656 return;
657
658 m_position = position;
659 emit positionChanged();
660 emit changed();
661}
662
663/*!
664 \qmlproperty vector3d QtQuick3D::InstanceListEntry::scale
665
666 This property specifies the scale for the instance as a vector containing the scale factor along the x, y and z axes.
667*/
668void QQuick3DInstanceListEntry::setScale(QVector3D scale)
669{
670 if (m_scale == scale)
671 return;
672
673 m_scale = scale;
674 emit scaleChanged();
675 emit changed();
676}
677
678/*!
679 \qmlproperty vector3d QtQuick3D::InstanceListEntry::eulerRotation
680
681 This property specifies the rotation for the instance as an Euler vector, that
682 is a vector containing the rotation in degrees around the x, y and z axes.
683*/
684void QQuick3DInstanceListEntry::setEulerRotation(QVector3D eulerRotation)
685{
686 if (m_useEulerRotation && m_eulerRotation == eulerRotation)
687 return;
688 m_eulerRotation = eulerRotation;
689 m_useEulerRotation = true;
690 emit eulerRotationChanged();
691 emit changed();
692}
693
694/*!
695 \qmlproperty quaternion QtQuick3D::InstanceListEntry::rotation
696
697 This property specifies the rotation for the instance as a quaternion.
698*/
699void QQuick3DInstanceListEntry::setRotation(QQuaternion rotation)
700{
701 if (!m_useEulerRotation && m_rotation == rotation)
702 return;
703
704 m_rotation = rotation;
705 m_useEulerRotation = false;
706 emit rotationChanged();
707 emit changed();
708}
709
710/*!
711 \qmlproperty color QtQuick3D::InstanceListEntry::color
712
713 This property specifies the color for the instance.
714*/
715void QQuick3DInstanceListEntry::setColor(QColor color)
716{
717 if (m_color == color)
718 return;
719
720 m_color = color;
721 emit colorChanged();
722 emit changed();
723}
724
725/*!
726 \qmlproperty vector4d QtQuick3D::InstanceListEntry::customData
727
728 This property specifies the custom data for the instance. This is not used by default,
729 but is made available to the vertex shader of custom materials as \c INSTANCE_DATA.
730*/
731void QQuick3DInstanceListEntry::setCustomData(QVector4D customData)
732{
733 if (m_customData == customData)
734 return;
735
736 m_customData = customData;
737 emit customDataChanged();
738 emit changed();
739}
740
741/*!
742 \qmltype FileInstancing
743 \inherits Instancing
744 \inqmlmodule QtQuick3D
745 \since 6.2
746 \brief Allows reading instance tables from file.
747
748 The FileInstancing type makes it possible to read instance tables from files.
749
750 There are two supported file formats: XML, and a Qt-specific binary format. The
751 binary file format uses the same layout as the table that is uploaded to the GPU,
752 so it can be directly mapped to memory. The \l{Instancer Tool}{instancer} tool converts
753 from XML to the binary format.
754
755 This is an example of the XML file format:
756 \badcode
757 <?xml version="1.0" encoding="UTF-8" ?>
758 <InstanceTable>
759 <Instance position="0 200 0" scale="0.75 0.75 0.75" custom="20 20" color="#ffcf7f"/>
760 <Instance position="0 -100 0" scale="0.5 0.5 0.5" color="red"/>
761 <Instance position="0 -200 0" eulerRotation="0 0 60" color="darkred" custom="10 40 0 0"/>
762 </InstanceTable>
763 \endcode
764
765 In order to be valid, the XML file must have a top-level \c{InstanceTable} element. Each
766 instance is represented by an \c{Instance} element inside the \c{InstanceTable}. Unknown
767 elements are silently ignored.
768
769 An \c{Instance} element can have a number of attributes. \c{color} attributes are specified by the normal Qt
770 SVG color names, or by hexadecimal notation. \c{vector3d} and {vector4d} attributes are specified by
771 a string of space-separated numbers, where missing trailing numbers indicate zeroes. The following
772 attributes are supported:
773 \table
774 \header
775 \li name
776 \li type
777 \row
778 \li \c position
779 \li \c vector3d
780 \row
781 \li \c scale
782 \li \c vector3d
783 \row
784 \li \c eulerRotation
785 \li \c vector3d
786 \row
787 \li \c quaternion
788 \li \c vector4d
789 \row
790 \li \c custom
791 \li \c vector4d
792 \row
793 \li \c color
794 \li \c color
795 \endtable
796 Unknown attributes are silently ignored.
797*/
798
799/*!
800 \qmlproperty url QtQuick3D::FileInstancing::source
801
802 This property holds the location of an XML or binary file containing the instance data.
803
804 If the file name has a ".bin" extension, it is assumed to refer to a binary file.
805 Otherwise it is assumed to refer to an XML file. If an XML file \e{foo.xml} is specified, and
806 the file \e{foo.xml.bin} exists, the binary file \e{foo.xml.bin} will be loaded instead.
807*/
808
809/*!
810 \qmlproperty int QtQuick3D::FileInstancing::instanceCount
811 \since 6.3
812
813 This read-only property contains the number of instances in the instance table.
814*/
815
816static constexpr quint16 currentMajorVersion = 1;
817
818struct QQuick3DInstancingBinaryFileHeader
819{
820 char magic[4] = { 'Q', 't', 'I', 'R' };
821 const quint16 majorVersion = currentMajorVersion;
822 const quint16 minorVersion = 0;
823 const quint32 stride = sizeof(QQuick3DInstancing::InstanceTableEntry);
824 quint32 offset;
825 quint32 count;
826};
827
828static bool writeInstanceTable(QIODevice *out, const QByteArray &instanceData, int instanceCount)
829{
830 QQuick3DInstancingBinaryFileHeader header;
831
832 header.offset = sizeof(header);
833 header.count = instanceCount;
834
835 if (instanceData.size() != qsizetype(header.stride) * instanceCount) {
836 qWarning() << "inconsistent data";
837 return false;
838 }
839
840 // Ignoring endianness: Assume we always create on little-endian, and then special-case reading if we need to.
841
842 out->write(data: reinterpret_cast<const char *>(&header), len: sizeof(header));
843 out->write(data: instanceData.constData(), len: instanceData.size());
844 return true;
845}
846
847
848bool QQuick3DFileInstancing::loadFromBinaryFile(const QString &filename)
849{
850 auto binaryFile = std::make_unique<QFile>(args: filename);
851 if (!binaryFile->open(flags: QFile::ReadOnly))
852 return false;
853
854 constexpr auto headerSize = sizeof(QQuick3DInstancingBinaryFileHeader);
855 const quint64 fileSize = binaryFile->size();
856 if (fileSize < headerSize) {
857 qWarning() << "data file too small";
858 return false;
859 }
860 const char *data = reinterpret_cast<const char *>(binaryFile->map(offset: 0, size: fileSize));
861 const auto *header = reinterpret_cast<const QQuick3DInstancingBinaryFileHeader *>(data);
862
863 if (header->majorVersion > currentMajorVersion) {
864 qWarning() << "Version" << header->majorVersion << "is too new";
865 return false;
866 }
867
868 if (fileSize != headerSize + header->count * header->stride) {
869 qWarning() << "wrong data size";
870 return false;
871 }
872
873 delete m_dataFile;
874
875 // In order to use fromRawData safely, the file has to stay open so that the mmap stays valid
876 m_dataFile = binaryFile.release();
877
878 m_instanceData = QByteArray::fromRawData(data: data + header->offset, size: header->count * header->stride);
879 m_instanceCount = header->count;
880
881 return true;
882}
883
884bool QQuick3DFileInstancing::loadFromXmlFile(const QString &filename)
885{
886 QFile f(filename);
887 if (!f.open(flags: QFile::ReadOnly))
888 return false;
889
890 bool valid = false;
891 QXmlStreamReader reader(&f);
892 int instances = 0;
893
894 //### Why is there not a QTextStream constructor that takes a QStringView
895 const auto toVector3D = [](const QStringView &str) {
896 float x, y, z;
897 QTextStream(str.toLocal8Bit()) >> x >> y >> z;
898 return QVector3D { x, y, z };
899 };
900 const auto toVector4D = [](const QStringView &str) {
901 float x, y, z, w;
902 QTextStream(str.toLocal8Bit()) >> x >> y >> z >> w;
903 return QVector4D { x, y, z, w };
904 };
905
906 QByteArray instanceData;
907
908 while (reader.readNextStartElement()) {
909
910 if (reader.name() == QLatin1String("InstanceTable")) {
911 valid = true;
912 while (reader.readNextStartElement()) {
913 if (reader.name() == QLatin1String("Instance")) {
914 QColor color = Qt::white;
915 QVector3D position;
916 QVector3D eulerRotation;
917 QQuaternion quaternion;
918 bool useQuaternion = false;
919 QVector4D custom;
920 QVector3D scale { 1, 1, 1 };
921 for (auto &attr : reader.attributes()) {
922 if (attr.name() == QLatin1String("color")) {
923 color = QColor::fromString(name: attr.value());
924 } else if (attr.name() == QLatin1String("position")) {
925 position = toVector3D(attr.value());
926 } else if (attr.name() == QLatin1String("eulerRotation")) {
927 eulerRotation = toVector3D(attr.value());
928 } else if (attr.name() == QLatin1String("scale")) {
929 scale = toVector3D(attr.value());
930 } else if (attr.name() == QLatin1String("quaternion")) {
931 quaternion = QQuaternion(toVector4D(attr.value()));
932 useQuaternion = true;
933 } else if (attr.name() == QLatin1String("custom")) {
934 custom = toVector4D(attr.value());
935 }
936 }
937 auto entry = useQuaternion ? calculateTableEntryFromQuaternion(position, scale, rotation: quaternion, color, customData: custom)
938 : calculateTableEntry(position, scale, eulerRotation, color, customData: custom);
939 instanceData.append(s: reinterpret_cast<const char *>(&entry), len: sizeof(entry));
940 instances++;
941 }
942 reader.skipCurrentElement();
943 }
944 } else {
945 reader.skipCurrentElement();
946 }
947 }
948
949 if (valid) {
950 m_instanceCount = instances;
951 m_instanceData = instanceData;
952 }
953
954 f.close();
955 return valid;
956}
957
958int QQuick3DFileInstancing::writeToBinaryFile(QIODevice *out)
959{
960 bool success = writeInstanceTable(out, instanceData: m_instanceData, instanceCount: m_instanceCount);
961 return success ? m_instanceCount : -1;
962}
963
964int QQuick3DFileInstancing::instanceCount() const
965{
966 return m_instanceCount;
967}
968
969bool QQuick3DFileInstancing::loadFromFile(const QUrl &source)
970{
971 const QQmlContext *context = qmlContext(this);
972
973 const QString filePath = QQmlFile::urlToLocalFileOrQrc(context ? context->resolvedUrl(source) : source);
974
975 if (filePath.endsWith(QStringLiteral(".bin")))
976 return loadFromBinaryFile(filename: filePath);
977
978 const QString binaryFilePath = filePath + QStringLiteral(".bin");
979
980 int oldCount = m_instanceCount;
981 bool success = loadFromBinaryFile(filename: binaryFilePath) || loadFromXmlFile(filename: filePath);
982 if (m_instanceCount != oldCount)
983 emit instanceCountChanged();
984
985 return success;
986}
987
988QQuick3DFileInstancing::QQuick3DFileInstancing(QQuick3DObject *parent) : QQuick3DInstancing(parent) { }
989
990QQuick3DFileInstancing::~QQuick3DFileInstancing()
991{
992 delete m_dataFile;
993}
994
995const QUrl &QQuick3DFileInstancing::source() const
996{
997 return m_source;
998}
999
1000void QQuick3DFileInstancing::setSource(const QUrl &newSource)
1001{
1002 if (m_source == newSource)
1003 return;
1004 m_source = newSource;
1005 m_dirty = true;
1006 markDirty();
1007
1008 emit sourceChanged();
1009}
1010
1011QByteArray QQuick3DFileInstancing::getInstanceBuffer(int *instanceCount)
1012{
1013 if (m_dirty) {
1014 if (!loadFromFile(source: m_source)) {
1015 qWarning() << Q_FUNC_INFO << "could not load" << m_source;
1016 m_instanceData = {};
1017 m_instanceCount = 0;
1018 }
1019 m_dirty = false;
1020 }
1021
1022 if (instanceCount)
1023 *instanceCount = m_instanceCount;
1024 return m_instanceData;
1025}
1026
1027static_assert(sizeof(QQuick3DInstancing::InstanceTableEntry) == sizeof(QSSGRenderInstanceTableEntry)
1028 && alignof(QQuick3DInstancing::InstanceTableEntry) == alignof(QSSGRenderInstanceTableEntry),
1029 "QSSGRenderInstanceTableEntry and QQuick3DInstancing::InstanceTableEntry do not match");
1030
1031QT_END_NAMESPACE
1032

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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