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