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 | |
13 | QT_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 | |
45 | QQuick3DParticleModelShape::QQuick3DParticleModelShape(QObject *parent) |
46 | : QQuick3DParticleAbstractShape(parent) |
47 | { |
48 | |
49 | } |
50 | |
51 | QQuick3DParticleModelShape::~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 | */ |
63 | bool 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 | */ |
86 | QQmlComponent *QQuick3DParticleModelShape::delegate() const |
87 | { |
88 | return m_delegate; |
89 | } |
90 | |
91 | void QQuick3DParticleModelShape::setFill(bool fill) |
92 | { |
93 | if (m_fill == fill) |
94 | return; |
95 | |
96 | m_fill = fill; |
97 | Q_EMIT fillChanged(); |
98 | } |
99 | |
100 | QVector3D QQuick3DParticleModelShape::getPosition(int particleIndex) |
101 | { |
102 | return randomPositionModel(particleIndex); |
103 | } |
104 | |
105 | static 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 | |
126 | void 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 | |
136 | void 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 | |
148 | QVector3D 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 | |
209 | void QQuick3DParticleModelShape::clearModelVertexPositions() |
210 | { |
211 | m_vertexPositions.clear(); |
212 | m_modelTriangleAreas.clear(); |
213 | m_modelTriangleAreasSum = 0; |
214 | } |
215 | |
216 | void 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 | |
309 | QT_END_NAMESPACE |
310 | |