| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "qquick3dparticlemodelblendparticle_p.h" |
| 5 | #include "qquick3dparticleemitter_p.h" |
| 6 | #include "qquick3dparticlerandomizer_p.h" |
| 7 | |
| 8 | #include <QtCore/qdir.h> |
| 9 | #include <QtQml/qqmlfile.h> |
| 10 | |
| 11 | #include <QtQuick3D/private/qquick3dobject_p.h> |
| 12 | #include <QtQuick3D/private/qquick3dgeometry_p.h> |
| 13 | |
| 14 | #include <QtQuick3DUtils/private/qssgutils_p.h> |
| 15 | #include <QtQuick3DRuntimeRender/private/qssgrenderparticles_p.h> |
| 16 | #include <QtQuick3DRuntimeRender/private/qssgrendergeometry_p.h> |
| 17 | #include <QtQuick3DRuntimeRender/private/qssgrendermodel_p.h> |
| 18 | #include <QtQuick3DUtils/private/qssgmesh_p.h> |
| 19 | |
| 20 | QT_BEGIN_NAMESPACE |
| 21 | |
| 22 | /*! |
| 23 | \qmltype ModelBlendParticle3D |
| 24 | \inherits Particle3D |
| 25 | \inqmlmodule QtQuick3D.Particles3D |
| 26 | \brief Blends particle effect with a 3D model. |
| 27 | |
| 28 | The type provides a way to blend particle effect with a 3D model. The provided model needs to be |
| 29 | triangle-based. Each triangle in the model is converted into a particle, which are then used by |
| 30 | the emitter. Instead of particle shader, the model is shaded using the \l {Model::materials}{material} |
| 31 | specified in the model. The way the effect is blended is determined by the \l modelBlendMode. |
| 32 | |
| 33 | The possible modes are: |
| 34 | \list |
| 35 | \li \b Construct, where the model gets constructed from the particles. |
| 36 | \li \b Explode, where the model gets converted into particles. |
| 37 | \li \b Transfer, where Construct and Explode are combined to create an effect where the model is |
| 38 | transferred from one place to another. |
| 39 | \endlist |
| 40 | |
| 41 | By default the particles are emitted in the order they are specified in the model unless \l emitMode is set |
| 42 | to \c Random or \l emitMode is set to \c Activation and \l activationNode is set. |
| 43 | |
| 44 | Some features defined in base class and emitters are not functional with this type: |
| 45 | \list |
| 46 | \li \l Particle3D::alignMode is not functional since the particles can be in arbitrary orientation |
| 47 | in the model. |
| 48 | \li\l Particle3D::sortMode is not functional since the particles are always rendered in the order |
| 49 | the primitives are specified in the model. |
| 50 | \li \l ParticleEmitter3D::depthBias is not functional since the model depth bias is used instead. |
| 51 | \endlist |
| 52 | |
| 53 | \note The default \l {Particle3D::fadeInEffect}{fadeInEffect} and \l {Particle3D::fadeInEffect}{fadeOutEffect} |
| 54 | are \c Particle3D.FadeNone. |
| 55 | */ |
| 56 | |
| 57 | QQuick3DParticleModelBlendParticle::QQuick3DParticleModelBlendParticle(QQuick3DNode *parent) |
| 58 | : QQuick3DParticle(*new QQuick3DObjectPrivate(QQuick3DObjectPrivate::Type::ModelBlendParticle), parent) |
| 59 | { |
| 60 | setFadeInEffect(QQuick3DParticle::FadeNone); |
| 61 | setFadeOutEffect(QQuick3DParticle::FadeNone); |
| 62 | QQuick3DParticle::doSetMaxAmount(amount: 0); |
| 63 | } |
| 64 | |
| 65 | QQuick3DParticleModelBlendParticle::~QQuick3DParticleModelBlendParticle() |
| 66 | { |
| 67 | delete m_model; |
| 68 | delete m_modelGeometry; |
| 69 | } |
| 70 | |
| 71 | /*! |
| 72 | \qmlproperty Component ModelBlendParticle3D::delegate |
| 73 | |
| 74 | The delegate provides a template defining the model for the ModelBlendParticle3D. |
| 75 | |
| 76 | For example, using the default sphere model with default material |
| 77 | |
| 78 | \qml |
| 79 | Component { |
| 80 | id: modelComponent |
| 81 | Model { |
| 82 | source: "#Sphere" |
| 83 | scale: Qt.vector3d(0.5, 0.5, 0.5) |
| 84 | materials: DefaultMaterial { diffuseColor: "red" } |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | ModelBlendParticle3D { |
| 89 | id: particleRedSphere |
| 90 | delegate: modelComponent |
| 91 | } |
| 92 | \endqml |
| 93 | */ |
| 94 | QQmlComponent *QQuick3DParticleModelBlendParticle::delegate() const |
| 95 | { |
| 96 | return m_delegate; |
| 97 | } |
| 98 | |
| 99 | void QQuick3DParticleModelBlendParticle::setDelegate(QQmlComponent *delegate) |
| 100 | { |
| 101 | if (delegate == m_delegate) |
| 102 | return; |
| 103 | m_delegate = delegate; |
| 104 | |
| 105 | reset(); |
| 106 | regenerate(); |
| 107 | Q_EMIT delegateChanged(); |
| 108 | } |
| 109 | |
| 110 | /*! |
| 111 | \qmlproperty Node ModelBlendParticle3D::endNode |
| 112 | |
| 113 | This property holds the node that specifies the transformation for the model at the end |
| 114 | of particle effect. It defines the size, position and rotation where the model is constructed |
| 115 | when using the \c ModelBlendParticle3D.Construct and \c ModelBlendParticle3D.Transfer blend modes. |
| 116 | */ |
| 117 | QQuick3DNode *QQuick3DParticleModelBlendParticle::endNode() const |
| 118 | { |
| 119 | return m_endNode; |
| 120 | } |
| 121 | |
| 122 | /*! |
| 123 | \qmlproperty enumeration ModelBlendParticle3D::ModelBlendMode |
| 124 | |
| 125 | Defines the blending mode for the particle effect. |
| 126 | |
| 127 | \value ModelBlendParticle3D.Explode |
| 128 | The model gets exploded i.e. the particles are emitted from the position of the model. |
| 129 | \value ModelBlendParticle3D.Construct |
| 130 | The model gets constructed i.e the particles fly from the emitter and construct the model at the end. |
| 131 | \value ModelBlendParticle3D.Transfer |
| 132 | Combines Explode and Transfer for the same model. |
| 133 | */ |
| 134 | /*! |
| 135 | \qmlproperty ModelBlendMode ModelBlendParticle3D::modelBlendMode |
| 136 | |
| 137 | This property holds blending mode for the particle effect. |
| 138 | */ |
| 139 | QQuick3DParticleModelBlendParticle::ModelBlendMode QQuick3DParticleModelBlendParticle::modelBlendMode() const |
| 140 | { |
| 141 | return m_modelBlendMode; |
| 142 | } |
| 143 | |
| 144 | /*! |
| 145 | \qmlproperty int ModelBlendParticle3D::endTime |
| 146 | |
| 147 | This property holds the end time of the particle in milliseconds. The end time is used during construction |
| 148 | and defines duration from particle lifetime at the end where the effect is blended with |
| 149 | the model positions. Before the end time the particles positions are defined only by the |
| 150 | particle effect, but during end time the particle position is blended linearly with the model |
| 151 | end position. |
| 152 | */ |
| 153 | int QQuick3DParticleModelBlendParticle::endTime() const |
| 154 | { |
| 155 | return m_endTime; |
| 156 | } |
| 157 | |
| 158 | /*! |
| 159 | \qmlproperty Node ModelBlendParticle3D::activationNode |
| 160 | |
| 161 | This property holds a node that activates particles and overrides the reqular emit routine. |
| 162 | The activation node can be used to control how the particles are emitted spatially when the |
| 163 | model is exploded/constructed from the particles. |
| 164 | The activation node emits a particle if the center of that particle is on the positive half |
| 165 | of the z-axis of the activation node. Animating the activation node to move trough the model |
| 166 | will cause the particles to be emitted sequentially along the path the activation node moves. |
| 167 | */ |
| 168 | QQuick3DNode *QQuick3DParticleModelBlendParticle::activationNode() const |
| 169 | { |
| 170 | return m_activationNode; |
| 171 | } |
| 172 | |
| 173 | /*! |
| 174 | \qmlproperty enumeration ModelBlendParticle3D::ModelBlendEmitMode |
| 175 | |
| 176 | Defines the emit mode of the particles |
| 177 | |
| 178 | \value ModelBlendParticle3D.Sequential |
| 179 | The particles are emitted in the order they are defined in the model. |
| 180 | \value ModelBlendParticle3D.Random |
| 181 | The particles are emitted in random order. |
| 182 | \value ModelBlendParticle3D.Activation |
| 183 | The particles are emitted when they are activated by the \l activationNode. |
| 184 | */ |
| 185 | /*! |
| 186 | \qmlproperty bool ModelBlendParticle3D::emitMode |
| 187 | |
| 188 | This property holds the emit mode of the particles. |
| 189 | */ |
| 190 | QQuick3DParticleModelBlendParticle::ModelBlendEmitMode QQuick3DParticleModelBlendParticle::emitMode() const |
| 191 | { |
| 192 | return m_emitMode; |
| 193 | } |
| 194 | |
| 195 | void QQuick3DParticleModelBlendParticle::setEndNode(QQuick3DNode *node) |
| 196 | { |
| 197 | if (m_endNode == node) |
| 198 | return; |
| 199 | if (m_endNode) |
| 200 | QObject::disconnect(receiver: this); |
| 201 | |
| 202 | m_endNode = node; |
| 203 | |
| 204 | if (m_endNode) { |
| 205 | QObject::connect(sender: m_endNode, signal: &QQuick3DNode::positionChanged, context: this, slot: &QQuick3DParticleModelBlendParticle::handleEndNodeChanged); |
| 206 | QObject::connect(sender: m_endNode, signal: &QQuick3DNode::rotationChanged, context: this, slot: &QQuick3DParticleModelBlendParticle::handleEndNodeChanged); |
| 207 | QObject::connect(sender: m_endNode, signal: &QQuick3DNode::scaleChanged, context: this, slot: &QQuick3DParticleModelBlendParticle::handleEndNodeChanged); |
| 208 | } |
| 209 | handleEndNodeChanged(); |
| 210 | Q_EMIT endNodeChanged(); |
| 211 | } |
| 212 | |
| 213 | void QQuick3DParticleModelBlendParticle::setModelBlendMode(ModelBlendMode mode) |
| 214 | { |
| 215 | if (m_modelBlendMode == mode) |
| 216 | return; |
| 217 | m_modelBlendMode = mode; |
| 218 | reset(); |
| 219 | Q_EMIT modelBlendModeChanged(); |
| 220 | } |
| 221 | |
| 222 | void QQuick3DParticleModelBlendParticle::setEndTime(int endTime) |
| 223 | { |
| 224 | if (endTime == m_endTime) |
| 225 | return; |
| 226 | m_endTime = endTime; |
| 227 | Q_EMIT endTimeChanged(); |
| 228 | } |
| 229 | |
| 230 | void QQuick3DParticleModelBlendParticle::setActivationNode(QQuick3DNode *activationNode) |
| 231 | { |
| 232 | if (m_activationNode == activationNode) |
| 233 | return; |
| 234 | |
| 235 | m_activationNode = activationNode; |
| 236 | Q_EMIT activationNodeChanged(); |
| 237 | } |
| 238 | |
| 239 | void QQuick3DParticleModelBlendParticle::setEmitMode(ModelBlendEmitMode emitMode) |
| 240 | { |
| 241 | if (m_emitMode == emitMode) |
| 242 | return; |
| 243 | |
| 244 | m_emitMode = emitMode; |
| 245 | Q_EMIT emitModeChanged(); |
| 246 | } |
| 247 | |
| 248 | void QQuick3DParticleModelBlendParticle::regenerate() |
| 249 | { |
| 250 | delete m_model; |
| 251 | m_model = nullptr; |
| 252 | |
| 253 | if (!isComponentComplete()) |
| 254 | return; |
| 255 | |
| 256 | if (!m_delegate) |
| 257 | return; |
| 258 | |
| 259 | if (QQuick3DParticleSystem::isGloballyDisabled()) |
| 260 | return; |
| 261 | |
| 262 | auto *obj = m_delegate->create(context: m_delegate->creationContext()); |
| 263 | |
| 264 | m_model = qobject_cast<QQuick3DModel *>(object: obj); |
| 265 | if (m_model) { |
| 266 | updateParticles(); |
| 267 | auto *psystem = QQuick3DParticle::system(); |
| 268 | m_model->setParent(psystem); |
| 269 | m_model->setParentItem(psystem); |
| 270 | } else { |
| 271 | delete obj; |
| 272 | } |
| 273 | handleEndNodeChanged(); |
| 274 | } |
| 275 | |
| 276 | static QSSGMesh::Mesh loadModelBlendParticleMesh(const QString &source) |
| 277 | { |
| 278 | QString src = source; |
| 279 | if (source.startsWith(c: QLatin1Char('#'))) { |
| 280 | src = QSSGBufferManager::primitivePath(primitive: source); |
| 281 | src.prepend(s: QLatin1String(":/" )); |
| 282 | } |
| 283 | src = QDir::cleanPath(path: src); |
| 284 | if (src.startsWith(s: QLatin1String("qrc:/" ))) |
| 285 | src = src.mid(position: 3); |
| 286 | |
| 287 | QFile file(src); |
| 288 | if (!file.open(flags: QFile::ReadOnly)) |
| 289 | return {}; |
| 290 | return QSSGMesh::Mesh::loadMesh(device: &file); |
| 291 | } |
| 292 | |
| 293 | static QVector3D getPosition(const quint8 *srcVertices, quint32 idx, quint32 vertexStride, quint32 posOffset) |
| 294 | { |
| 295 | const quint8 *vertex = srcVertices + idx * vertexStride; |
| 296 | return *reinterpret_cast<const QVector3D *>(vertex + posOffset); |
| 297 | } |
| 298 | |
| 299 | static float calcTriangleRadius(const QVector3D ¢er, const QVector3D &p0, const QVector3D &p1, const QVector3D &p2) |
| 300 | { |
| 301 | return qMax(a: center.distanceToPoint(point: p1), b: qMax(a: center.distanceToPoint(point: p2), b: center.distanceToPoint(point: p0))); |
| 302 | } |
| 303 | |
| 304 | static void copyToUnindexedVertices(QByteArray &unindexedVertexData, |
| 305 | QVector<QVector3D> ¢erData, |
| 306 | float &maxTriangleRadius, |
| 307 | const QByteArray &vertexBufferData, |
| 308 | quint32 vertexStride, |
| 309 | quint32 posOffset, |
| 310 | const QByteArray &indexBufferData, |
| 311 | bool u16Indices, |
| 312 | quint32 primitiveCount) |
| 313 | { |
| 314 | const quint8 *srcVertices = reinterpret_cast<const quint8 *>(vertexBufferData.data()); |
| 315 | quint8 *dst = reinterpret_cast<quint8 *>(unindexedVertexData.data()); |
| 316 | const quint16 *indexData16 = reinterpret_cast<const quint16 *>(indexBufferData.begin()); |
| 317 | const quint32 *indexData32 = reinterpret_cast<const quint32 *>(indexBufferData.begin()); |
| 318 | const float c_div3 = 1.0f / 3.0f; |
| 319 | for (quint32 i = 0; i < primitiveCount; i++) { |
| 320 | quint32 i0, i1, i2; |
| 321 | if (u16Indices) { |
| 322 | i0 = indexData16[3 * i]; |
| 323 | i1 = indexData16[3 * i + 1]; |
| 324 | i2 = indexData16[3 * i + 2]; |
| 325 | } else { |
| 326 | i0 = indexData32[3 * i]; |
| 327 | i1 = indexData32[3 * i + 1]; |
| 328 | i2 = indexData32[3 * i + 2]; |
| 329 | } |
| 330 | QVector3D p0 = getPosition(srcVertices, idx: i0, vertexStride, posOffset); |
| 331 | QVector3D p1 = getPosition(srcVertices, idx: i1, vertexStride, posOffset); |
| 332 | QVector3D p2 = getPosition(srcVertices, idx: i2, vertexStride, posOffset); |
| 333 | QVector3D center = (p0 + p1 + p2) * c_div3; |
| 334 | centerData[i] = center; |
| 335 | maxTriangleRadius = qMax(a: maxTriangleRadius, b: calcTriangleRadius(center, p0, p1, p2)); |
| 336 | memcpy(dest: dst, src: srcVertices + i0 * vertexStride, n: vertexStride); |
| 337 | dst += vertexStride; |
| 338 | memcpy(dest: dst, src: srcVertices + i1 * vertexStride, n: vertexStride); |
| 339 | dst += vertexStride; |
| 340 | memcpy(dest: dst, src: srcVertices + i2 * vertexStride, n: vertexStride); |
| 341 | dst += vertexStride; |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | static void getVertexCenterData(QVector<QVector3D> ¢erData, |
| 346 | float &maxTriangleRadius, |
| 347 | const QByteArray &vertexBufferData, |
| 348 | quint32 vertexStride, |
| 349 | quint32 posOffset, |
| 350 | quint32 primitiveCount) |
| 351 | { |
| 352 | const quint8 *srcVertices = reinterpret_cast<const quint8 *>(vertexBufferData.data()); |
| 353 | const float c_div3 = 1.0f / 3.0f; |
| 354 | for (quint32 i = 0; i < primitiveCount; i++) { |
| 355 | QVector3D p0 = getPosition(srcVertices, idx: 3 * i, vertexStride, posOffset); |
| 356 | QVector3D p1 = getPosition(srcVertices, idx: 3 * i + 1, vertexStride, posOffset); |
| 357 | QVector3D p2 = getPosition(srcVertices, idx: 3 * i + 2, vertexStride, posOffset); |
| 358 | QVector3D center = (p0 + p1 + p2) * c_div3; |
| 359 | centerData[i] = center; |
| 360 | maxTriangleRadius = qMax(a: maxTriangleRadius, b: calcTriangleRadius(center, p0, p1, p2)); |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | void QQuick3DParticleModelBlendParticle::updateParticles() |
| 365 | { |
| 366 | m_maxTriangleRadius = 0.f; |
| 367 | |
| 368 | // The primitives needs to be triangle list without indexing, because each triangle |
| 369 | // needs to be it's own primitive and we need vertex index to get the particle index, |
| 370 | // which we don't get with indexed primitives |
| 371 | if (m_model->geometry()) { |
| 372 | QQuick3DGeometry *geometry = m_model->geometry(); |
| 373 | if (geometry->primitiveType() != QQuick3DGeometry::PrimitiveType::Triangles) { |
| 374 | qWarning () << "ModelBlendParticle3D: Invalid geometry primitive type, must be Triangles. " ; |
| 375 | return; |
| 376 | } |
| 377 | auto vertexBuffer = geometry->vertexData(); |
| 378 | auto indexBuffer = geometry->indexData(); |
| 379 | |
| 380 | if (!vertexBuffer.size()) { |
| 381 | qWarning () << "ModelBlendParticle3D: Invalid geometry, vertexData is empty. " ; |
| 382 | return; |
| 383 | } |
| 384 | |
| 385 | const auto attributeBySemantic = [&](const QQuick3DGeometry *geometry, QQuick3DGeometry::Attribute::Semantic semantic) { |
| 386 | for (int i = 0; i < geometry->attributeCount(); i++) { |
| 387 | const auto attr = geometry->attribute(index: i); |
| 388 | if (attr.semantic == semantic) |
| 389 | return attr; |
| 390 | } |
| 391 | Q_ASSERT(0); |
| 392 | return QQuick3DGeometry::Attribute(); |
| 393 | }; |
| 394 | |
| 395 | if (indexBuffer.size()) { |
| 396 | m_modelGeometry = new QQuick3DGeometry; |
| 397 | |
| 398 | m_modelGeometry->setBounds(min: geometry->boundsMin(), max: geometry->boundsMax()); |
| 399 | m_modelGeometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles); |
| 400 | m_modelGeometry->setStride(geometry->stride()); |
| 401 | |
| 402 | for (int i = 0; i < geometry->attributeCount(); i++) { |
| 403 | auto attr = geometry->attribute(index: i); |
| 404 | if (attr.semantic != QQuick3DGeometry::Attribute::IndexSemantic) |
| 405 | m_modelGeometry->addAttribute(att: attr); |
| 406 | } |
| 407 | |
| 408 | // deindex data |
| 409 | QByteArray unindexedVertexData; |
| 410 | quint32 primitiveCount = indexBuffer.size(); |
| 411 | auto indexAttribute = attributeBySemantic(geometry, QQuick3DGeometry::Attribute::IndexSemantic); |
| 412 | bool u16IndexType = indexAttribute.componentType == QQuick3DGeometry::Attribute::U16Type; |
| 413 | if (u16IndexType) |
| 414 | primitiveCount /= 6; |
| 415 | else |
| 416 | primitiveCount /= 12; |
| 417 | |
| 418 | unindexedVertexData.resize(size: geometry->stride() * primitiveCount * 3); |
| 419 | m_centerData.resize(size: primitiveCount); |
| 420 | m_particleCount = primitiveCount; |
| 421 | copyToUnindexedVertices(unindexedVertexData, |
| 422 | centerData&: m_centerData, |
| 423 | maxTriangleRadius&: m_maxTriangleRadius, |
| 424 | vertexBufferData: vertexBuffer, |
| 425 | vertexStride: geometry->stride(), |
| 426 | posOffset: attributeBySemantic(geometry, QQuick3DGeometry::Attribute::PositionSemantic).offset, |
| 427 | indexBufferData: indexBuffer, |
| 428 | u16Indices: u16IndexType, |
| 429 | primitiveCount); |
| 430 | |
| 431 | m_modelGeometry->setVertexData(unindexedVertexData); |
| 432 | m_model->setGeometry(m_modelGeometry); |
| 433 | } else { |
| 434 | // can use provided geometry directly |
| 435 | quint32 primitiveCount = vertexBuffer.size() / geometry->stride() / 3; |
| 436 | m_centerData.resize(size: primitiveCount); |
| 437 | m_particleCount = primitiveCount; |
| 438 | getVertexCenterData(centerData&: m_centerData, |
| 439 | maxTriangleRadius&: m_maxTriangleRadius, |
| 440 | vertexBufferData: vertexBuffer, |
| 441 | vertexStride: geometry->stride(), |
| 442 | posOffset: attributeBySemantic(geometry, QQuick3DGeometry::Attribute::PositionSemantic).offset, |
| 443 | primitiveCount); |
| 444 | } |
| 445 | } else { |
| 446 | const QQmlContext *context = qmlContext(this); |
| 447 | QString src = m_model->source().toString(); |
| 448 | if (context && !src.startsWith(c: QLatin1Char('#'))) |
| 449 | src = QQmlFile::urlToLocalFileOrQrc(context->resolvedUrl(m_model->source())); |
| 450 | QSSGMesh::Mesh mesh = loadModelBlendParticleMesh(source: src); |
| 451 | if (!mesh.isValid()) { |
| 452 | qWarning () << "ModelBlendParticle3D: Unable to load mesh: " << src; |
| 453 | return; |
| 454 | } |
| 455 | if (mesh.drawMode() != QSSGMesh::Mesh::DrawMode::Triangles) { |
| 456 | qWarning () << "ModelBlendParticle3D: Invalid mesh primitive type, must be Triangles. " ; |
| 457 | return; |
| 458 | } |
| 459 | |
| 460 | m_modelGeometry = new QQuick3DGeometry; |
| 461 | |
| 462 | const auto vertexBuffer = mesh.vertexBuffer(); |
| 463 | const auto indexBuffer = mesh.indexBuffer(); |
| 464 | |
| 465 | const auto entryOffset = [&](const QSSGMesh::Mesh::VertexBuffer &vb, const QByteArray &name) -> int { |
| 466 | for (const auto &e : vb.entries) { |
| 467 | if (e.name == name) { |
| 468 | Q_ASSERT(e.componentType == QSSGMesh::Mesh::ComponentType::Float32); |
| 469 | return e.offset; |
| 470 | } |
| 471 | } |
| 472 | Q_ASSERT(0); |
| 473 | return -1; |
| 474 | }; |
| 475 | const auto toAttribute = [&](const QSSGMesh::Mesh::VertexBufferEntry &e) -> QQuick3DGeometry::Attribute { |
| 476 | QQuick3DGeometry::Attribute a; |
| 477 | a.componentType = QQuick3DGeometryPrivate::toComponentType(componentType: e.componentType); |
| 478 | a.offset = e.offset; |
| 479 | a.semantic = QQuick3DGeometryPrivate::semanticFromName(name: e.name); |
| 480 | return a; |
| 481 | }; |
| 482 | |
| 483 | const auto indexedPrimitiveCount = [&](const QSSGMesh::Mesh::IndexBuffer &indexBuffer) -> quint32 { |
| 484 | if (indexBuffer.componentType == QSSGMesh::Mesh::ComponentType::UnsignedInt16) |
| 485 | return quint32(indexBuffer.data.size() / sizeof(quint16) / 3); |
| 486 | return quint32(indexBuffer.data.size() / sizeof(quint32) / 3); |
| 487 | }; |
| 488 | |
| 489 | if (indexBuffer.data.size()) { |
| 490 | // deindex data |
| 491 | QByteArray unindexedVertexData; |
| 492 | quint32 primitiveCount = indexedPrimitiveCount(indexBuffer); |
| 493 | bool u16IndexType = indexBuffer.componentType == QSSGMesh::Mesh::ComponentType::UnsignedInt16; |
| 494 | unindexedVertexData.resize(size: vertexBuffer.stride * primitiveCount * 3); |
| 495 | m_centerData.resize(size: primitiveCount); |
| 496 | m_particleCount = primitiveCount; |
| 497 | |
| 498 | copyToUnindexedVertices(unindexedVertexData, |
| 499 | centerData&: m_centerData, |
| 500 | maxTriangleRadius&: m_maxTriangleRadius, |
| 501 | vertexBufferData: vertexBuffer.data, |
| 502 | vertexStride: vertexBuffer.stride, |
| 503 | posOffset: entryOffset(vertexBuffer, QByteArray(QSSGMesh::MeshInternal::getPositionAttrName())), |
| 504 | indexBufferData: indexBuffer.data, |
| 505 | u16Indices: u16IndexType, |
| 506 | primitiveCount); |
| 507 | m_modelGeometry->setBounds(min: mesh.subsets().first().bounds.min, max: mesh.subsets().first().bounds.max); |
| 508 | m_modelGeometry->setStride(vertexBuffer.stride); |
| 509 | m_modelGeometry->setVertexData(unindexedVertexData); |
| 510 | m_modelGeometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles); |
| 511 | } else { |
| 512 | // can use vertexbuffer directly |
| 513 | quint32 primitiveCount = vertexBuffer.data.size() / vertexBuffer.stride / 3; |
| 514 | m_centerData.resize(size: primitiveCount); |
| 515 | m_particleCount = primitiveCount; |
| 516 | getVertexCenterData(centerData&: m_centerData, |
| 517 | maxTriangleRadius&: m_maxTriangleRadius, |
| 518 | vertexBufferData: vertexBuffer.data, |
| 519 | vertexStride: vertexBuffer.stride, |
| 520 | posOffset: entryOffset(vertexBuffer, QByteArray(QSSGMesh::MeshInternal::getPositionAttrName())), |
| 521 | primitiveCount); |
| 522 | m_modelGeometry->setBounds(min: mesh.subsets().first().bounds.min, max: mesh.subsets().first().bounds.max); |
| 523 | m_modelGeometry->setStride(vertexBuffer.stride); |
| 524 | m_modelGeometry->setVertexData(vertexBuffer.data); |
| 525 | m_modelGeometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles); |
| 526 | } |
| 527 | for (auto &e : vertexBuffer.entries) |
| 528 | m_modelGeometry->addAttribute(att: toAttribute(e)); |
| 529 | for (auto &s : mesh.subsets()) |
| 530 | m_modelGeometry->addSubset(offset: s.offset, count: s.count, boundsMin: s.bounds.min, boundsMax: s.bounds.max, name: s.name); |
| 531 | |
| 532 | m_model->setSource({}); |
| 533 | m_model->setGeometry(m_modelGeometry); |
| 534 | } |
| 535 | |
| 536 | QMatrix4x4 transform = m_model->sceneTransform(); |
| 537 | if (m_model->parentNode()) |
| 538 | transform = m_model->parentNode()->sceneTransform().inverted() * transform; |
| 539 | const QVector3D scale = QSSGUtils::mat44::getScale(m: transform); |
| 540 | // Take max component scale for a conservative bounds estimation |
| 541 | const float scaleMax = qMax(a: scale.x(), b: qMax(a: scale.y(), b: scale.z())); |
| 542 | m_maxTriangleRadius *= scaleMax; |
| 543 | |
| 544 | m_triangleParticleData.resize(size: m_particleCount); |
| 545 | m_particleData.resize(size: m_particleCount); |
| 546 | m_particleData.fill(t: {}); |
| 547 | for (int i = 0; i < m_particleCount; i++) { |
| 548 | m_triangleParticleData[i].center = m_centerData[i]; |
| 549 | m_centerData[i] = transform.map(point: m_centerData[i]); |
| 550 | if (m_modelBlendMode == Construct) { |
| 551 | m_triangleParticleData[i].size = 0.0f; |
| 552 | } else { |
| 553 | m_triangleParticleData[i].size = 1.0f; |
| 554 | m_triangleParticleData[i].position = m_centerData[i]; |
| 555 | } |
| 556 | } |
| 557 | QQuick3DParticle::doSetMaxAmount(amount: m_particleCount); |
| 558 | } |
| 559 | |
| 560 | QSSGRenderGraphObject *QQuick3DParticleModelBlendParticle::updateSpatialNode(QSSGRenderGraphObject *node) |
| 561 | { |
| 562 | if (!m_model) |
| 563 | return node; |
| 564 | auto *spatialNode = QQuick3DObjectPrivate::get(item: m_model)->spatialNode; |
| 565 | if (!spatialNode) { |
| 566 | spatialNode = QQuick3DObjectPrivate::updateSpatialNode(o: m_model, n: nullptr); |
| 567 | QQuick3DObjectPrivate::get(item: m_model)->spatialNode = spatialNode; |
| 568 | Q_QUICK3D_PROFILE_ASSIGN_ID_SG(this, spatialNode); |
| 569 | } |
| 570 | #if QT_CONFIG(qml_debug) |
| 571 | if (m_modelGeometry) { |
| 572 | auto *geometrySpatialNode = QQuick3DObjectPrivate::get(item: m_modelGeometry)->spatialNode; |
| 573 | if (geometrySpatialNode) |
| 574 | Q_QUICK3D_PROFILE_ASSIGN_ID_SG(this, geometrySpatialNode); |
| 575 | } |
| 576 | #endif |
| 577 | |
| 578 | QSSGRenderModel *model = static_cast<QSSGRenderModel *>(spatialNode); |
| 579 | |
| 580 | if (!model->particleBuffer) { |
| 581 | QSSGParticleBuffer *buffer = model->particleBuffer = new QSSGParticleBuffer; |
| 582 | buffer->resize(particleCount: m_particleCount, particleSize: sizeof(QSSGTriangleParticle)); |
| 583 | } |
| 584 | QQuick3DParticleSystem *psystem = QQuick3DParticle::system(); |
| 585 | QMatrix4x4 particleMatrix = psystem->sceneTransform().inverted() * m_model->sceneTransform(); |
| 586 | model->particleMatrix = particleMatrix.inverted(); |
| 587 | model->hasTransparency = fadeInEffect() == QQuick3DParticle::FadeOpacity || fadeOutEffect() == QQuick3DParticle::FadeOpacity; |
| 588 | updateParticleBuffer(buffer: model->particleBuffer, sceneTransform: psystem->sceneTransform()); |
| 589 | |
| 590 | return node; |
| 591 | } |
| 592 | |
| 593 | void QQuick3DParticleModelBlendParticle::componentComplete() |
| 594 | { |
| 595 | if (!system() && qobject_cast<QQuick3DParticleSystem *>(object: parentItem())) |
| 596 | setSystem(qobject_cast<QQuick3DParticleSystem *>(object: parentItem())); |
| 597 | |
| 598 | // don't call particles componentComplete, we don't wan't to emit maxAmountChanged yet |
| 599 | QQuick3DObject::componentComplete(); |
| 600 | regenerate(); |
| 601 | } |
| 602 | |
| 603 | void QQuick3DParticleModelBlendParticle::doSetMaxAmount(int) |
| 604 | { |
| 605 | qWarning() << "ModelBlendParticle3D.maxAmount: Unable to set maximum amount, because it is set from the model." ; |
| 606 | return; |
| 607 | } |
| 608 | |
| 609 | int QQuick3DParticleModelBlendParticle::nextCurrentIndex(const QQuick3DParticleEmitter *emitter) |
| 610 | { |
| 611 | if (!m_perEmitterData.contains(key: emitter)) { |
| 612 | m_perEmitterData.insert(key: emitter, value: PerEmitterData()); |
| 613 | auto &perEmitter = m_perEmitterData[emitter]; |
| 614 | perEmitter.emitter = emitter; |
| 615 | perEmitter.emitterIndex = m_nextEmitterIndex++; |
| 616 | } |
| 617 | auto &perEmitter = m_perEmitterData[emitter]; |
| 618 | int index = QQuick3DParticle::nextCurrentIndex(emitter); |
| 619 | if (m_triangleParticleData[index].emitterIndex != perEmitter.emitterIndex) { |
| 620 | if (m_triangleParticleData[index].emitterIndex >= 0) |
| 621 | perEmitterData(emitterIndex: m_triangleParticleData[index].emitterIndex).particleCount--; |
| 622 | perEmitter.particleCount++; |
| 623 | } |
| 624 | m_triangleParticleData[index].emitterIndex = perEmitter.emitterIndex; |
| 625 | return index; |
| 626 | } |
| 627 | |
| 628 | |
| 629 | void QQuick3DParticleModelBlendParticle::setParticleData(int particleIndex, |
| 630 | const QVector3D &position, |
| 631 | const QVector3D &rotation, |
| 632 | const QVector4D &color, |
| 633 | float size, float age) |
| 634 | { |
| 635 | auto &dst = m_triangleParticleData[particleIndex]; |
| 636 | dst = {.position: position, .rotation: rotation, .center: dst.center, .color: color, .age: age, .size: size, .emitterIndex: dst.emitterIndex}; |
| 637 | m_dataChanged = true; |
| 638 | } |
| 639 | |
| 640 | QQuick3DParticleModelBlendParticle::PerEmitterData &QQuick3DParticleModelBlendParticle::perEmitterData(int emitterIndex) |
| 641 | { |
| 642 | for (auto &perEmitter : m_perEmitterData) { |
| 643 | if (perEmitter.emitterIndex == emitterIndex) |
| 644 | return perEmitter; |
| 645 | } |
| 646 | return n_noPerEmitterData; |
| 647 | } |
| 648 | |
| 649 | void QQuick3DParticleModelBlendParticle::updateParticleBuffer(QSSGParticleBuffer *buffer, const QMatrix4x4 &sceneTransform) |
| 650 | { |
| 651 | const auto &particles = m_triangleParticleData; |
| 652 | |
| 653 | if (!buffer || !m_dataChanged) |
| 654 | return; |
| 655 | |
| 656 | const int particleCount = m_particleCount; |
| 657 | |
| 658 | char *dest = buffer->pointer(); |
| 659 | const TriangleParticleData *src = particles.data(); |
| 660 | const int pps = buffer->particlesPerSlice(); |
| 661 | const int ss = buffer->sliceStride(); |
| 662 | const int slices = buffer->sliceCount(); |
| 663 | const float c_degToRad = float(M_PI / 180.0f); |
| 664 | int i = 0; |
| 665 | QSSGBounds3 bounds; |
| 666 | for (int s = 0; s < slices; s++) { |
| 667 | QSSGTriangleParticle *dp = reinterpret_cast<QSSGTriangleParticle *>(dest); |
| 668 | for (int p = 0; p < pps && i < particleCount; ) { |
| 669 | if (src->size > 0.0f) |
| 670 | bounds.include(v: src->position); |
| 671 | dp->position = src->position; |
| 672 | dp->rotation = src->rotation * c_degToRad; |
| 673 | dp->color = src->color; |
| 674 | dp->age = src->age; |
| 675 | dp->center = src->center; |
| 676 | dp->size = src->size; |
| 677 | dp++; |
| 678 | p++; |
| 679 | i++; |
| 680 | src++; |
| 681 | } |
| 682 | dest += ss; |
| 683 | } |
| 684 | |
| 685 | bounds.fatten(distance: m_maxTriangleRadius); |
| 686 | bounds.transform(inMatrix: sceneTransform); |
| 687 | buffer->setBounds(bounds); |
| 688 | m_dataChanged = false; |
| 689 | } |
| 690 | |
| 691 | void QQuick3DParticleModelBlendParticle::itemChange(QQuick3DObject::ItemChange change, |
| 692 | const QQuick3DObject::ItemChangeData &value) |
| 693 | { |
| 694 | QQuick3DObject::itemChange(change, value); |
| 695 | if (change == ItemParentHasChanged && value.sceneManager) |
| 696 | regenerate(); |
| 697 | } |
| 698 | |
| 699 | void QQuick3DParticleModelBlendParticle::reset() |
| 700 | { |
| 701 | QQuick3DParticle::reset(); |
| 702 | if (m_particleCount) { |
| 703 | for (int i = 0; i < m_particleCount; i++) { |
| 704 | if (m_modelBlendMode == Construct) { |
| 705 | m_triangleParticleData[i].size = 0.0f; |
| 706 | } else { |
| 707 | m_triangleParticleData[i].size = 1.0f; |
| 708 | m_triangleParticleData[i].position = m_triangleParticleData[i].center; |
| 709 | } |
| 710 | } |
| 711 | } |
| 712 | } |
| 713 | |
| 714 | QVector3D QQuick3DParticleModelBlendParticle::particleCenter(int particleIndex) const |
| 715 | { |
| 716 | return m_centerData[particleIndex]; |
| 717 | } |
| 718 | |
| 719 | bool QQuick3DParticleModelBlendParticle::lastParticle() const |
| 720 | { |
| 721 | return m_currentIndex >= m_maxAmount - 1; |
| 722 | } |
| 723 | |
| 724 | static QMatrix3x3 qt_fromEulerRotation(const QVector3D &eulerRotation) |
| 725 | { |
| 726 | float x = qDegreesToRadians(degrees: eulerRotation.x()); |
| 727 | float y = qDegreesToRadians(degrees: eulerRotation.y()); |
| 728 | float z = qDegreesToRadians(degrees: eulerRotation.z()); |
| 729 | float a = cos(x: x); |
| 730 | float b = sin(x: x); |
| 731 | float c = cos(x: y); |
| 732 | float d = sin(x: y); |
| 733 | float e = cos(x: z); |
| 734 | float f = sin(x: z); |
| 735 | QMatrix3x3 ret; |
| 736 | float bd = b * d; |
| 737 | float ad = a * d; |
| 738 | ret(0,0) = c * e; |
| 739 | ret(0,1) = -c * f; |
| 740 | ret(0,2) = d; |
| 741 | ret(1,0) = bd * e + a * f; |
| 742 | ret(1,1) = a * e - bd * f; |
| 743 | ret(1,2) = -b * c; |
| 744 | ret(2,0) = b * f - ad * e; |
| 745 | ret(2,1) = ad * f + b * e; |
| 746 | ret(2,2) = a * c; |
| 747 | return ret; |
| 748 | } |
| 749 | |
| 750 | void QQuick3DParticleModelBlendParticle::handleEndNodeChanged() |
| 751 | { |
| 752 | if (m_endNode && m_model) { |
| 753 | if (!m_model->rotation().isIdentity()) { |
| 754 | // Use the same function as the shader for end node rotation so that they produce same matrix |
| 755 | QMatrix3x3 r1 = qt_fromEulerRotation(eulerRotation: m_endNode->eulerRotation()); |
| 756 | QMatrix3x3 r2 = m_model->rotation().toRotationMatrix(); |
| 757 | QMatrix3x3 r = r2 * r1.transposed() * r2.transposed(); |
| 758 | m_endNodeRotation = m_endNode->eulerRotation(); |
| 759 | m_endRotationMatrix = QMatrix4x4(r); |
| 760 | } else { |
| 761 | m_endNodeRotation = m_endNode->eulerRotation(); |
| 762 | m_endRotationMatrix = QMatrix4x4(m_endNode->rotation().toRotationMatrix().transposed()); |
| 763 | } |
| 764 | m_endNodePosition = m_endNode->position(); |
| 765 | m_endNodeScale = m_endNode->scale(); |
| 766 | } else { |
| 767 | m_endNodePosition = QVector3D(); |
| 768 | m_endNodeRotation = QVector3D(); |
| 769 | m_endNodeScale = QVector3D(1.0f, 1.0f, 1.0f); |
| 770 | m_endRotationMatrix.setToIdentity(); |
| 771 | } |
| 772 | } |
| 773 | |
| 774 | QVector3D QQuick3DParticleModelBlendParticle::particleEndPosition(int idx) const |
| 775 | { |
| 776 | return m_endRotationMatrix.map(point: QVector3D(m_endNodeScale * m_centerData[idx])) + m_endNodePosition; |
| 777 | } |
| 778 | |
| 779 | QVector3D QQuick3DParticleModelBlendParticle::particleEndRotation(int) const |
| 780 | { |
| 781 | return m_endNodeRotation; |
| 782 | } |
| 783 | |
| 784 | int QQuick3DParticleModelBlendParticle::randomIndex(int particleIndex) |
| 785 | { |
| 786 | if (m_randomParticles.isEmpty()) { |
| 787 | m_randomParticles.resize(size: m_maxAmount); |
| 788 | for (int i = 0; i < m_maxAmount; i++) |
| 789 | m_randomParticles[i] = i; |
| 790 | |
| 791 | // Randomize particle indices just once |
| 792 | QRandomGenerator rand(system()->rand()->generator()); |
| 793 | for (int i = 0; i < m_maxAmount; i++) { |
| 794 | int ridx = rand.generate() % m_maxAmount; |
| 795 | if (i != ridx) |
| 796 | qSwap(value1&: m_randomParticles[i], value2&: m_randomParticles[ridx]); |
| 797 | } |
| 798 | } |
| 799 | return m_randomParticles[particleIndex]; |
| 800 | } |
| 801 | |
| 802 | QT_END_NAMESPACE |
| 803 | |