Revision control

Copy as Markdown

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qquick3dparticlemodelshape_p.h"
#include "qquick3dparticlerandomizer_p.h"
#include "qquick3dparticlesystem_p.h"
#include "qquick3dparticleutils_p.h"
#include <QtQml/qqmlfile.h>
#include <QtQuick3D/private/qquick3dmodel_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
#include <algorithm>
QT_BEGIN_NAMESPACE
/*!
\qmltype ParticleModelShape3D
\inherits ParticleAbstractShape3D
\inqmlmodule QtQuick3D.Particles3D
\brief Offers particle shape from model for emitters and affectors.
\since 6.2
The ParticleModelShape3D element can be used to get particle shape from a 3D model.
For example, to emit particles from outlines of a model shape:
\qml
Component {
id: suzanneComponent
Model {
source: "meshes/suzanne.mesh"
scale: Qt.vector3d(100, 100, 100)
}
}
ParticleEmitter3D {
shape: ParticleModelShape3D {
model: suzanneComponent
fill: false
}
...
}
\endqml
*/
QQuick3DParticleModelShape::QQuick3DParticleModelShape(QObject *parent)
: QQuick3DParticleAbstractShape(parent)
{
}
QQuick3DParticleModelShape::~QQuick3DParticleModelShape()
{
delete m_model;
}
/*!
\qmlproperty bool ParticleModelShape3D::fill
This property defines if the shape should be filled or just use the shape outlines.
The default value is \c true.
*/
bool QQuick3DParticleModelShape::fill() const
{
return m_fill;
}
/*!
\qmlproperty Component ParticleModelShape3D::delegate
The delegate provides a template defining the model for the ParticleModelShape3D.
For example, using the default sphere model with default material
\qml
Component {
id: modelComponent
Model {
source: "#Sphere"
scale: Qt.vector3d(0.5, 0.5, 0.5)
materials: DefaultMaterial { diffuseColor: "red" }
}
}
ParticleModelShape3D {
delegate: modelComponent
}
\endqml
*/
QQmlComponent *QQuick3DParticleModelShape::delegate() const
{
return m_delegate;
}
void QQuick3DParticleModelShape::setFill(bool fill)
{
if (m_fill == fill)
return;
m_fill = fill;
Q_EMIT fillChanged();
}
QVector3D QQuick3DParticleModelShape::getPosition(int particleIndex)
{
return randomPositionModel(particleIndex);
}
QVector3D QQuick3DParticleModelShape::getSurfaceNormal(int particleIndex)
{
if (m_fill)
return QVector3D();
if (m_cachedIndex != particleIndex)
getPosition(particleIndex);
return m_cachedNormal;
}
void QQuick3DParticleModelShape::setDelegate(QQmlComponent *delegate)
{
if (delegate == m_delegate)
return;
m_delegate = delegate;
clearModelVertexPositions();
createModel();
Q_EMIT delegateChanged();
}
void QQuick3DParticleModelShape::createModel()
{
delete m_model;
m_model = nullptr;
if (!m_delegate)
return;
auto *obj = m_delegate->create(m_delegate->creationContext());
m_model = qobject_cast<QQuick3DModel *>(obj);
if (!m_model)
delete obj;
}
QVector3D QQuick3DParticleModelShape::randomPositionModel(int particleIndex)
{
if (m_model) {
calculateModelVertexPositions();
const QVector<QVector3D> &positions = m_vertexPositions;
if (positions.size() > 0) {
auto rand = m_system->rand();
// Calculate model triangle areas so that the random triangle selection can be weighted
// by the area. This way particles are uniformly emitted from the whole model.
if (m_modelTriangleAreas.size() == 0) {
m_modelTriangleAreas.reserve(positions.size() / 3);
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_modelTriangleAreasSum += area;
m_modelTriangleAreas.append(m_modelTriangleAreasSum);
m_modelTriangleCenter += v1 + v2 + v3;
}
m_modelTriangleCenter /= positions.size();
}
const float rndWeight = rand->get(particleIndex, QPRand::Shape1) * m_modelTriangleAreasSum;
// Use binary search to find the weighted random index
int index = std::lower_bound(m_modelTriangleAreas.begin(), m_modelTriangleAreas.end(), rndWeight) - m_modelTriangleAreas.begin();
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;
if (m_fill) {
// The model is filled by selecting a random point between a random surface point
// and the center of the model. The random point selection is exponentially weighted
// towards the surface so that particles aren't clustered in the center.
const float uniform = rand->get(particleIndex, QPRand::Shape4);
const float lambda = 5.0f;
const float alpha = -qLn(1 - (1 - qExp(-lambda)) * uniform) / lambda;
pos += (m_modelTriangleCenter - pos) * alpha;
} else {
m_cachedIndex = particleIndex;
m_cachedNormal = QVector3D::crossProduct(v2 - v1, v3 - v2).normalized();
}
auto *parent = parentNode();
if (parent) {
QMatrix4x4 mat;
mat.rotate(parent->rotation() * m_model->rotation());
if (!m_fill)
m_cachedNormal = mat.mapVector(m_cachedNormal);
return mat.mapVector(pos * parent->sceneScale() * m_model->scale());
}
}
}
return QVector3D(0, 0, 0);
}
void QQuick3DParticleModelShape::clearModelVertexPositions()
{
m_vertexPositions.clear();
m_modelTriangleAreas.clear();
m_modelTriangleAreasSum = 0;
}
void QQuick3DParticleModelShape::calculateModelVertexPositions()
{
if (m_vertexPositions.empty())
m_vertexPositions = positionsFromModel(m_model, nullptr, qmlContext(this));
}
QT_END_NAMESPACE