1 | // Copyright (C) 2023 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "proceduralmesh_p.h" |
5 | |
6 | #include <QtQuick3D/private/qquick3dobject_p.h> |
7 | |
8 | #include <QtQuick/QQuickWindow> |
9 | |
10 | #include <rhi/qrhi.h> |
11 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | /*! |
15 | \qmltype ProceduralMesh |
16 | \inqmlmodule QtQuick3D.Helpers |
17 | \inherits Geometry |
18 | \brief Allows creation of Geometry from QML. |
19 | \since 6.6 |
20 | |
21 | ProceduralMesh is a helper type that allows creation of Geometry instances |
22 | from QML. The Geometry component is Abstract, and is usually created |
23 | from C++. |
24 | |
25 | \qml |
26 | component TorusMesh : ProceduralMesh { |
27 | property real rings: 50 |
28 | property real segments: 50 |
29 | property real radius: 100.0 |
30 | property real tubeRadius: 10.0 |
31 | property var meshArrays: generateTorus(rings, segments, radius, tubeRadius) |
32 | positions: meshArrays.verts |
33 | normals: meshArrays.normals |
34 | uv0s: meshArrays.uvs |
35 | indexes: meshArrays.indices |
36 | |
37 | function generateTorus(rings: real, segments: real, radius: real, tubeRadius: real) { |
38 | let verts = [] |
39 | let normals = [] |
40 | let uvs = [] |
41 | let indices = [] |
42 | |
43 | for (let i = 0; i <= rings; ++i) { |
44 | for (let j = 0; j <= segments; ++j) { |
45 | let u = i / rings * Math.PI * 2; |
46 | let v = j / segments * Math.PI * 2; |
47 | |
48 | let centerX = radius * Math.cos(u); |
49 | let centerZ = radius * Math.sin(u); |
50 | |
51 | let posX = centerX + tubeRadius * Math.cos(v) * Math.cos(u); |
52 | let posY = tubeRadius * Math.sin(v); |
53 | let posZ = centerZ + tubeRadius * Math.cos(v) * Math.sin(u); |
54 | |
55 | verts.push(Qt.vector3d(posX, posY, posZ)); |
56 | |
57 | let normal = Qt.vector3d(posX - centerX, posY, posZ - centerZ).normalized(); |
58 | normals.push(normal); |
59 | |
60 | uvs.push(Qt.vector2d(i / rings, j / segments)); |
61 | } |
62 | } |
63 | |
64 | for (let i = 0; i < rings; ++i) { |
65 | for (let j = 0; j < segments; ++j) { |
66 | let a = (segments + 1) * i + j; |
67 | let b = (segments + 1) * (i + 1) + j; |
68 | let c = (segments + 1) * (i + 1) + j + 1; |
69 | let d = (segments + 1) * i + j + 1; |
70 | |
71 | // Generate two triangles for each quad in the mesh |
72 | // Adjust order to be counter-clockwise |
73 | indices.push(a, d, b); |
74 | indices.push(b, d, c); |
75 | } |
76 | } |
77 | return { verts: verts, normals: normals, uvs: uvs, indices: indices } |
78 | } |
79 | } |
80 | \endqml |
81 | |
82 | The above code defines a component TorusMesh that can be used as Geometry for use |
83 | with a Model component. When the ring, segments, radius or tubeRadius properties |
84 | are modified the geometry will be updated. |
85 | |
86 | The ProceduralMesh component is not as flexible nor as performant as creating |
87 | Geometry in C++, but makes up for it in convenience and simplicity. The |
88 | properties are fixed attribute lists that when filled will automatically |
89 | generate the necessary buffers. |
90 | |
91 | */ |
92 | |
93 | /*! |
94 | \qmlproperty List<QVector3D> ProceduralMesh::positions |
95 | The positions attribute list. If this list remains empty nothing no geometry |
96 | will be generated. |
97 | */ |
98 | |
99 | /*! |
100 | \qmlproperty List<QVector3D> ProceduralMesh::normals |
101 | Holds the normals attribute list. |
102 | */ |
103 | |
104 | /*! |
105 | \qmlproperty List<QVector3D> ProceduralMesh::tangents |
106 | Holds the tangents attribute list. |
107 | */ |
108 | |
109 | /*! |
110 | \qmlproperty List<QVector3D> ProceduralMesh::binormals |
111 | Holds the binormals attribute list. |
112 | */ |
113 | |
114 | /*! |
115 | \qmlproperty List<QVector2D> ProceduralMesh::uv0s |
116 | This property defines a list of uv coordinates for the first uv channel (uv0) |
117 | */ |
118 | |
119 | /*! |
120 | \qmlproperty List<QVector2D> ProceduralMesh::uv1s |
121 | This property defines a list of uv coordinates for the second uv channel (uv1) |
122 | */ |
123 | |
124 | /*! |
125 | \qmlproperty List<QVector4D> ProceduralMesh::colors |
126 | This property defines a list of vertex color values. |
127 | */ |
128 | |
129 | /*! |
130 | \qmlproperty List<QVector4D> ProceduralMesh::joints |
131 | This property defines a list of joint indices for skinning. |
132 | */ |
133 | |
134 | /*! |
135 | \qmlproperty List<QVector4D> ProceduralMesh::weights |
136 | This property defines a list of joint weights for skinning. |
137 | */ |
138 | |
139 | /*! |
140 | \qmlproperty List<int> ProceduralMesh::indexes |
141 | This property defines a list of indexes into the attribute lists. If this list remains empty |
142 | the vertex buffer values will be used directly. |
143 | */ |
144 | |
145 | /*! |
146 | \qmlproperty enumeration ProceduralMesh::primitiveMode |
147 | |
148 | This property defines the primitive mode to use when rendering the geometry. |
149 | |
150 | \value ProceduralMesh.Points The points primitive mode is used. |
151 | \value ProceduralMesh.LineStrip The line strip primitive mode is used. |
152 | \value ProceduralMesh.Lines The lines primitive mode is used. |
153 | \value ProceduralMesh.TriangleStrip The triangles strip primitive mode is |
154 | used. |
155 | \value ProceduralMesh.TriangleFan The triangle fan primitive mode is used. |
156 | \value ProceduralMesh.Triangles The triangles primitive mode is used. |
157 | \default ProceduralMesh.Triangles |
158 | |
159 | \note Not all modes are supported on all rendering backends. |
160 | */ |
161 | |
162 | /*! |
163 | \qmlproperty List<ProceduralMeshSubset> ProceduralMesh::subsets |
164 | |
165 | This property defines a list of subsets to split the geometry data into. |
166 | Each subset can have it's own material. The order of this array |
167 | corresponds to the materials list of Model when using this geometry. |
168 | |
169 | This property is optional and when empty results in a single subset. |
170 | |
171 | \note Any subset that specifies values outside of the range of available |
172 | vertex/index values will lead to that subset being ignored. |
173 | */ |
174 | |
175 | /*! |
176 | \qmltype ProceduralMeshSubset |
177 | \inqmlmodule QtQuick3D.Helpers |
178 | \inherits QtObject |
179 | \brief Defines a subset of a ProceduralMesh. |
180 | \since 6.6 |
181 | |
182 | This type defines a subset of a ProceduralMesh. Each subset can have it's own |
183 | material and can be used to split the geometry into multiple draw calls. |
184 | |
185 | \sa ProceduralMesh::subsets |
186 | |
187 | */ |
188 | |
189 | /*! |
190 | \qmlproperty int ProceduralMeshSubset::offset |
191 | This property defines the starting index for this subset. \default 0 |
192 | */ |
193 | |
194 | /*! |
195 | \qmlproperty int ProceduralMeshSubset::count |
196 | This property defines the number of indices to use for this subset. This property must be set for the subset to have content. |
197 | |
198 | \default 0 |
199 | */ |
200 | |
201 | /*! |
202 | \qmlproperty Material ProceduralMeshSubset::name |
203 | This property defines a name of the subset. This property is optional, and is only used to |
204 | tag the subset for debugging purposes. |
205 | */ |
206 | |
207 | ProceduralMesh::ProceduralMesh() |
208 | { |
209 | |
210 | } |
211 | |
212 | QList<QVector3D> ProceduralMesh::positions() const |
213 | { |
214 | return m_positions; |
215 | } |
216 | |
217 | void ProceduralMesh::setPositions(const QList<QVector3D> &newPositions) |
218 | { |
219 | if (m_positions == newPositions) |
220 | return; |
221 | m_positions = newPositions; |
222 | Q_EMIT positionsChanged(); |
223 | requestUpdate(); |
224 | } |
225 | |
226 | ProceduralMesh::PrimitiveMode ProceduralMesh::primitiveMode() const |
227 | { |
228 | return m_primitiveMode; |
229 | } |
230 | |
231 | void ProceduralMesh::setPrimitiveMode(PrimitiveMode newPrimitiveMode) |
232 | { |
233 | if (m_primitiveMode == newPrimitiveMode) |
234 | return; |
235 | |
236 | // Do some sanity checking |
237 | if (newPrimitiveMode < Points || newPrimitiveMode > Triangles) { |
238 | qWarning() << "Invalid primitive mode specified" ; |
239 | return; |
240 | } |
241 | |
242 | if (newPrimitiveMode == PrimitiveMode::TriangleFan) { |
243 | if (!supportsTriangleFanPrimitive()) { |
244 | qWarning() << "TriangleFan is not supported by the current backend" ; |
245 | return; |
246 | } |
247 | } |
248 | |
249 | m_primitiveMode = newPrimitiveMode; |
250 | Q_EMIT primitiveModeChanged(); |
251 | requestUpdate(); |
252 | } |
253 | |
254 | void ProceduralMesh::requestUpdate() |
255 | { |
256 | if (!m_updateRequested) { |
257 | QMetaObject::invokeMethod(obj: this, member: "updateGeometry" , c: Qt::QueuedConnection); |
258 | m_updateRequested = true; |
259 | } |
260 | } |
261 | |
262 | void ProceduralMesh::updateGeometry() |
263 | { |
264 | m_updateRequested = false; |
265 | // reset the geometry |
266 | clear(); |
267 | |
268 | setPrimitiveType(PrimitiveType(m_primitiveMode)); |
269 | |
270 | // Figure out which attributes are being used |
271 | const auto expectedLength = m_positions.size(); |
272 | bool hasPositions = !m_positions.isEmpty(); |
273 | if (!hasPositions) { |
274 | setStride(0); |
275 | update(); |
276 | return; // If there are no positions, there is no point :-) |
277 | } |
278 | bool hasNormals = m_normals.size() >= expectedLength; |
279 | bool hasTangents = m_tangents.size() >= expectedLength; |
280 | bool hasBinormals = m_binormals.size() >= expectedLength; |
281 | bool hasUV0s = m_uv0s.size() >= expectedLength; |
282 | bool hasUV1s = m_uv1s.size() >= expectedLength; |
283 | bool hasColors = m_colors.size() >= expectedLength; |
284 | bool hasJoints = m_joints.size() >= expectedLength; |
285 | bool hasWeights = m_weights.size() >= expectedLength; |
286 | bool hasIndexes = !m_indexes.isEmpty(); |
287 | |
288 | int offset = 0; |
289 | if (hasPositions) { |
290 | addAttribute(semantic: Attribute::Semantic::PositionSemantic, offset, componentType: Attribute::ComponentType::F32Type); |
291 | offset += 3 * sizeof(float); |
292 | } |
293 | |
294 | if (hasNormals) { |
295 | addAttribute(semantic: Attribute::Semantic::NormalSemantic, offset, componentType: Attribute::ComponentType::F32Type); |
296 | offset += 3 * sizeof(float); |
297 | } |
298 | |
299 | if (hasTangents) { |
300 | addAttribute(semantic: Attribute::Semantic::TangentSemantic, offset, componentType: Attribute::ComponentType::F32Type); |
301 | offset += 3 * sizeof(float); |
302 | } |
303 | |
304 | if (hasBinormals) { |
305 | addAttribute(semantic: Attribute::Semantic::BinormalSemantic, offset, componentType: Attribute::ComponentType::F32Type); |
306 | offset += 3 * sizeof(float); |
307 | } |
308 | |
309 | if (hasUV0s) { |
310 | addAttribute(semantic: Attribute::Semantic::TexCoord0Semantic, offset, componentType: Attribute::ComponentType::F32Type); |
311 | offset += 2 * sizeof(float); |
312 | } |
313 | |
314 | if (hasUV1s) { |
315 | addAttribute(semantic: Attribute::Semantic::TexCoord1Semantic, offset, componentType: Attribute::ComponentType::F32Type); |
316 | offset += 2 * sizeof(float); |
317 | } |
318 | |
319 | if (hasColors) { |
320 | addAttribute(semantic: Attribute::Semantic::ColorSemantic, offset, componentType: Attribute::ComponentType::F32Type); |
321 | offset += 4 * sizeof(float); |
322 | } |
323 | |
324 | if (hasJoints) { |
325 | addAttribute(semantic: Attribute::Semantic::JointSemantic, offset, componentType: Attribute::ComponentType::F32Type); |
326 | offset += 4 * sizeof(float); |
327 | } |
328 | |
329 | if (hasWeights) { |
330 | addAttribute(semantic: Attribute::Semantic::WeightSemantic, offset, componentType: Attribute::ComponentType::F32Type); |
331 | offset += 4 * sizeof(float); |
332 | } |
333 | |
334 | if (hasIndexes) |
335 | addAttribute(semantic: Attribute::Semantic::IndexSemantic, offset: 0, componentType: Attribute::ComponentType::U32Type); |
336 | |
337 | // Set up the vertex buffer |
338 | const int stride = offset; |
339 | const qsizetype bufferSize = expectedLength * stride; |
340 | setStride(stride); |
341 | |
342 | QVector<float> vertexBufferData; |
343 | vertexBufferData.reserve(asize: bufferSize / sizeof(float)); |
344 | |
345 | QVector3D minBounds; |
346 | QVector3D maxBounds; |
347 | |
348 | for (qsizetype i = 0; i < expectedLength; ++i) { |
349 | // start writing float values to vertexBuffer |
350 | if (hasPositions) { |
351 | const auto &position = m_positions[i]; |
352 | vertexBufferData.append(t: position.x()); |
353 | vertexBufferData.append(t: position.y()); |
354 | vertexBufferData.append(t: position.z()); |
355 | minBounds.setX(qMin(a: minBounds.x(), b: position.x())); |
356 | maxBounds.setX(qMax(a: maxBounds.x(), b: position.x())); |
357 | minBounds.setY(qMin(a: minBounds.y(), b: position.y())); |
358 | maxBounds.setY(qMax(a: maxBounds.y(), b: position.y())); |
359 | minBounds.setZ(qMin(a: minBounds.z(), b: position.z())); |
360 | maxBounds.setZ(qMax(a: maxBounds.z(), b: position.z())); |
361 | } |
362 | if (hasNormals) { |
363 | const auto &normal = m_normals[i]; |
364 | vertexBufferData.append(t: normal.x()); |
365 | vertexBufferData.append(t: normal.y()); |
366 | vertexBufferData.append(t: normal.z()); |
367 | } |
368 | |
369 | if (hasBinormals) { |
370 | const auto &binormal = m_binormals[i]; |
371 | vertexBufferData.append(t: binormal.x()); |
372 | vertexBufferData.append(t: binormal.y()); |
373 | vertexBufferData.append(t: binormal.z()); |
374 | } |
375 | |
376 | if (hasTangents) { |
377 | const auto &tangent = m_tangents[i]; |
378 | vertexBufferData.append(t: tangent.x()); |
379 | vertexBufferData.append(t: tangent.y()); |
380 | vertexBufferData.append(t: tangent.z()); |
381 | } |
382 | |
383 | if (hasUV0s) { |
384 | const auto &uv0 = m_uv0s[i]; |
385 | vertexBufferData.append(t: uv0.x()); |
386 | vertexBufferData.append(t: uv0.y()); |
387 | } |
388 | |
389 | if (hasUV1s) { |
390 | const auto &uv1 = m_uv1s[i]; |
391 | vertexBufferData.append(t: uv1.x()); |
392 | vertexBufferData.append(t: uv1.y()); |
393 | } |
394 | |
395 | if (hasColors) { |
396 | const auto &color = m_colors[i]; |
397 | vertexBufferData.append(t: color.x()); |
398 | vertexBufferData.append(t: color.y()); |
399 | vertexBufferData.append(t: color.z()); |
400 | vertexBufferData.append(t: color.w()); |
401 | } |
402 | |
403 | if (hasJoints) { |
404 | const auto &joint = m_joints[i]; |
405 | vertexBufferData.append(t: joint.x()); |
406 | vertexBufferData.append(t: joint.y()); |
407 | vertexBufferData.append(t: joint.z()); |
408 | vertexBufferData.append(t: joint.w()); |
409 | } |
410 | |
411 | if (hasWeights) { |
412 | const auto &weight = m_weights[i]; |
413 | vertexBufferData.append(t: weight.x()); |
414 | vertexBufferData.append(t: weight.y()); |
415 | vertexBufferData.append(t: weight.z()); |
416 | vertexBufferData.append(t: weight.w()); |
417 | } |
418 | } |
419 | |
420 | setBounds(min: minBounds, max: maxBounds); |
421 | QByteArray vertexBuffer(reinterpret_cast<char *>(vertexBufferData.data()), bufferSize); |
422 | setVertexData(vertexBuffer); |
423 | |
424 | // Index Buffer |
425 | if (hasIndexes) { |
426 | const qsizetype indexLength = m_indexes.size(); |
427 | QByteArray indexBuffer; |
428 | indexBuffer.reserve(asize: indexLength * sizeof(unsigned int)); |
429 | for (qsizetype i = 0; i < indexLength; ++i) { |
430 | const auto &index = m_indexes[i]; |
431 | indexBuffer.append(s: reinterpret_cast<const char *>(&index), len: sizeof(unsigned int)); |
432 | } |
433 | setIndexData(indexBuffer); |
434 | } |
435 | |
436 | // Subsets |
437 | // Subsets are optional so if none are specified the whole mesh is a single submesh |
438 | if (!m_subsets.isEmpty()) { |
439 | for (const auto &subset : m_subsets) { |
440 | QVector3D subsetMinBounds; |
441 | QVector3D subsetMaxBounds; |
442 | // Range checking is necessary because the user could have specified subset values |
443 | // that are out of range of the vertex/index buffer |
444 | bool outOfRange = false; |
445 | for (qsizetype i = subset->offset(); i < subset->offset() + subset->count(); ++i) { |
446 | if (hasPositions) { |
447 | qsizetype index = i; |
448 | if (hasIndexes) { |
449 | if (i < m_indexes.size()) { |
450 | index = m_indexes[i]; |
451 | } else { |
452 | outOfRange = true; |
453 | break; |
454 | } |
455 | } |
456 | if (index < m_positions.size()) { |
457 | const auto &position = m_positions[index]; |
458 | subsetMinBounds.setX(qMin(a: subsetMinBounds.x(), b: position.x())); |
459 | subsetMaxBounds.setX(qMax(a: subsetMaxBounds.x(), b: position.x())); |
460 | subsetMinBounds.setY(qMin(a: subsetMinBounds.y(), b: position.y())); |
461 | subsetMaxBounds.setY(qMax(a: subsetMaxBounds.y(), b: position.y())); |
462 | subsetMinBounds.setZ(qMin(a: subsetMinBounds.z(), b: position.z())); |
463 | subsetMaxBounds.setZ(qMax(a: subsetMaxBounds.z(), b: position.z())); |
464 | } else { |
465 | outOfRange = true; |
466 | break; |
467 | } |
468 | } |
469 | } |
470 | if (!outOfRange) |
471 | addSubset(offset: subset->offset(), count: subset->count(), boundsMin: subsetMinBounds, boundsMax: subsetMaxBounds, name: subset->name()); |
472 | else |
473 | qWarning(msg: "Skipping invalid subset: Out of Range" ); |
474 | } |
475 | } |
476 | |
477 | update(); |
478 | } |
479 | |
480 | void ProceduralMesh::subsetDestroyed(QObject *subset) |
481 | { |
482 | if (m_subsets.removeAll(t: subset)) |
483 | requestUpdate(); |
484 | } |
485 | |
486 | bool ProceduralMesh::supportsTriangleFanPrimitive() const |
487 | { |
488 | static bool supportQueried = false; |
489 | static bool triangleFanSupported = false; |
490 | if (!supportQueried) { |
491 | const auto &manager = QQuick3DObjectPrivate::get(item: this)->sceneManager; |
492 | if (manager) { |
493 | auto window = manager->window(); |
494 | if (window) { |
495 | auto rhi = window->rhi(); |
496 | if (rhi) { |
497 | triangleFanSupported = rhi->isFeatureSupported(feature: QRhi::TriangleFanTopology); |
498 | supportQueried = true; |
499 | } |
500 | } |
501 | } |
502 | } |
503 | |
504 | return triangleFanSupported; |
505 | } |
506 | |
507 | void ProceduralMesh::qmlAppendProceduralMeshSubset(QQmlListProperty<ProceduralMeshSubset> *list, ProceduralMeshSubset *subset) |
508 | { |
509 | if (subset == nullptr) |
510 | return; |
511 | ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object); |
512 | self->m_subsets.push_back(t: subset); |
513 | |
514 | connect(sender: subset, signal: &ProceduralMeshSubset::isDirty, context: self, slot: &ProceduralMesh::requestUpdate); |
515 | connect(sender: subset, signal: &QObject::destroyed, context: self, slot: &ProceduralMesh::subsetDestroyed); |
516 | |
517 | self->requestUpdate(); |
518 | } |
519 | |
520 | ProceduralMeshSubset *ProceduralMesh::qmlProceduralMeshSubsetAt(QQmlListProperty<ProceduralMeshSubset> *list, qsizetype index) |
521 | { |
522 | ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object); |
523 | return self->m_subsets.at(i: index); |
524 | |
525 | } |
526 | |
527 | qsizetype ProceduralMesh::qmlProceduralMeshSubsetCount(QQmlListProperty<ProceduralMeshSubset> *list) |
528 | { |
529 | ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object); |
530 | return self->m_subsets.count(); |
531 | } |
532 | |
533 | void ProceduralMesh::qmlClearProceduralMeshSubset(QQmlListProperty<ProceduralMeshSubset> *list) |
534 | { |
535 | ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object); |
536 | self->m_subsets.clear(); |
537 | self->requestUpdate(); |
538 | } |
539 | |
540 | QList<unsigned int> ProceduralMesh::indexes() const |
541 | { |
542 | return m_indexes; |
543 | } |
544 | |
545 | void ProceduralMesh::setIndexes(const QList<unsigned int> &newIndexes) |
546 | { |
547 | if (m_indexes == newIndexes) |
548 | return; |
549 | m_indexes = newIndexes; |
550 | Q_EMIT indexesChanged(); |
551 | requestUpdate(); |
552 | } |
553 | |
554 | QList<QVector3D> ProceduralMesh::normals() const |
555 | { |
556 | return m_normals; |
557 | } |
558 | |
559 | void ProceduralMesh::setNormals(const QList<QVector3D> &newNormals) |
560 | { |
561 | if (m_normals == newNormals) |
562 | return; |
563 | m_normals = newNormals; |
564 | Q_EMIT normalsChanged(); |
565 | requestUpdate(); |
566 | } |
567 | |
568 | QList<QVector3D> ProceduralMesh::tangents() const |
569 | { |
570 | return m_tangents; |
571 | } |
572 | |
573 | void ProceduralMesh::setTangents(const QList<QVector3D> &newTangents) |
574 | { |
575 | if (m_tangents == newTangents) |
576 | return; |
577 | m_tangents = newTangents; |
578 | Q_EMIT tangentsChanged(); |
579 | requestUpdate(); |
580 | } |
581 | |
582 | QList<QVector3D> ProceduralMesh::binormals() const |
583 | { |
584 | return m_binormals; |
585 | } |
586 | |
587 | void ProceduralMesh::setBinormals(const QList<QVector3D> &newBinormals) |
588 | { |
589 | if (m_binormals == newBinormals) |
590 | return; |
591 | m_binormals = newBinormals; |
592 | Q_EMIT binormalsChanged(); |
593 | requestUpdate(); |
594 | } |
595 | |
596 | QList<QVector2D> ProceduralMesh::uv0s() const |
597 | { |
598 | return m_uv0s; |
599 | } |
600 | |
601 | void ProceduralMesh::setUv0s(const QList<QVector2D> &newUv0s) |
602 | { |
603 | if (m_uv0s == newUv0s) |
604 | return; |
605 | m_uv0s = newUv0s; |
606 | Q_EMIT uv0sChanged(); |
607 | requestUpdate(); |
608 | } |
609 | |
610 | QList<QVector2D> ProceduralMesh::uv1s() const |
611 | { |
612 | return m_uv1s; |
613 | } |
614 | |
615 | void ProceduralMesh::setUv1s(const QList<QVector2D> &newUv1s) |
616 | { |
617 | if (m_uv1s == newUv1s) |
618 | return; |
619 | m_uv1s = newUv1s; |
620 | Q_EMIT uv1sChanged(); |
621 | requestUpdate(); |
622 | } |
623 | |
624 | QList<QVector4D> ProceduralMesh::colors() const |
625 | { |
626 | return m_colors; |
627 | } |
628 | |
629 | void ProceduralMesh::setColors(const QList<QVector4D> &newColors) |
630 | { |
631 | if (m_colors == newColors) |
632 | return; |
633 | m_colors = newColors; |
634 | Q_EMIT colorsChanged(); |
635 | requestUpdate(); |
636 | } |
637 | |
638 | QList<QVector4D> ProceduralMesh::joints() const |
639 | { |
640 | return m_joints; |
641 | } |
642 | |
643 | void ProceduralMesh::setJoints(const QList<QVector4D> &newJoints) |
644 | { |
645 | if (m_joints == newJoints) |
646 | return; |
647 | m_joints = newJoints; |
648 | Q_EMIT jointsChanged(); |
649 | requestUpdate(); |
650 | } |
651 | |
652 | QList<QVector4D> ProceduralMesh::weights() const |
653 | { |
654 | return m_weights; |
655 | } |
656 | |
657 | void ProceduralMesh::setWeights(const QList<QVector4D> &newWeights) |
658 | { |
659 | if (m_weights == newWeights) |
660 | return; |
661 | m_weights = newWeights; |
662 | Q_EMIT weightsChanged(); |
663 | requestUpdate(); |
664 | } |
665 | |
666 | QQmlListProperty<ProceduralMeshSubset> ProceduralMesh::subsets() |
667 | { |
668 | return QQmlListProperty<ProceduralMeshSubset>(this, |
669 | nullptr, |
670 | ProceduralMesh::qmlAppendProceduralMeshSubset, |
671 | ProceduralMesh::qmlProceduralMeshSubsetCount, |
672 | ProceduralMesh::qmlProceduralMeshSubsetAt, |
673 | ProceduralMesh::qmlClearProceduralMeshSubset); |
674 | } |
675 | |
676 | int ProceduralMeshSubset::offset() const |
677 | { |
678 | return m_offset; |
679 | } |
680 | |
681 | void ProceduralMeshSubset::setOffset(int newOffset) |
682 | { |
683 | if (m_offset == newOffset) |
684 | return; |
685 | |
686 | m_offset = newOffset; |
687 | Q_EMIT offsetChanged(); |
688 | Q_EMIT isDirty(); |
689 | } |
690 | |
691 | int ProceduralMeshSubset::count() const |
692 | { |
693 | return m_count; |
694 | } |
695 | |
696 | void ProceduralMeshSubset::setCount(int newCount) |
697 | { |
698 | if (m_count == newCount) |
699 | return; |
700 | |
701 | m_count = newCount; |
702 | Q_EMIT countChanged(); |
703 | Q_EMIT isDirty(); |
704 | } |
705 | |
706 | QString ProceduralMeshSubset::name() const |
707 | { |
708 | return m_name; |
709 | } |
710 | |
711 | void ProceduralMeshSubset::setName(const QString &newName) |
712 | { |
713 | if (m_name == newName) |
714 | return; |
715 | |
716 | m_name = newName; |
717 | Q_EMIT nameChanged(); |
718 | Q_EMIT isDirty(); |
719 | } |
720 | |
721 | QT_END_NAMESPACE |
722 | |