1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qquick3dparticlemodelshape_p.h"
5#include "qquick3dparticlerandomizer_p.h"
6#include "qquick3dparticlesystem_p.h"
7#include <QtCore/qdir.h>
8#include <QtQml/qqmlfile.h>
9#include <QtQuick3D/private/qquick3dmodel_p.h>
10#include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
11#include <algorithm>
12
13QT_BEGIN_NAMESPACE
14
15/*!
16 \qmltype ParticleModelShape3D
17 \inherits ParticleAbtractShape3D
18 \inqmlmodule QtQuick3D.Particles3D
19 \brief Offers particle shape from model for emitters and affectors.
20 \since 6.2
21
22 The ParticleModelShape3D element can be used to get particle shape from a 3D model.
23
24 For example, to emit particles from outlines of a model shape:
25
26 \qml
27 Component {
28 id: suzanneComponent
29 Model {
30 source: "meshes/suzanne.mesh"
31 scale: Qt.vector3d(100, 100, 100)
32 }
33 }
34
35 ParticleEmitter3D {
36 shape: ParticleModelShape3D {
37 model: suzanneComponent
38 fill: false
39 }
40 ...
41 }
42 \endqml
43*/
44
45QQuick3DParticleModelShape::QQuick3DParticleModelShape(QObject *parent)
46 : QQuick3DParticleAbstractShape(parent)
47{
48
49}
50
51QQuick3DParticleModelShape::~QQuick3DParticleModelShape()
52{
53 delete m_model;
54}
55
56/*!
57 \qmlproperty bool ParticleModelShape3D::fill
58
59 This property defines if the shape should be filled or just use the shape outlines.
60
61 The default value is \c true.
62*/
63bool QQuick3DParticleModelShape::fill() const
64{
65 return m_fill;
66}
67
68/*!
69 \qmlproperty Component ParticleModelShape3D::delegate
70 The delegate provides a template defining the model for the ParticleModelShape3D.
71 For example, using the default sphere model with default material
72 \qml
73 Component {
74 id: modelComponent
75 Model {
76 source: "#Sphere"
77 scale: Qt.vector3d(0.5, 0.5, 0.5)
78 materials: DefaultMaterial { diffuseColor: "red" }
79 }
80 }
81 ParticleModelShape3D {
82 delegate: modelComponent
83 }
84 \endqml
85*/
86QQmlComponent *QQuick3DParticleModelShape::delegate() const
87{
88 return m_delegate;
89}
90
91void QQuick3DParticleModelShape::setFill(bool fill)
92{
93 if (m_fill == fill)
94 return;
95
96 m_fill = fill;
97 Q_EMIT fillChanged();
98}
99
100QVector3D QQuick3DParticleModelShape::getPosition(int particleIndex)
101{
102 return randomPositionModel(particleIndex);
103}
104
105static QSSGMesh::Mesh loadModelShapeMesh(const QString &source)
106{
107 QString src = source;
108 if (source.startsWith(c: QLatin1Char('#'))) {
109 src = QSSGBufferManager::primitivePath(primitive: source);
110 src.prepend(s: QLatin1String(":/"));
111 }
112 src = QDir::cleanPath(path: src);
113 if (src.startsWith(s: QLatin1String("qrc:/")))
114 src = src.mid(position: 3);
115 QSSGMesh::Mesh mesh;
116 QFileInfo fileInfo = QFileInfo(src);
117 if (fileInfo.exists()) {
118 QFile file(fileInfo.absoluteFilePath());
119 if (!file.open(flags: QFile::ReadOnly))
120 return {};
121 mesh = QSSGMesh::Mesh::loadMesh(device: &file);
122 }
123 return mesh;
124}
125
126void QQuick3DParticleModelShape::setDelegate(QQmlComponent *delegate)
127{
128 if (delegate == m_delegate)
129 return;
130 m_delegate = delegate;
131 clearModelVertexPositions();
132 createModel();
133 Q_EMIT delegateChanged();
134}
135
136void QQuick3DParticleModelShape::createModel()
137{
138 delete m_model;
139 m_model = nullptr;
140 if (!m_delegate)
141 return;
142 auto *obj = m_delegate->create(context: m_delegate->creationContext());
143 m_model = qobject_cast<QQuick3DModel *>(object: obj);
144 if (!m_model)
145 delete obj;
146}
147
148QVector3D QQuick3DParticleModelShape::randomPositionModel(int particleIndex)
149{
150 if (m_model) {
151 calculateModelVertexPositions();
152
153 const QVector<QVector3D> &positions = m_vertexPositions;
154 if (positions.size() > 0) {
155 auto rand = m_system->rand();
156
157 // Calculate model triangle areas so that the random triangle selection can be weighted
158 // by the area. This way particles are uniformly emitted from the whole model.
159 if (m_modelTriangleAreas.size() == 0) {
160 m_modelTriangleAreas.reserve(asize: positions.size() / 3);
161 for (int i = 0; i + 2 < positions.size(); i += 3) {
162 const QVector3D &v1 = positions[i];
163 const QVector3D &v2 = positions[i + 1];
164 const QVector3D &v3 = positions[i + 2];
165 const float area = QVector3D::crossProduct(v1: v1 - v2, v2: v1 - v3).length() * 0.5f;
166 m_modelTriangleAreasSum += area;
167 m_modelTriangleAreas.append(t: m_modelTriangleAreasSum);
168 m_modelTriangleCenter += v1 + v2 + v3;
169 }
170 m_modelTriangleCenter /= positions.size();
171 }
172
173 const float rndWeight = rand->get(particleIndex, user: QPRand::Shape1) * m_modelTriangleAreasSum;
174
175 // Use binary search to find the weighted random index
176 int index = std::lower_bound(first: m_modelTriangleAreas.begin(), last: m_modelTriangleAreas.end(), val: rndWeight) - m_modelTriangleAreas.begin();
177
178 const QVector3D &v1 = positions[index * 3];
179 const QVector3D &v2 = positions[index * 3 + 1];
180 const QVector3D &v3 = positions[index * 3 + 2];
181 const float a = rand->get(particleIndex, user: QPRand::Shape2);
182 const float b = rand->get(particleIndex, user: QPRand::Shape3);
183 const float aSqrt = qSqrt(v: a);
184
185 // Calculate a random point from the selected triangle
186 QVector3D pos = (1.0 - aSqrt) * v1 + (aSqrt * (1.0 - b)) * v2 + (b * aSqrt) * v3;
187
188 if (m_fill) {
189 // The model is filled by selecting a random point between a random surface point
190 // and the center of the model. The random point selection is exponentially weighted
191 // towards the surface so that particles aren't clustered in the center.
192 const float uniform = rand->get(particleIndex, user: QPRand::Shape4);
193 const float lambda = 5.0f;
194 const float alpha = -qLn(v: 1 - (1 - qExp(v: -lambda)) * uniform) / lambda;
195 pos += (m_modelTriangleCenter - pos) * alpha;
196 }
197
198 auto *parent = parentNode();
199 if (parent) {
200 QMatrix4x4 mat;
201 mat.rotate(quaternion: parent->rotation() * m_model->rotation());
202 return mat.mapVector(vector: pos * parent->sceneScale() * m_model->scale());
203 }
204 }
205 }
206 return QVector3D(0, 0, 0);
207}
208
209void QQuick3DParticleModelShape::clearModelVertexPositions()
210{
211 m_vertexPositions.clear();
212 m_modelTriangleAreas.clear();
213 m_modelTriangleAreasSum = 0;
214}
215
216void QQuick3DParticleModelShape::calculateModelVertexPositions()
217{
218 if (m_vertexPositions.empty()) {
219 QVector<QVector3D> indicedPositions;
220 QVector<QVector3D> positions;
221
222 if (m_model->geometry()) {
223 QQuick3DGeometry *geometry = m_model->geometry();
224 bool hasIndexBuffer = false;
225 QQuick3DGeometry::Attribute::ComponentType indexBufferFormat;
226 int posOffset = 0;
227 QQuick3DGeometry::Attribute::ComponentType posType = QQuick3DGeometry::Attribute::U16Type;
228 for (int i = 0; i < geometry->attributeCount(); ++i) {
229 auto attribute = geometry->attribute(index: i);
230 if (attribute.semantic == QQuick3DGeometry::Attribute::PositionSemantic) {
231 posOffset = attribute.offset;
232 posType = attribute.componentType;
233 } else if (attribute.semantic == QQuick3DGeometry::Attribute::IndexSemantic) {
234 hasIndexBuffer = true;
235 indexBufferFormat = attribute.componentType;
236 }
237 }
238 if (posType == QQuick3DGeometry::Attribute::F32Type) {
239 const auto &data = geometry->vertexData();
240 int stride = geometry->stride();
241 for (int i = 0; i < data.size(); i += stride) {
242 float v[3];
243 memcpy(dest: v, src: data + posOffset + i, n: sizeof(v));
244 positions.append(t: QVector3D(v[0], v[1], v[2]));
245 }
246 if (hasIndexBuffer) {
247 const auto &data = geometry->vertexData();
248 int indexSize = 4;
249 if (indexBufferFormat == QQuick3DGeometry::Attribute::U16Type)
250 indexSize = 2;
251 for (int i = 0; i < data.size(); i += indexSize) {
252 qsizetype index = 0;
253 memcpy(dest: &index, src: data + i, n: indexSize);
254 if (positions.size() > index)
255 indicedPositions.append(t: positions[index]);
256 }
257 }
258 }
259 } else {
260 const QQmlContext *context = qmlContext(this);
261 QString src = m_model->source().toString();
262 if (context && !src.startsWith(c: QLatin1Char('#')))
263 src = QQmlFile::urlToLocalFileOrQrc(context->resolvedUrl(m_model->source()));
264 QSSGMesh::Mesh mesh = loadModelShapeMesh(source: src);
265 if (!mesh.isValid())
266 return;
267 if (mesh.drawMode() != QSSGMesh::Mesh::DrawMode::Triangles)
268 return;
269
270 auto entries = mesh.vertexBuffer().entries;
271 int posOffset = 0;
272 int posCount = 0;
273 // Just set 'posType' to something to avoid invalid 'maybe-uninitialized' warning
274 QSSGMesh::Mesh::ComponentType posType = QSSGMesh::Mesh::ComponentType::UnsignedInt8;
275 for (int i = 0; i < entries.size(); ++i) {
276 const char *nameStr = entries[i].name.constData();
277 if (!strcmp(s1: nameStr, s2: QSSGMesh::MeshInternal::getPositionAttrName())) {
278 posOffset = entries[i].offset;
279 posCount = entries[i].componentCount;
280 posType = entries[i].componentType;
281 break;
282 }
283 }
284 if (posCount == 3 && posType == QSSGMesh::Mesh::ComponentType::Float32) {
285 const auto &data = mesh.vertexBuffer().data;
286 int stride = mesh.vertexBuffer().stride;
287 for (int i = 0; i < data.size(); i += stride) {
288 float v[3];
289 memcpy(dest: v, src: data + posOffset + i, n: sizeof(v));
290 positions.append(t: QVector3D(v[0], v[1], v[2]));
291 }
292 const auto &indexData = mesh.indexBuffer().data;
293 int indexSize = QSSGMesh::MeshInternal::byteSizeForComponentType(componentType: mesh.indexBuffer().componentType);
294 for (int i = 0; i < indexData.size(); i += indexSize) {
295 qsizetype index = 0;
296 memcpy(dest: &index, src: indexData + i, n: indexSize);
297 if (positions.size() > index)
298 indicedPositions.append(t: positions[index]);
299 }
300 }
301 }
302 if (!indicedPositions.empty())
303 m_vertexPositions = indicedPositions;
304 else
305 m_vertexPositions = positions;
306 }
307}
308
309QT_END_NAMESPACE
310

source code of qtquick3d/src/quick3dparticles/qquick3dparticlemodelshape.cpp