1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qquick3dparticlemodelparticle_p.h" |
5 | |
6 | QT_BEGIN_NAMESPACE |
7 | |
8 | /*! |
9 | \qmltype ModelParticle3D |
10 | \inherits Particle3D |
11 | \inqmlmodule QtQuick3D.Particles3D |
12 | \brief Particle using a Qt Quick 3D Model. |
13 | \since 6.2 |
14 | |
15 | The ModelParticle3D is a logical particle element that creates particles |
16 | from a Qt Quick 3D \l Model component. |
17 | */ |
18 | |
19 | QQuick3DParticleModelParticle::QQuick3DParticleModelParticle(QQuick3DNode *parent) |
20 | : QQuick3DParticle(parent) |
21 | , m_initialScale(1.0f, 1.0f, 1.0f) |
22 | { |
23 | QObject::connect(sender: this, signal: &QQuick3DParticle::maxAmountChanged, slot: [this]() { |
24 | handleMaxAmountChanged(amount: m_maxAmount); |
25 | }); |
26 | QObject::connect(sender: this, signal: &QQuick3DParticle::sortModeChanged, slot: [this]() { |
27 | handleSortModeChanged(mode: sortMode()); |
28 | }); |
29 | } |
30 | |
31 | void QQuick3DParticleModelParticle::handleMaxAmountChanged(int amount) |
32 | { |
33 | if (m_particleData.size() == amount) |
34 | return; |
35 | |
36 | m_particleData.resize(size: amount); |
37 | m_particleData.fill(t: {}); |
38 | } |
39 | |
40 | /*! |
41 | \qmlproperty Component ModelParticle3D::delegate |
42 | |
43 | The delegate provides a template defining each object instantiated by the particle. |
44 | |
45 | For example, to allocate 200 red cube particles: |
46 | |
47 | \qml |
48 | Component { |
49 | id: particleComponent |
50 | Model { |
51 | source: "#Cube" |
52 | scale: Qt.vector3d(0.2, 0.2, 0.2) |
53 | materials: DefaultMaterial { } |
54 | } |
55 | } |
56 | |
57 | ModelParticle3D { |
58 | id: particleRed |
59 | delegate: particleComponent |
60 | maxAmount: 200 |
61 | color: "#ff0000" |
62 | } |
63 | \endqml |
64 | */ |
65 | QQmlComponent *QQuick3DParticleModelParticle::delegate() const |
66 | { |
67 | return m_delegate.data(); |
68 | } |
69 | |
70 | void QQuick3DParticleModelParticle::setDelegate(QQmlComponent *delegate) |
71 | { |
72 | if (delegate == m_delegate) |
73 | return; |
74 | m_delegate = delegate; |
75 | |
76 | regenerate(); |
77 | Q_EMIT delegateChanged(); |
78 | } |
79 | |
80 | class QQuick3DParticleInstanceTable : public QQuick3DInstancing |
81 | { |
82 | struct SortData |
83 | { |
84 | float age; |
85 | int index; |
86 | }; |
87 | |
88 | public: |
89 | QQuick3DParticleInstanceTable() {} |
90 | void clear() { m_instances.clear(); m_sortData.clear(); } |
91 | void commit() { sort(); markDirty(); } |
92 | void addInstance(const QVector3D &position, |
93 | const QVector3D &scale, |
94 | const QVector3D &eulerRotation, |
95 | const QColor &color, |
96 | float age) { |
97 | auto entry = calculateTableEntry(position, scale, eulerRotation, color); |
98 | m_instances.append(s: reinterpret_cast<char *>(&entry), len: sizeof(InstanceTableEntry)); |
99 | if (m_ageSorting) |
100 | m_sortData.append(t: {.age: age, .index: int(m_instances.size() / sizeof(InstanceTableEntry))}); |
101 | } |
102 | void setSorting(bool enable, bool inverted = false) |
103 | { |
104 | m_ageSorting = enable; |
105 | m_inverted = inverted; |
106 | } |
107 | protected: |
108 | QByteArray getInstanceBuffer(int *instanceCount) override |
109 | { |
110 | if (instanceCount) |
111 | *instanceCount = int(m_instances.size() / sizeof(InstanceTableEntry)); |
112 | |
113 | if (!m_ageSorting) |
114 | return m_instances; |
115 | return m_sortedInstances; |
116 | } |
117 | void sort() |
118 | { |
119 | if (!m_ageSorting) |
120 | return; |
121 | |
122 | if (m_inverted) { |
123 | std::sort(first: m_sortData.begin(), last: m_sortData.end(), comp: [&](const SortData &a, const SortData &b) { |
124 | return a.age < b.age; |
125 | }); |
126 | } else { |
127 | std::sort(first: m_sortData.begin(), last: m_sortData.end(), comp: [&](const SortData &a, const SortData &b) { |
128 | return a.age > b.age; |
129 | }); |
130 | } |
131 | m_sortedInstances.resize(size: m_instances.size()); |
132 | const InstanceTableEntry *src = reinterpret_cast<InstanceTableEntry *>(m_instances.data()); |
133 | InstanceTableEntry *dst = reinterpret_cast<InstanceTableEntry *>(m_sortedInstances.data()); |
134 | for (auto &e : m_sortData) |
135 | *dst++ = src[e.index]; |
136 | } |
137 | |
138 | private: |
139 | QList<SortData> m_sortData; |
140 | QByteArray m_instances; |
141 | QByteArray m_sortedInstances; |
142 | bool m_ageSorting = false; |
143 | bool m_inverted = false; |
144 | }; |
145 | |
146 | void QQuick3DParticleModelParticle::handleSortModeChanged(QQuick3DParticle::SortMode mode) |
147 | { |
148 | if (m_instanceTable) { |
149 | if (mode == QQuick3DParticle::SortNewest || mode == QQuick3DParticle::SortOldest) |
150 | m_instanceTable->setSorting(enable: true, inverted: mode == QQuick3DParticle::SortNewest); |
151 | else |
152 | m_instanceTable->setSorting(enable: false); |
153 | m_instanceTable->setDepthSortingEnabled(mode == QQuick3DParticle::SortDistance); |
154 | } |
155 | } |
156 | |
157 | /*! |
158 | \qmlproperty Instancing ModelParticle3D::instanceTable |
159 | |
160 | The instanceTable provides access to the \l Instancing table of the model particle. |
161 | ModelParticle3D uses an internal instance table to implement efficient rendering. |
162 | This table can be applied to the \l{Model::instancing}{instancing} property of models |
163 | that are not part of the particle system. |
164 | |
165 | It is also possible to use this feature to provide an instancing table without showing any |
166 | particles. This is done by omitting the \l delegate property. For example: |
167 | |
168 | \qml |
169 | ParticleSystem3D { |
170 | id: psystem |
171 | ModelParticle3D { |
172 | id: particleRed |
173 | maxAmount: 200 |
174 | color: "#ff0000" |
175 | colorVariation: Qt.vector4d(0.5,0.5,0.5,0.5) |
176 | } |
177 | |
178 | ParticleEmitter3D { |
179 | particle: particleRed |
180 | velocity: VectorDirection3D { |
181 | direction: Qt.vector3d(-20, 200, 0) |
182 | directionVariation: Qt.vector3d(20, 20, 20) |
183 | } |
184 | particleScale: 0.2 |
185 | emitRate: 20 |
186 | lifeSpan: 2000 |
187 | } |
188 | } |
189 | |
190 | Model { |
191 | source: "#Sphere" |
192 | instancing: particleRed.instanceTable |
193 | materials: PrincipledMaterial { |
194 | baseColor: "yellow" |
195 | } |
196 | } |
197 | \endqml |
198 | */ |
199 | |
200 | QQuick3DInstancing *QQuick3DParticleModelParticle::instanceTable() const |
201 | { |
202 | return m_instanceTable; |
203 | } |
204 | |
205 | void QQuick3DParticleModelParticle::clearInstanceTable() |
206 | { |
207 | if (m_instanceTable) |
208 | m_instanceTable->clear(); |
209 | } |
210 | |
211 | void QQuick3DParticleModelParticle::addInstance(const QVector3D &position, const QVector3D &scale, const QVector3D &eulerRotation, const QColor &color, float age) |
212 | { |
213 | if (m_instanceTable) |
214 | m_instanceTable->addInstance(position, scale, eulerRotation, color, age); |
215 | } |
216 | |
217 | void QQuick3DParticleModelParticle::commitInstance() |
218 | { |
219 | if (m_instanceTable) { |
220 | m_instanceTable->setHasTransparency(hasTransparency()); |
221 | m_instanceTable->commit(); |
222 | } |
223 | } |
224 | |
225 | static void setInstancing(QQuick3DNode *node, QQuick3DInstancing *instanceTable, float bias) |
226 | { |
227 | auto *asModel = qobject_cast<QQuick3DModel *>(object: node); |
228 | if (asModel) { |
229 | asModel->setInstancing(instanceTable); |
230 | asModel->setDepthBias(bias); |
231 | } |
232 | const auto children = node->childItems(); |
233 | for (auto *child : children) { |
234 | auto *childNode = qobject_cast<QQuick3DNode *>(object: child); |
235 | if (childNode) |
236 | setInstancing(node: childNode, instanceTable, bias); |
237 | } |
238 | } |
239 | |
240 | void QQuick3DParticleModelParticle::updateDepthBias(float bias) |
241 | { |
242 | setInstancing(node: m_node, instanceTable: m_instanceTable, bias); |
243 | } |
244 | |
245 | void QQuick3DParticleModelParticle::regenerate() |
246 | { |
247 | delete m_node; |
248 | m_node = nullptr; |
249 | |
250 | if (!isComponentComplete()) |
251 | return; |
252 | |
253 | if (!m_instanceTable) { |
254 | m_instanceTable = new QQuick3DParticleInstanceTable(); |
255 | m_instanceTable->setParent(this); |
256 | m_instanceTable->setParentItem(this); |
257 | emit instanceTableChanged(); |
258 | } else { |
259 | m_instanceTable->clear(); |
260 | } |
261 | |
262 | if (m_delegate.isNull()) |
263 | return; |
264 | |
265 | auto *obj = m_delegate->create(context: m_delegate->creationContext()); |
266 | |
267 | m_node = qobject_cast<QQuick3DNode *>(object: obj); |
268 | if (m_node) { |
269 | setInstancing(node: m_node, instanceTable: m_instanceTable, bias: depthBias()); |
270 | auto *particleSystem = system(); |
271 | m_node->setParent(particleSystem); |
272 | m_node->setParentItem(particleSystem); |
273 | } else { |
274 | delete obj; |
275 | } |
276 | } |
277 | |
278 | void QQuick3DParticleModelParticle::componentComplete() |
279 | { |
280 | if (!system() && qobject_cast<QQuick3DParticleSystem *>(object: parentItem())) |
281 | setSystem(qobject_cast<QQuick3DParticleSystem *>(object: parentItem())); |
282 | |
283 | QQuick3DParticle::componentComplete(); |
284 | regenerate(); |
285 | } |
286 | |
287 | void QQuick3DParticleModelParticle::itemChange(QQuick3DObject::ItemChange change, const QQuick3DObject::ItemChangeData &value) |
288 | { |
289 | QQuick3DObject::itemChange(change, value); |
290 | if (change == ItemParentHasChanged) |
291 | regenerate(); |
292 | } |
293 | |
294 | QT_END_NAMESPACE |
295 | |