Revision control

++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include "qquick3dparticlesceneshape_p.h"
++ #include "qquick3dparticlemodelshape_p.h"
++ #include "qquick3dparticlerandomizer_p.h"
++ #include "qquick3dparticlesystem_p.h"
++ #include "qquick3dparticleutils_p.h"
++ #include <QtQml/qqmlfile.h>
++ #include <QtQuick3D/QQuick3DGeometry>
++ #include <QtQuick3D/private/qquick3dmodel_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
++ #include <algorithm>
++
++ QT_BEGIN_NAMESPACE
++
++ /*!
++ \qmltype ParticleSceneShape3D
++ \inherits ParticleAbstractShape3D
++ \inqmlmodule QtQuick3D.Particles3D
++ \brief Offers particle shape from a scene for emitters and affectors.
++ \since 6.11
++
++ The ParticleSceneShape3D element can be used to get particle shape from a 3D scene.
++ The shape envelopes the scene into single shape based on the models in a scene.
++
++ For example,
++
++ \qml
++ ParticleEmitter3D {
++ id: emitter
++ particle: particle
++
++ ...
++
++ shape: ParticleSceneShape3D {
++ id: sceneShape
++ scene: sceneRoot
++ sceneCenter: node.scenePosition
++ sceneExtents: Qt.vector3d(1000, 500, 1000)
++ }
++
++ ...
++ }
++ \endqml
++ */
++
++ QQuick3DParticleSceneShape::QQuick3DParticleSceneShape(QObject *parent)
++ : QQuick3DParticleAbstractShape(parent)
++ {
++
++ }
++
++ QQuick3DParticleSceneShape::~QQuick3DParticleSceneShape()
++ {
++ delete m_geometry;
++ clearModelVertexPositions();
++ }
++
++ /*!
++ \qmlproperty Node ParticleSceneShape3D::scene
++
++ This property holds the root node of the scene. The scene shape is dynamically
++ calculated based on the child models of the scene. If the scene changes,
++ the shape is recalculated. If a model is not visible in the scene, it won't be
++ included in the shape. Only the node visibility is checked by the implementation.
++ If the material is fully transparent or the node gets filtered out, the shape
++ will still contain those nodes unless they are manyally excluded.
++ */
++ QQuick3DNode *QQuick3DParticleSceneShape::scene() const
++ {
++ return m_scene;
++ }
++
++ void QQuick3DParticleSceneShape::setScene(QQuick3DNode *scene)
++ {
++ if (m_scene == scene)
++ return;
++ m_scene = scene;
++ clearModelVertexPositions();
++ if (scene)
++ createShapeData();
++ }
++
++ /*!
++ \qmlproperty Node ParticleSceneShape3D::sceneCenter
++
++ This property holds the center point where the shape is calculated.
++
++ The default value is \c Qt.vector3(0, 0, 0).
++ */
++ QVector3D QQuick3DParticleSceneShape::sceneCenter() const
++ {
++ return m_sceneCenter;
++ }
++
++ void QQuick3DParticleSceneShape::setSceneCenter(const QVector3D &center)
++ {
++ if (m_sceneCenter == center)
++ return;
++
++ m_sceneCenter = center;
++ Q_EMIT sceneCenterChanged();
++ markDirty();
++ }
++
++ /*!
++ \qmlproperty Node ParticleSceneShape3D::sceneExtents
++
++ This property holds the extents for which the shape is calculated.
++ The extents are added to the scene center and any polygon outside
++ this volume are left out of the shape. If this value is empty, all
++ polygons are included.
++
++ The default value is \c Qt.vector3(0, 0, 0).
++ */
++ QVector3D QQuick3DParticleSceneShape::sceneExtents() const
++ {
++ return m_sceneExtents;
++ }
++
++ void QQuick3DParticleSceneShape::setSceneExtents(const QVector3D &extents)
++ {
++ if (m_sceneExtents == extents)
++ return;
++
++ m_sceneExtents = extents;
++ Q_EMIT sceneExtentsChanged();
++ markDirty();
++ }
++
++ /*!
++ \qmlproperty real ParticleSceneShape3D::shapeResolution
++
++ This property holds the resolution of the shape. The higher the value
++ the more accurately the shape resembles individual models.
++
++ The minimum value is 1.0, the maximum value is 100.0;
++ \default 10.0;
++ */
++ float QQuick3DParticleSceneShape::shapeResolution() const
++ {
++ return m_shapeResolution;
++ }
++
++ void QQuick3DParticleSceneShape::setShapeResolution(float resolution)
++ {
++ if (qFuzzyCompare(resolution, m_shapeResolution))
++ return;
++
++ m_shapeResolution = qBound(1.0f, resolution, 100.0f);
++ Q_EMIT sceneExtentsChanged();
++ markDirty();
++ }
++
++ /*!
++ \qmlproperty Node ParticleSceneShape3D::geometry
++
++ This property holds the geometry of the generated shape. It can be used to render
++ the shape to visualize it.
++ */
++ QQuick3DGeometry *QQuick3DParticleSceneShape::geometry() const
++ {
++ return m_geometry;
++ }
++
++ /*!
++ \qmlproperty list<Node> ParticleSceneShape3D::excludedNodes
++
++ This property holds the list of nodes that are excluded when building
++ the shape. Model is excluded from the shape if it is one of the nodes
++ or their child node.
++ */
++ QList<QQuick3DNode *> QQuick3DParticleSceneShape::excludedNodes() const
++ {
++ return m_excludeList;
++ }
++
++ void QQuick3DParticleSceneShape::setExcludedNodes(const QList<QQuick3DNode *> &nodes)
++ {
++ if (nodes == m_excludeList)
++ return;
++ m_excludeList = nodes;
++ Q_EMIT excludedNodesChanged();
++ markDirty(false, true);
++ }
++
++ QVector3D QQuick3DParticleSceneShape::getPosition(int particleIndex)
++ {
++ return randomPositionModel(particleIndex);
++ }
++
++ QVector3D QQuick3DParticleSceneShape::getSurfaceNormal(int particleIndex)
++ {
++ if (m_cachedIndex != particleIndex)
++ getPosition(particleIndex);
++ return m_cachedNormal;
++ }
++
++ void QQuick3DParticleSceneShape::markDirty(bool dirty, bool excludeDirty)
++ {
++ for (auto &model : m_data) {
++ model.dirty |= dirty;
++ model.excludedDirty |= excludeDirty;
++ }
++ }
++
++ /*
++ SceneNapkinMesh is a grid mesh that is placed on top of the scene
++ following the scene geometry. If neighbouring cells have large
++ enough height difference, they are not connected and the shape
++ has a hole in that position.
++ */
++ struct QQuick3DParticleSceneShape::SceneNapkinMesh
++ {
++ struct Triangle
++ {
++ QVector3D p0, p1, p2;
++ };
++
++ struct Cell
++ {
++ float x, z;
++ float w, d;
++ float y;
++ int subcellCount = 0;
++ int ccx, ccz;
++ Cell *subcells = nullptr;
++ static constexpr float c_maximumYDiff = 40.0f;
++
++ ~Cell() {
++ delete [] subcells;
++ }
++ QList<Triangle> triangles;
++
++ void init(float x, float z, float w, float d, int ccx, int ccz, float y)
++ {
++ this->ccx = ccx;
++ this->ccz = ccz;
++ this->x = x;
++ this->z = z;
++ this->w = w;
++ this->d = d;
++ this->y = y;
++ subcellCount = ccz * ccx;
++ subcells = new Cell[subcellCount];
++ initCells(subcells, x, z, w, d, ccx, ccz, y);
++ }
++
++ QVector3D center() const
++ {
++ return QVector3D(x + 0.5f * w, y, z + 0.5f * d);
++ }
++
++ void initCells(Cell *cells, float x, float z, float w, float d, int ccx, int ccz, float y)
++ {
++ float wccx = w / float(ccx - 1);
++ float dccz = d / float(ccz - 1);
++ for (int i = 0; i < ccx; i++) {
++ for (int j = 0; j < ccz; j++) {
++ int idx = i * ccz + j;
++ cells[idx].x = x + i * wccx;
++ cells[idx].z = z + j * dccz;
++ cells[idx].w = wccx;
++ cells[idx].d = dccz;
++ cells[idx].subcellCount = 0;
++ cells[idx].subcells = nullptr;
++ cells[idx].y = y;
++ cells[idx].ccx = 0;
++ cells[idx].ccz = 0;
++ }
++ }
++ }
++
++ void update(const QVector3D &v1, const QVector3D &v2, const QVector3D &v3, bool append = true)
++ {
++ if (subcellCount == 0) {
++ // Extents was empty so simply collect the triangles
++ if (append)
++ triangles.append({v1, v2, v3});
++ return;
++ }
++ // First get the rectangle this triangle forms ...
++ float wccx = w / float(ccx - 1);
++ float dccz = d / float(ccz - 1);
++ int cx1 = (v1.x() - x) / wccx;
++ int cz1 = (v1.z() - z) / dccz;
++ int cx2 = (v2.x() - x) / wccx;
++ int cz2 = (v2.z() - z) / dccz;
++ int cx3 = (v3.x() - x) / wccx;
++ int cz3 = (v3.z() - z) / dccz;
++ cx1 = qBound(0, cx1, ccx - 1);
++ cx2 = qBound(0, cx2, ccx - 1);
++ cx3 = qBound(0, cx3, ccx - 1);
++ cz1 = qBound(0, cz1, ccz - 1);
++ cz2 = qBound(0, cz2, ccz - 1);
++ cz3 = qBound(0, cz3, ccz - 1);
++ int minx = qMin(qMin(cx1, cx2), cx3);
++ int maxx = qMax(qMax(cx1, cx2), cx3);
++ int minz = qMin(qMin(cz1, cz2), cz3);
++ int maxz = qMax(qMax(cz1, cz2), cz3);
++
++ // ... then test all cells inside this rectangle if they are inside the triangle.
++ // If they are, adjust the height of the cell to the height of the triangle.
++ for (int i = minx; i <= maxx; i++) {
++ for (int j = minz; j <= maxz; j++) {
++ int idx = i * ccz + j;
++ QVector3D point = subcells[idx].center();
++ point.setY(this->y);
++ float py;
++ bool isIn = yOfTriangle(point, v1, v2, v3, py);
++ if (isIn) {
++ subcells[idx].y = qMax(subcells[idx].y, py);
++ if (append)
++ subcells[idx].triangles.append({v1, v2, v3});
++ } else if (minx == maxx && minz == maxz) { // Triangle is smaller than a cell so update anyway
++ subcells[idx].y = qMax(subcells[idx].y, qMax(v1.y(),qMax(v2.y(), v3.y())));
++ if (append)
++ subcells[idx].triangles.append({v1, v2, v3});
++ }
++ }
++ }
++ }
++
++ void updateSubcells()
++ {
++ for (const auto &triangle : std::as_const(triangles))
++ update(triangle.p0, triangle.p1, triangle.p2, false);
++ }
++
++ QList<QVector3D> positions(float miny, float icp100)
++ {
++ QList<QVector3D> ret;
++ Q_ASSERT(subcells);
++ int pointCount = (ccx - 1) * (ccz - 1) * 6;
++ ret.reserve(pointCount);
++
++ // Form the mesh grid from the cells
++ for (int i = 0; i < ccx - 1; i++) {
++ for (int j = 0; j < ccz - 1; j++) {
++ int ca = i * ccz + j;
++ int cb = (i + 1) * ccz + j;
++ auto &sc = subcells[ca];
++ if (sc.subcells) {
++ ret.append(subcells[ca].positions(miny, icp100));
++ } else {
++ QVector3D a, b, c, d;
++ a = subcells[ca].center();
++ b = subcells[ca + 1].center();
++ c = subcells[cb].center();
++ d = subcells[cb + 1].center();
++ float ydiff1 = qMax(qMax(qAbs(a.y() - b.y()), qAbs(a.y() - c.y())), qAbs(b.y() - c.y()));
++ float ydiff2 = qMax(qMax(qAbs(d.y() - b.y()), qAbs(d.y() - c.y())), qAbs(b.y() - c.y()));
++ // Only add grid cell if the y is greated than the ground
++ if (a.y() > miny && b.y() > miny && c.y() > miny && d.y() > miny) {
++ // Do not add triangle if the height difference to neighbour is too big.
++ if (ydiff1 < c_maximumYDiff) {
++ ret.append(a);
++ ret.append(b);
++ ret.append(c);
++ }
++ if (ydiff2 < c_maximumYDiff) {
++ ret.append(c);
++ ret.append(b);
++ ret.append(d);
++ }
++ }
++ }
++ }
++ }
++ return ret;
++ }
++ };
++
++ Cell rootCell;
++ float width = 0;
++ float depth = 0;
++ float x = 0;
++ float z = 0;
++ float icp100;
++ QVector3D ext;
++ QVector3D cent;
++ QVector3D boundsMin, boundsMax;
++
++ SceneNapkinMesh() {}
++ ~SceneNapkinMesh() {}
++
++ void init(const QVector3D &extents, const QVector3D &center, float initialCellsPer100)
++ {
++ ext = extents;
++ cent = center;
++ icp100 = initialCellsPer100;
++ x = center.x()-extents.x();
++ z = center.z()-extents.z();
++ if (extents.isNull()) {
++ // If the extents is empty, we must collect all the triangles
++ // and calculate the bounds while collecting them
++ constexpr auto maxVal = std::numeric_limits<float>().max();
++ constexpr auto minVal = std::numeric_limits<float>().min();
++ boundsMin = QVector3D(maxVal, maxVal, maxVal);
++ boundsMax = QVector3D(minVal, minVal, minVal);
++ return;
++ }
++ width = extents.x() * 2.0f;
++ depth = extents.z() * 2.0f;
++ float dwp = 100.0f / icp100;
++
++ int ccx = width / dwp + 1;
++ int ccz = depth / dwp + 1;
++
++ rootCell.init(x, z, width, depth, ccx, ccz, center.y()-extents.y());
++ }
++
++ // Calculate y from top-down inside triangle [p0, p1, p2] based on point p, which
++ // must be inside the triangle.
++ static bool yOfTriangle(QVector3D p, QVector3D p0, QVector3D p1, QVector3D p2, float &y)
++ {
++ const auto scalarTriple = [](QVector3D a, QVector3D b, QVector3D c) {
++ return QVector3D::dotProduct(QVector3D::crossProduct(a, b), c);
++ };
++ const auto sameSign = [](float a, float b) {
++ return (a < 0.0f && b < 0.0f) || (a > 0.0f && b > 0.0f);
++ };
++
++ // Vectors from point to triangle vertices
++ QVector3D pp0 = p0 - p;
++ QVector3D pp1 = p1 - p;
++ QVector3D pp2 = p2 - p;
++
++ // Compute the barycentric coordinates (u, v, w) using parallelepiped volumes.
++ QVector3D m = QVector3D::crossProduct(QVector3D(0, 1, 0), pp2);
++ float u = QVector3D::dotProduct(pp1, m);
++ float v = -QVector3D::dotProduct(pp0, m);
++ if (!qFuzzyIsNull(u) && !qFuzzyIsNull(v) && !sameSign(u, v))
++ return false;
++ float w = scalarTriple(QVector3D(0, 1, 0), pp1, pp0);
++ if (!qFuzzyIsNull(w) && !sameSign(u, w))
++ return false;
++ float den = 1.0f / (u + v + w);
++ u *= den;
++ v *= den;
++ w *= den;
++ y = p0.y() * u + p1.y() * v + p2.y() * w;
++ return true;
++ }
++ void update(const QVector3D &v1, const QVector3D &v2, const QVector3D &v3)
++ {
++ if (ext.isNull()) {
++ // Update bounds if extents is empty
++ boundsMax.setX(qMax(boundsMax.x(), qMax(v1.x(), qMax(v2.x(), v3.x()))));
++ boundsMax.setY(qMax(boundsMax.y(), qMax(v1.y(), qMax(v2.y(), v3.y()))));
++ boundsMax.setZ(qMax(boundsMax.z(), qMax(v1.z(), qMax(v2.z(), v3.z()))));
++
++ boundsMin.setX(qMin(boundsMin.x(), qMin(v1.x(), qMin(v2.x(), v3.x()))));
++ boundsMin.setY(qMin(boundsMin.y(), qMin(v1.y(), qMin(v2.y(), v3.y()))));
++ boundsMin.setZ(qMin(boundsMin.z(), qMin(v1.z(), qMin(v2.z(), v3.z()))));
++ }
++ rootCell.update(v1, v2, v3);
++ }
++ QList<QVector3D> positions()
++ {
++ float miny = cent.y() - ext.y();
++ if (ext.isNull()) {
++ // If extends was empty, rootCell simply collected all triangles
++ // Now it needs to update subcells
++ if (rootCell.triangles.isEmpty())
++ return {};
++ QVector3D center = (boundsMin + boundsMax) * 0.5;
++ QVector3D extents = (boundsMax - boundsMin) * 0.5;
++ init(extents, center, icp100);
++ rootCell.updateSubcells();
++ miny = center.y() - extents.y();
++ }
++ return rootCell.positions(miny, icp100);
++ }
++ };
++
++ void QQuick3DParticleSceneShape::updateGeometry()
++ {
++ // Only update geometry if the geometry is used
++ static const QMetaMethod geometryChangedSignal = QMetaMethod::fromSignal(&QQuick3DParticleSceneShape::geometryChanged);
++ if (!isSignalConnected(geometryChangedSignal))
++ return;
++
++ if (m_geometry)
++ delete m_geometry;
++ m_geometry = new QQuick3DGeometry();
++ QVector3D boxMin, boxMax;
++ if (m_sceneExtents.isNull()) {
++ boxMin = m_sceneMesh->boundsMin;
++ boxMax = m_sceneMesh->boundsMax;
++ } else {
++ boxMin = m_sceneCenter - m_sceneExtents;
++ boxMax = m_sceneCenter + m_sceneExtents;
++ }
++ m_geometry->setBounds(boxMin, boxMax);
++ QByteArray data((char *)m_sceneData.vertexPositions.data(), m_sceneData.vertexPositions.size() * sizeof(QVector3D));
++ m_geometry->setVertexData(data);
++ m_geometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
++ m_geometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0, QQuick3DGeometry::Attribute::F32Type);
++ m_geometry->setStride(sizeof(float) * 3);
++ emit geometryChanged();
++ }
++
++ void QQuick3DParticleSceneShape::createShapeData()
++ {
++ if (!m_scene)
++ return;
++
++ m_root = m_scene;
++
++ if (m_root) {
++ auto models = m_root->findChildren<QQuick3DModel*>();
++ auto rootModel = qobject_cast<QQuick3DModel*>(m_root);
++ if (rootModel)
++ models.append(rootModel);
++ int index = 0;
++ for (const auto &model : std::as_const(models)) {
++ ShapeData data;
++ if ((model->geometry() != nullptr && model->geometry() == m_geometry))
++ continue;
++ data.model = model;
++ data.sceneTransformConnection = QObject::connect(data.model, &QQuick3DNode::sceneTransformChanged, this, [&,index](){
++ m_data[index].dirty = true;
++ });
++ data.visibilityConnection = QObject::connect(data.model, &QQuick3DNode::visibleChanged, this, [&,index](){
++ m_data[index].dirty = true;
++ });
++ m_data.push_back(std::move(data));
++ index++;
++ }
++ }
++ }
++
++ QVector3D QQuick3DParticleSceneShape::randomPositionModel(int particleIndex)
++ {
++ if (m_root) {
++ calculateModelVertexPositions();
++
++ float totalArea = 0.0;
++ if (m_sceneData.vertexPositions.isEmpty())
++ return {};
++ if (m_sceneData.modelTriangleAreas.size() == 0) {
++ const auto & positions = m_sceneData.vertexPositions;
++ m_sceneData.modelTriangleAreas.reserve(positions.size() / 3);
++ m_sceneData.modelTriangleAreasSum = 0.0f;
++ m_sceneData.modelTriangleCenter = {};
++ for (int i = 0; i + 2 < positions.size(); i += 3) {
++ const QVector3D &v1 = positions[i];
++ const QVector3D &v2 = positions[i + 1];
++ const QVector3D &v3 = positions[i + 2];
++ const float area = QVector3D::crossProduct(v1 - v2, v1 - v3).length() * 0.5f;
++ m_sceneData.modelTriangleAreasSum += area;
++ m_sceneData.modelTriangleAreas.append(m_sceneData.modelTriangleAreasSum);
++ m_sceneData.modelTriangleCenter += v1 + v2 + v3;
++ }
++ m_sceneData.modelTriangleCenter /= positions.size();
++ }
++ totalArea += m_sceneData.modelTriangleAreasSum;
++
++ auto rand = m_system->rand();
++ const float rndWeight = rand->get(particleIndex, QPRand::Shape1) * totalArea;
++ // Use binary search to find the weighted random index
++ int index = std::lower_bound(m_sceneData.modelTriangleAreas.begin(), m_sceneData.modelTriangleAreas.end(), rndWeight) - m_sceneData.modelTriangleAreas.begin();
++
++ const QVector<QVector3D> &positions = m_sceneData.vertexPositions;
++
++ const QVector3D &v1 = positions[index * 3];
++ const QVector3D &v2 = positions[index * 3 + 1];
++ const QVector3D &v3 = positions[index * 3 + 2];
++ const float a = rand->get(particleIndex, QPRand::Shape2);
++ const float b = rand->get(particleIndex, QPRand::Shape3);
++ const float aSqrt = qSqrt(a);
++
++ // Calculate a random point from the selected triangle
++ QVector3D pos = (1.0 - aSqrt) * v1 + (aSqrt * (1.0 - b)) * v2 + (b * aSqrt) * v3;
++ m_cachedIndex = particleIndex;
++ m_cachedNormal = QVector3D::crossProduct(v2 - v1, v3 - v2).normalized();
++
++ auto *parent = parentNode();
++ if (parent) {
++ QMatrix4x4 mat;
++ mat.rotate(parent->rotation());
++ m_cachedNormal = mat.mapVector(m_cachedNormal);
++ return mat.mapVector(pos * parent->sceneScale());
++ }
++
++ }
++ return QVector3D(0, 0, 0);
++ }
++
++ void QQuick3DParticleSceneShape::clearModelVertexPositions()
++ {
++ for (auto &model : m_data) {
++ QObject::disconnect(model.sceneTransformConnection);
++ QObject::disconnect(model.visibilityConnection);
++ }
++ m_data.clear();
++ }
++
++ QList<QVector3D> QQuick3DParticleSceneShape::excludeTriangles(const QList<QVector3D> &list)
++ {
++ QList<QVector3D> ret;
++ QVector3D boxMin, boxMax;
++ boxMin = m_sceneCenter - m_sceneExtents;
++ boxMax = m_sceneCenter + m_sceneExtents;
++ for (int i = 0; i < list.size(); i+=3) {
++ QVector3D v0 = list[i];
++ QVector3D v1 = list[i + 1];
++ QVector3D v2 = list[i + 2];
++ QVector3D normal = QVector3D::crossProduct(v1 - v0, v2 - v1).normalized();
++ // The normal must point upwards
++ if (normal.y() < 0.0f)
++ continue;
++ if (!m_sceneExtents.isNull()) {
++ // Check if all v0-v2 are outside scene extents and don't add them if they are
++ if (!(qAbs(v0.x() - m_sceneCenter.x()) < m_sceneExtents.x() &&
++ qAbs(v0.y() - m_sceneCenter.y()) < m_sceneExtents.y() &&
++ qAbs(v0.z() - m_sceneCenter.z()) < m_sceneExtents.z() &&
++ qAbs(v1.x() - m_sceneCenter.x()) < m_sceneExtents.x() &&
++ qAbs(v1.y() - m_sceneCenter.y()) < m_sceneExtents.y() &&
++ qAbs(v1.z() - m_sceneCenter.z()) < m_sceneExtents.z() &&
++ qAbs(v2.x() - m_sceneCenter.x()) < m_sceneExtents.x() &&
++ qAbs(v2.y() - m_sceneCenter.y()) < m_sceneExtents.y() &&
++ qAbs(v2.z() - m_sceneCenter.z()) < m_sceneExtents.z())) {
++ // If so make sure they are not crossing the extents
++ if (v0.x() < boxMin.x() && v1.x() < boxMin.x() && v2.x() < boxMin.x())
++ continue;
++ if (v0.x() > boxMax.x() && v1.x() > boxMax.x() && v2.x() > boxMax.x())
++ continue;
++ if (v0.y() < boxMin.y() && v1.y() < boxMin.y() && v2.y() < boxMin.y())
++ continue;
++ if (v0.y() > boxMax.y() && v1.y() > boxMax.y() && v2.y() > boxMax.y())
++ continue;
++ if (v0.z() < boxMin.z() && v1.z() < boxMin.z() && v2.z() < boxMin.z())
++ continue;
++ if (v0.z() > boxMax.z() && v1.z() > boxMax.z() && v2.z() > boxMax.z())
++ continue;
++ }
++ }
++ ret.append(v0);
++ ret.append(v1);
++ ret.append(v2);
++ }
++ return ret;
++ }
++
++ bool QQuick3DParticleSceneShape::isVisibleRecursive(QQuick3DNode *node)
++ {
++ while (node) {
++ if (!node->visible())
++ return false;
++ node = node->parentNode();
++ }
++ return true;
++ }
++
++ bool QQuick3DParticleSceneShape::isExcluded(QQuick3DModel *model)
++ {
++ // Check if model parent is in the excluded list
++ for (const QQuick3DNode *node : std::as_const(m_excludeList)) {
++ QQuick3DNode *m = model;
++ while (m) {
++ if (m == node)
++ return true;
++ m = m->parentNode();
++ }
++ }
++ return false;
++ }
++
++ void QQuick3DParticleSceneShape::calculateModelVertexPositions()
++ {
++ bool update = false;
++ for (const auto &modelData : std::as_const(m_data)) {
++ update |= modelData.dirty || modelData.excludedDirty;
++ if (update)
++ break;
++ }
++ if (update) {
++ if (m_sceneMesh) {
++ delete m_sceneMesh;
++ m_sceneData.clear();
++ }
++ m_sceneMesh = new QQuick3DParticleSceneShape::SceneNapkinMesh();
++ m_sceneMesh->init(m_sceneExtents, m_sceneCenter, m_shapeResolution / 10.0f);
++ } else {
++ return;
++ }
++
++ for (auto &modelData : m_data) {
++ if (modelData.excludedDirty) {
++ modelData.excludedDirty = false;
++ modelData.excluded = isExcluded(modelData.model);
++ }
++ modelData.visible = isVisibleRecursive(modelData.model);
++ if (!modelData.visible || modelData.excluded) {
++ modelData.dirty = false;
++ continue;
++ }
++ if (modelData.dirty || modelData.vertexPositions.empty()) {
++ const QMatrix4x4 mat = modelData.model->sceneTransform();
++ modelData.vertexPositions = excludeTriangles(positionsFromModel(modelData.model, &mat, qmlContext(this)));
++ modelData.hasVertices = !modelData.vertexPositions.isEmpty();
++ modelData.modelTriangleAreas.clear();
++ }
++ modelData.dirty = false;
++ if (modelData.hasVertices && modelData.visible) {
++ int count = modelData.vertexPositions.size();
++ for (int k = 0; k < count; k+=3)
++ m_sceneMesh->update(modelData.vertexPositions[k], modelData.vertexPositions[k+1], modelData.vertexPositions[k+2]);
++ }
++ }
++ if (update) {
++ m_sceneData.vertexPositions = m_sceneMesh->positions();
++ updateGeometry();
++ }
++ }
++
++ QT_END_NAMESPACE