1 | // Copyright (C) 2022 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qcacheutils_p.h" |
5 | #include "qheightfieldshape_p.h" |
6 | |
7 | #include <QFileInfo> |
8 | #include <QImage> |
9 | #include <QQmlContext> |
10 | #include <QQmlFile> |
11 | #include <QtQuick3D/QQuick3DGeometry> |
12 | #include <extensions/PxExtensionsAPI.h> |
13 | |
14 | //######################################################################################## |
15 | // NOTE: |
16 | // Triangle mesh, heightfield or plane geometry shapes configured as eSIMULATION_SHAPE are |
17 | // not supported for non-kinematic PxRigidDynamic instances. |
18 | //######################################################################################## |
19 | |
20 | #include "foundation/PxVec3.h" |
21 | //#include "cooking/PxTriangleMeshDesc.h" |
22 | #include "extensions/PxDefaultStreams.h" |
23 | #include "geometry/PxHeightField.h" |
24 | #include "geometry/PxHeightFieldDesc.h" |
25 | |
26 | #include "qphysicsworld_p.h" |
27 | |
28 | QT_BEGIN_NAMESPACE |
29 | |
30 | // TODO: Unify with QQuick3DPhysicsMeshManager??? It's the same basic logic, |
31 | // but we're using images instead of meshes. |
32 | |
33 | class QQuick3DPhysicsHeightField |
34 | { |
35 | public: |
36 | QQuick3DPhysicsHeightField(const QString &qmlSource); |
37 | QQuick3DPhysicsHeightField(QQuickImage *image); |
38 | ~QQuick3DPhysicsHeightField(); |
39 | |
40 | void ref() { ++refCount; } |
41 | int deref() { return --refCount; } |
42 | void writeSamples(const QImage &heightMap); |
43 | physx::PxHeightField *heightField(); |
44 | |
45 | int rows() const; |
46 | int columns() const; |
47 | |
48 | private: |
49 | QString m_sourcePath; |
50 | // This raw pointer is safe to store since when the Image or |
51 | // HeightFieldShape is destroyed, this heightfield will be dereferenced |
52 | // from all shapes and deleted. |
53 | QQuickImage *m_image = nullptr; |
54 | physx::PxHeightFieldSample *m_samples = nullptr; |
55 | physx::PxHeightField *m_heightField = nullptr; |
56 | int m_rows = 0; |
57 | int m_columns = 0; |
58 | int refCount = 0; |
59 | }; |
60 | |
61 | class QQuick3DPhysicsHeightFieldManager |
62 | { |
63 | public: |
64 | static QQuick3DPhysicsHeightField *getHeightField(const QUrl &source, |
65 | const QObject *contextObject); |
66 | static QQuick3DPhysicsHeightField *getHeightField(QQuickImage *source); |
67 | static void releaseHeightField(QQuick3DPhysicsHeightField *heightField); |
68 | |
69 | private: |
70 | static QHash<QString, QQuick3DPhysicsHeightField *> heightFieldHash; |
71 | static QHash<QQuickImage *, QQuick3DPhysicsHeightField *> heightFieldImageHash; |
72 | }; |
73 | |
74 | QHash<QString, QQuick3DPhysicsHeightField *> QQuick3DPhysicsHeightFieldManager::heightFieldHash; |
75 | QHash<QQuickImage *, QQuick3DPhysicsHeightField *> |
76 | QQuick3DPhysicsHeightFieldManager::heightFieldImageHash; |
77 | |
78 | QQuick3DPhysicsHeightField * |
79 | QQuick3DPhysicsHeightFieldManager::getHeightField(const QUrl &source, const QObject *contextObject) |
80 | { |
81 | const QQmlContext *context = qmlContext(contextObject); |
82 | |
83 | const auto resolvedUrl = context ? context->resolvedUrl(source) : source; |
84 | const auto qmlSource = QQmlFile::urlToLocalFileOrQrc(resolvedUrl); |
85 | |
86 | auto *heightField = heightFieldHash.value(key: qmlSource); |
87 | if (!heightField) { |
88 | heightField = new QQuick3DPhysicsHeightField(qmlSource); |
89 | heightFieldHash[qmlSource] = heightField; |
90 | } |
91 | heightField->ref(); |
92 | return heightField; |
93 | } |
94 | |
95 | QQuick3DPhysicsHeightField *QQuick3DPhysicsHeightFieldManager::getHeightField(QQuickImage *source) |
96 | { |
97 | auto *heightField = heightFieldImageHash.value(key: source); |
98 | if (!heightField) { |
99 | heightField = new QQuick3DPhysicsHeightField(source); |
100 | heightFieldImageHash[source] = heightField; |
101 | } |
102 | heightField->ref(); |
103 | return heightField; |
104 | } |
105 | |
106 | void QQuick3DPhysicsHeightFieldManager::releaseHeightField(QQuick3DPhysicsHeightField *heightField) |
107 | { |
108 | if (heightField != nullptr && heightField->deref() == 0) { |
109 | qCDebug(lcQuick3dPhysics()) << "deleting height field"<< heightField; |
110 | erase_if(hash&: heightFieldHash, |
111 | pred: [heightField](std::pair<const QString &, QQuick3DPhysicsHeightField *&> h) { |
112 | return h.second == heightField; |
113 | }); |
114 | erase_if(hash&: heightFieldImageHash, |
115 | pred: [heightField](std::pair<QQuickImage *, QQuick3DPhysicsHeightField *&> h) { |
116 | return h.second == heightField; |
117 | }); |
118 | delete heightField; |
119 | } |
120 | } |
121 | |
122 | QQuick3DPhysicsHeightField::QQuick3DPhysicsHeightField(const QString &qmlSource) |
123 | : m_sourcePath(qmlSource) |
124 | { |
125 | } |
126 | |
127 | QQuick3DPhysicsHeightField::QQuick3DPhysicsHeightField(QQuickImage *image) : m_image(image) { } |
128 | |
129 | QQuick3DPhysicsHeightField::~QQuick3DPhysicsHeightField() |
130 | { |
131 | free(ptr: m_samples); |
132 | } |
133 | |
134 | void QQuick3DPhysicsHeightField::writeSamples(const QImage &heightMap) |
135 | { |
136 | m_rows = heightMap.height(); |
137 | m_columns = heightMap.width(); |
138 | int numRows = m_rows; |
139 | int numCols = m_columns; |
140 | |
141 | free(ptr: m_samples); |
142 | m_samples = reinterpret_cast<physx::PxHeightFieldSample *>( |
143 | malloc(size: sizeof(physx::PxHeightFieldSample) * (numRows * numCols))); |
144 | for (int i = 0; i < numCols; i++) |
145 | for (int j = 0; j < numRows; j++) { |
146 | float f = heightMap.pixelColor(x: i, y: j).valueF() - 0.5; |
147 | // qDebug() << i << j << f; |
148 | m_samples[i * numRows + j] = { .height: qint16(0xffff * f), .materialIndex0: 0, .materialIndex1: 0 }; //{qint16(i%3*2 + j), 0, 0}; |
149 | } |
150 | } |
151 | |
152 | physx::PxHeightField *QQuick3DPhysicsHeightField::heightField() |
153 | { |
154 | if (m_heightField) |
155 | return m_heightField; |
156 | |
157 | physx::PxPhysics *thePhysics = QPhysicsWorld::getPhysics(); |
158 | if (thePhysics == nullptr) |
159 | return nullptr; |
160 | |
161 | // No source set |
162 | if (m_image == nullptr && m_sourcePath.isEmpty()) |
163 | return nullptr; |
164 | |
165 | // Reading from image property has precedence |
166 | const bool readFromFile = m_image == nullptr; |
167 | |
168 | if (readFromFile) { |
169 | // Try read cached file |
170 | m_heightField = QCacheUtils::readCachedHeightField(filePath: m_sourcePath, physics&: *thePhysics); |
171 | if (m_heightField != nullptr) { |
172 | m_rows = m_heightField->getNbRows(); |
173 | m_columns = m_heightField->getNbColumns(); |
174 | return m_heightField; |
175 | } |
176 | |
177 | // Try read cooked file |
178 | m_heightField = QCacheUtils::readCookedHeightField(filePath: m_sourcePath, physics&: *thePhysics); |
179 | if (m_heightField != nullptr) { |
180 | m_rows = m_heightField->getNbRows(); |
181 | m_columns = m_heightField->getNbColumns(); |
182 | return m_heightField; |
183 | } |
184 | |
185 | // Try read image file |
186 | writeSamples(heightMap: QImage(m_sourcePath)); |
187 | } else { |
188 | writeSamples(heightMap: m_image->image()); |
189 | } |
190 | |
191 | int numRows = m_rows; |
192 | int numCols = m_columns; |
193 | auto samples = m_samples; |
194 | |
195 | physx::PxHeightFieldDesc hfDesc; |
196 | hfDesc.format = physx::PxHeightFieldFormat::eS16_TM; |
197 | hfDesc.nbColumns = numRows; |
198 | hfDesc.nbRows = numCols; |
199 | hfDesc.samples.data = samples; |
200 | hfDesc.samples.stride = sizeof(physx::PxHeightFieldSample); |
201 | |
202 | physx::PxDefaultMemoryOutputStream buf; |
203 | |
204 | const auto cooking = QPhysicsWorld::getCooking(); |
205 | if (numRows && numCols && cooking && cooking->cookHeightField(desc: hfDesc, stream&: buf)) { |
206 | auto size = buf.getSize(); |
207 | auto *data = buf.getData(); |
208 | physx::PxDefaultMemoryInputData input(data, size); |
209 | m_heightField = thePhysics->createHeightField(stream&: input); |
210 | qCDebug(lcQuick3dPhysics) << "created height field"<< m_heightField << numCols << numRows |
211 | << "from" |
212 | << (readFromFile ? m_sourcePath : QString::fromUtf8(utf8: "image")); |
213 | if (readFromFile) |
214 | QCacheUtils::writeCachedHeightField(filePath: m_sourcePath, buf); |
215 | } else { |
216 | qCWarning(lcQuick3dPhysics) << "Could not create height field from" |
217 | << (readFromFile ? m_sourcePath : QString::fromUtf8(utf8: "image")); |
218 | } |
219 | |
220 | return m_heightField; |
221 | } |
222 | |
223 | int QQuick3DPhysicsHeightField::rows() const |
224 | { |
225 | return m_rows; |
226 | } |
227 | |
228 | int QQuick3DPhysicsHeightField::columns() const |
229 | { |
230 | return m_columns; |
231 | } |
232 | |
233 | /*! |
234 | \qmltype HeightFieldShape |
235 | \inqmlmodule QtQuick3D.Physics |
236 | \inherits CollisionShape |
237 | \since 6.4 |
238 | \brief A collision shape where the elevation is defined by a height map. |
239 | |
240 | The HeightFieldShape type defines a physical surface where the height is determined by |
241 | the \l {QColor#The HSV Color Model}{value} of the pixels of the \l {source} image. The |
242 | x-axis of the image is mapped to the positive x-axis of the scene, and the y-axis of the |
243 | image is mapped to the negative z-axis of the scene. A typical use case is to represent |
244 | natural terrain. |
245 | |
246 | Objects that are controlled by the physics simulation cannot use HeightFieldShape: It can only |
247 | be used with \l StaticRigidBody and \l {DynamicRigidBody::isKinematic}{kinematic bodies}. |
248 | |
249 | \l [QtQuick3D]{HeightFieldGeometry}{QtQuick3D.Helpers.HeightFieldGeometry} is API compatible |
250 | with the HeightFieldShape type, and can be used to show the height field visually. To |
251 | improve performance, use a lower resolution version of the height map for the HeightFieldShape: |
252 | As long as the \l{extents} and the image aspect ratio are the same, the physics body and the |
253 | visual item will overlap. |
254 | |
255 | \sa {Qt Quick 3D Physics Shapes and Bodies}{Shapes and Bodies overview documentation} |
256 | */ |
257 | |
258 | /*! |
259 | \qmlproperty vector3d HeightFieldShape::extents |
260 | This property defines the extents of the height field. The default value |
261 | is \c{(100, 100, 100)} when the heightMap is square. If the heightMap is |
262 | non-square, the default value is reduced along the x- or z-axis, so the height |
263 | field will keep the aspect ratio of the image. |
264 | */ |
265 | |
266 | /*! |
267 | \qmlproperty url HeightFieldShape::source |
268 | This property defines the location of the heightMap file. |
269 | |
270 | Internally, HeightFieldShape converts the height map image to an optimized data structure. This |
271 | conversion can be done in advance. See the \l{Qt Quick 3D Physics Cooking}{cooking overview |
272 | documentation} for details. |
273 | |
274 | \note If both the \l{HeightFieldShape::}{image} and \l{HeightFieldShape::}{source} properties |
275 | are set then only \l{HeightFieldShape::}{image} will be used. |
276 | \sa HeightFieldShape::image |
277 | */ |
278 | |
279 | /*! |
280 | \qmlproperty Image HeightFieldShape::image |
281 | This property defines the image holding the heightMap. |
282 | |
283 | Internally, HeightFieldShape converts the height map image to an optimized data structure. This |
284 | conversion can be done in advance. See the \l{Qt Quick 3D Physics Cooking}{cooking overview |
285 | documentation} for details. |
286 | |
287 | \note If both the \l{HeightFieldShape::}{image} and \l{HeightFieldShape::}{source} properties |
288 | are set then only \l{HeightFieldShape::}{image} will be used. |
289 | \sa HeightFieldShape::source |
290 | \since 6.7 |
291 | */ |
292 | |
293 | QHeightFieldShape::QHeightFieldShape() = default; |
294 | |
295 | QHeightFieldShape::~QHeightFieldShape() |
296 | { |
297 | delete m_heightFieldGeometry; |
298 | if (m_heightField) |
299 | QQuick3DPhysicsHeightFieldManager::releaseHeightField(heightField: m_heightField); |
300 | } |
301 | |
302 | physx::PxGeometry *QHeightFieldShape::getPhysXGeometry() |
303 | { |
304 | if (m_dirtyPhysx || m_scaleDirty || !m_heightFieldGeometry) { |
305 | updatePhysXGeometry(); |
306 | } |
307 | return m_heightFieldGeometry; |
308 | } |
309 | |
310 | void QHeightFieldShape::updatePhysXGeometry() |
311 | { |
312 | delete m_heightFieldGeometry; |
313 | m_heightFieldGeometry = nullptr; |
314 | if (!m_heightField) |
315 | return; |
316 | |
317 | auto *hf = m_heightField->heightField(); |
318 | float rows = m_heightField->rows(); |
319 | float cols = m_heightField->columns(); |
320 | updateExtents(); |
321 | if (hf && cols > 1 && rows > 1) { |
322 | QVector3D scaledExtents = m_extents * sceneScale(); |
323 | m_heightFieldGeometry = new physx::PxHeightFieldGeometry( |
324 | hf, physx::PxMeshGeometryFlags(), scaledExtents.y() / 0x10000, |
325 | scaledExtents.x() / (cols - 1), scaledExtents.z() / (rows - 1)); |
326 | m_hfOffset = { -scaledExtents.x() / 2, 0, -scaledExtents.z() / 2 }; |
327 | |
328 | qCDebug(lcQuick3dPhysics) << "created height field geom"<< m_heightFieldGeometry << "scale" |
329 | << scaledExtents << m_heightField->columns() |
330 | << m_heightField->rows(); |
331 | } |
332 | m_dirtyPhysx = false; |
333 | } |
334 | |
335 | void QHeightFieldShape::updateExtents() |
336 | { |
337 | if (!m_heightField || m_extentsSetExplicitly) |
338 | return; |
339 | int numRows = m_heightField->rows(); |
340 | int numCols = m_heightField->columns(); |
341 | auto prevExt = m_extents; |
342 | if (numRows == numCols) { |
343 | m_extents = { 100, 100, 100 }; |
344 | } else if (numRows < numCols) { |
345 | float f = float(numRows) / float(numCols); |
346 | m_extents = { 100.f, 100.f, 100.f * f }; |
347 | } else { |
348 | float f = float(numCols) / float(numRows); |
349 | m_extents = { 100.f * f, 100.f, 100.f }; |
350 | } |
351 | if (m_extents != prevExt) { |
352 | emit extentsChanged(); |
353 | } |
354 | } |
355 | |
356 | const QUrl &QHeightFieldShape::source() const |
357 | { |
358 | return m_heightMapSource; |
359 | } |
360 | |
361 | void QHeightFieldShape::setSource(const QUrl &newSource) |
362 | { |
363 | if (m_heightMapSource == newSource) |
364 | return; |
365 | m_heightMapSource = newSource; |
366 | |
367 | // If we get a new source and our heightfield was from the old source |
368 | // (meaning it was NOT from an image) we deref |
369 | if (m_image == nullptr) { |
370 | QQuick3DPhysicsHeightFieldManager::releaseHeightField(heightField: m_heightField); |
371 | m_heightField = nullptr; |
372 | } |
373 | |
374 | // Load new height field only if we don't have image as source |
375 | if (m_image == nullptr && !newSource.isEmpty()) { |
376 | m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(source: m_heightMapSource, contextObject: this); |
377 | emit needsRebuild(this); |
378 | } |
379 | |
380 | m_dirtyPhysx = true; |
381 | emit sourceChanged(); |
382 | } |
383 | |
384 | QQuickImage *QHeightFieldShape::image() const |
385 | { |
386 | return m_image; |
387 | } |
388 | |
389 | void QHeightFieldShape::setImage(QQuickImage *newImage) |
390 | { |
391 | if (m_image == newImage) |
392 | return; |
393 | |
394 | if (m_image) |
395 | m_image->disconnect(receiver: this); |
396 | |
397 | m_image = newImage; |
398 | |
399 | if (m_image != nullptr) { |
400 | connect(sender: m_image, signal: &QObject::destroyed, context: this, slot: &QHeightFieldShape::imageDestroyed); |
401 | connect(sender: m_image, signal: &QQuickImage::paintedGeometryChanged, context: this, |
402 | slot: &QHeightFieldShape::imageGeometryChanged); |
403 | } |
404 | |
405 | // New image means we get a new heightfield so deref the old one |
406 | QQuick3DPhysicsHeightFieldManager::releaseHeightField(heightField: m_heightField); |
407 | m_heightField = nullptr; |
408 | |
409 | if (m_image != nullptr) |
410 | m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(source: m_image); |
411 | else if (!m_heightMapSource.isEmpty()) |
412 | m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(source: m_heightMapSource, contextObject: this); |
413 | |
414 | m_dirtyPhysx = true; |
415 | emit needsRebuild(this); |
416 | emit imageChanged(); |
417 | } |
418 | |
419 | void QHeightFieldShape::imageDestroyed(QObject *image) |
420 | { |
421 | Q_ASSERT(m_image == image); |
422 | // Set image to null and the old one will be disconnected and dereferenced |
423 | setImage(nullptr); |
424 | } |
425 | |
426 | void QHeightFieldShape::imageGeometryChanged() |
427 | { |
428 | Q_ASSERT(m_image); |
429 | // Using image has precedence so it is safe to assume this is the current source |
430 | QQuick3DPhysicsHeightFieldManager::releaseHeightField(heightField: m_heightField); |
431 | m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(source: m_image); |
432 | m_dirtyPhysx = true; |
433 | emit needsRebuild(this); |
434 | } |
435 | |
436 | const QVector3D &QHeightFieldShape::extents() const |
437 | { |
438 | return m_extents; |
439 | } |
440 | |
441 | void QHeightFieldShape::setExtents(const QVector3D &newExtents) |
442 | { |
443 | m_extentsSetExplicitly = true; |
444 | if (m_extents == newExtents) |
445 | return; |
446 | m_extents = newExtents; |
447 | |
448 | m_dirtyPhysx = true; |
449 | |
450 | emit needsRebuild(this); |
451 | emit extentsChanged(); |
452 | } |
453 | |
454 | QT_END_NAMESPACE |
455 |
Definitions
- QQuick3DPhysicsHeightField
- ref
- deref
- QQuick3DPhysicsHeightFieldManager
- heightFieldHash
- heightFieldImageHash
- getHeightField
- getHeightField
- releaseHeightField
- QQuick3DPhysicsHeightField
- QQuick3DPhysicsHeightField
- ~QQuick3DPhysicsHeightField
- writeSamples
- heightField
- rows
- columns
- QHeightFieldShape
- ~QHeightFieldShape
- getPhysXGeometry
- updatePhysXGeometry
- updateExtents
- source
- setSource
- image
- setImage
- imageDestroyed
- imageGeometryChanged
- extents
Learn Advanced QML with KDAB
Find out more