Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qheightfieldshape.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
7
8#include <limits>
9#include <vector>
10
11#include <QFileInfo>
12#include <QImage>
13#include <QQmlContext>
14#include <QQmlFile>
15#include <QtQuick3D/QQuick3DGeometry>
16#include <extensions/PxExtensionsAPI.h>
17
18//########################################################################################
19// NOTE:
20// Triangle mesh, heightfield or plane geometry shapes configured as eSIMULATION_SHAPE are
21// not supported for non-kinematic PxRigidDynamic instances.
22//########################################################################################
23
24#include "foundation/PxVec3.h"
25//#include "cooking/PxTriangleMeshDesc.h"
26#include "cooking/PxCooking.h"
27#include "extensions/PxDefaultStreams.h"
28#include "geometry/PxHeightField.h"
29#include "geometry/PxHeightFieldDesc.h"
30
33
35
36// TODO: Unify with QQuick3DPhysicsMeshManager??? It's the same basic logic,
37// but we're using images instead of meshes.
38
40{
41public:
42 QQuick3DPhysicsHeightField(const QString &qmlSource);
43 QQuick3DPhysicsHeightField(QQuickImage *image);
45
46 void ref() { ++refCount; }
47 int deref() { return --refCount; }
48 void writeSamples(const QImage &heightMap);
49 physx::PxHeightField *heightField();
50
51 int rows() const;
52 int columns() const;
53
54private:
55 void forgetStalePhysXHeightField();
56
57 QString m_sourcePath;
58 // This raw pointer is safe to store since when the Image or
59 // HeightFieldShape is destroyed, this heightfield will be dereferenced
60 // from all shapes and deleted.
61 QQuickImage *m_image = nullptr;
62 std::vector<physx::PxHeightFieldSample> m_samples;
63 physx::PxHeightField *m_heightField = nullptr;
64 quint32 m_physicsGeneration = 0;
65 int m_rows = 0;
66 int m_columns = 0;
67 int refCount = 0;
68};
69
71{
72public:
73 static QQuick3DPhysicsHeightField *getHeightField(const QUrl &source,
74 const QObject *contextObject);
75 static QQuick3DPhysicsHeightField *getHeightField(QQuickImage *source);
77
78private:
79 static QHash<QString, QQuick3DPhysicsHeightField *> heightFieldHash;
80 static QHash<QQuickImage *, QQuick3DPhysicsHeightField *> heightFieldImageHash;
81};
82
83QHash<QString, QQuick3DPhysicsHeightField *> QQuick3DPhysicsHeightFieldManager::heightFieldHash;
84QHash<QQuickImage *, QQuick3DPhysicsHeightField *>
85 QQuick3DPhysicsHeightFieldManager::heightFieldImageHash;
86
88QQuick3DPhysicsHeightFieldManager::getHeightField(const QUrl &source, const QObject *contextObject)
89{
90 const QQmlContext *context = qmlContext(contextObject);
91
92 const auto resolvedUrl = context ? context->resolvedUrl(source) : source;
93 const auto qmlSource = QQmlFile::urlToLocalFileOrQrc(resolvedUrl);
94
95 auto *heightField = heightFieldHash.value(qmlSource);
96 if (!heightField) {
97 heightField = new QQuick3DPhysicsHeightField(qmlSource);
98 heightFieldHash[qmlSource] = heightField;
99 }
100 heightField->ref();
101 return heightField;
102}
103
105{
106 auto *heightField = heightFieldImageHash.value(source);
107 if (!heightField) {
108 heightField = new QQuick3DPhysicsHeightField(source);
109 heightFieldImageHash[source] = heightField;
110 }
111 heightField->ref();
112 return heightField;
113}
114
116{
117 if (heightField != nullptr && heightField->deref() == 0) {
118 qCDebug(lcQuick3dPhysics()) << "deleting height field" << heightField;
119 erase_if(heightFieldHash,
120 [heightField](std::pair<const QString &, QQuick3DPhysicsHeightField *&> h) {
121 return h.second == heightField;
122 });
123 erase_if(heightFieldImageHash,
124 [heightField](std::pair<QQuickImage *, QQuick3DPhysicsHeightField *&> h) {
125 return h.second == heightField;
126 });
127 delete heightField;
128 }
129}
130
135
137
139{
140 forgetStalePhysXHeightField();
141 if (m_heightField)
142 m_heightField->release();
143}
144
145// Drops the PhysX height field if it was destroyed together with the PxPhysics that created it.
146// That happens when the last PhysicsWorld is destroyed while shapes using this height field
147// remain, so the destructor must not release it again, and the next world must not get it.
148void QQuick3DPhysicsHeightField::forgetStalePhysXHeightField()
149{
150 const auto &s_physx = StaticPhysXObjects::getReference();
151 if (m_physicsGeneration == s_physx.physicsGeneration)
152 return;
153 m_heightField = nullptr;
154 m_physicsGeneration = s_physx.physicsGeneration;
155}
156
157void QQuick3DPhysicsHeightField::writeSamples(const QImage &heightMap)
158{
159 if (Q_UNLIKELY(heightMap.isNull())) {
160 m_rows = 0;
161 m_columns = 0;
162 m_samples.clear();
163 return;
164 }
165
166 m_rows = heightMap.height();
167 m_columns = heightMap.width();
168
169 const quint64 sampleCount = quint64(m_rows) * quint64(m_columns);
170 if (sampleCount > std::numeric_limits<size_t>::max()) {
171 qWarning() << "QQuick3DPhysicsHeightField: height map" << m_columns << "x" << m_rows
172 << "is too large to allocate.";
173 m_rows = 0;
174 m_columns = 0;
175 m_samples.clear();
176 return;
177 }
178
179 const size_t numRows = size_t(m_rows);
180 m_samples.resize(size_t(sampleCount));
181 for (int i = 0; i < m_columns; i++)
182 for (int j = 0; j < m_rows; j++) {
183 float f = heightMap.pixelColor(i, j).valueF() - 0.5;
184 // qDebug() << i << j << f;
185 m_samples[size_t(i) * numRows + size_t(j)] = { qint16(0xffff * f), 0, 0 }; //{qint16(i%3*2 + j), 0, 0};
186 }
187}
188
190{
191 forgetStalePhysXHeightField();
192 if (m_heightField)
193 return m_heightField;
194
195 physx::PxPhysics *thePhysics = QPhysicsWorld::getPhysics();
196 if (thePhysics == nullptr)
197 return nullptr;
198
199 // No source set
200 if (m_image == nullptr && m_sourcePath.isEmpty())
201 return nullptr;
202
203 // Reading from image property has precedence
204 const bool readFromFile = m_image == nullptr;
205
206 // Security note: This code reads user provided images and create heightfields from them.
207 // This is safe since we assume that QImage properly rejects invalid image files.
208 // It also reads cached and cooked heightfields but that file is marked.
209 if (readFromFile) {
210 // Try read cached file
211 m_heightField = QCacheUtils::readCachedHeightField(m_sourcePath, *thePhysics);
212 if (m_heightField != nullptr) {
213 m_rows = m_heightField->getNbRows();
214 m_columns = m_heightField->getNbColumns();
215 return m_heightField;
216 }
217
218 // Try read cooked file
219 m_heightField = QCacheUtils::readCookedHeightField(m_sourcePath, *thePhysics);
220 if (m_heightField != nullptr) {
221 m_rows = m_heightField->getNbRows();
222 m_columns = m_heightField->getNbColumns();
223 return m_heightField;
224 }
225
226 // Try read image file
227 writeSamples(QImage(m_sourcePath));
228 } else {
229 writeSamples(m_image->image());
230 }
231
232 int numRows = m_rows;
233 int numCols = m_columns;
234 const auto *samples = m_samples.data();
235
236 physx::PxHeightFieldDesc hfDesc;
237 hfDesc.format = physx::PxHeightFieldFormat::eS16_TM;
238 hfDesc.nbColumns = numRows;
239 hfDesc.nbRows = numCols;
240 hfDesc.samples.data = samples;
241 hfDesc.samples.stride = sizeof(physx::PxHeightFieldSample);
242
243 physx::PxDefaultMemoryOutputStream buf;
244
245 const auto *cookingParams = QPhysicsWorld::getCookingParams();
246 if (numRows && numCols && cookingParams && ::PxCookHeightField(hfDesc, buf)) {
247 auto size = buf.getSize();
248 auto *data = buf.getData();
249 physx::PxDefaultMemoryInputData input(data, size);
250 m_heightField = thePhysics->createHeightField(input);
251 qCDebug(lcQuick3dPhysics) << "created height field" << m_heightField << numCols << numRows
252 << "from"
253 << (readFromFile ? m_sourcePath : QString::fromUtf8("image"));
254 if (readFromFile)
255 QCacheUtils::writeCachedHeightField(m_sourcePath, buf);
256 } else {
257 qCWarning(lcQuick3dPhysics) << "Could not create height field from"
258 << (readFromFile ? m_sourcePath : QString::fromUtf8("image"));
259 }
260
261 return m_heightField;
262}
263
265{
266 return m_rows;
267}
268
270{
271 return m_columns;
272}
273
274/*!
275 \qmltype HeightFieldShape
276 \inqmlmodule QtQuick3D.Physics
277 \inherits CollisionShape
278 \since 6.4
279 \brief A collision shape where the elevation is defined by a height map.
280
281 The HeightFieldShape type defines a physical surface where the height is determined by
282 the \l {QColor#The HSV Color Model}{value} of the pixels of the \l {source} image. The
283 x-axis of the image is mapped to the positive x-axis of the scene, and the y-axis of the
284 image is mapped to the negative z-axis of the scene. A typical use case is to represent
285 natural terrain.
286
287 Objects that are controlled by the physics simulation cannot use HeightFieldShape: It can only
288 be used with \l StaticRigidBody and \l {DynamicRigidBody::isKinematic}{kinematic bodies}.
289
290 \l [QtQuick3D]{HeightFieldGeometry}{QtQuick3D.Helpers.HeightFieldGeometry} is API compatible
291 with the HeightFieldShape type, and can be used to show the height field visually. To
292 improve performance, use a lower resolution version of the height map for the HeightFieldShape:
293 As long as the \l{extents} and the image aspect ratio are the same, the physics body and the
294 visual item will overlap.
295
296 \sa {Qt Quick 3D Physics Shapes, Bodies and Joints}
297*/
298
299/*!
300 \qmlproperty vector3d HeightFieldShape::extents
301 This property defines the extents of the height field. The default value
302 is \c{(100, 100, 100)} when the heightMap is square. If the heightMap is
303 non-square, the default value is reduced along the x- or z-axis, so the height
304 field will keep the aspect ratio of the image.
305*/
306
307/*!
308 \qmlproperty url HeightFieldShape::source
309 This property defines the location of the heightMap file.
310
311 Internally, HeightFieldShape converts the height map image to an optimized data structure. This
312 conversion can be done in advance. See the \l{Qt Quick 3D Physics Cooking}{cooking overview
313 documentation} for details.
314
315 \note If both the \l{HeightFieldShape::}{image} and \l{HeightFieldShape::}{source} properties
316 are set then only \l{HeightFieldShape::}{image} will be used.
317 \sa HeightFieldShape::image
318*/
319
320/*!
321 \qmlproperty Image HeightFieldShape::image
322 This property defines the image holding the heightMap.
323
324 Internally, HeightFieldShape converts the height map image to an optimized data structure. This
325 conversion can be done in advance. See the \l{Qt Quick 3D Physics Cooking}{cooking overview
326 documentation} for details.
327
328 \note If both the \l{HeightFieldShape::}{image} and \l{HeightFieldShape::}{source} properties
329 are set then only \l{HeightFieldShape::}{image} will be used.
330 \sa HeightFieldShape::source
331 \since 6.7
332*/
333
334QHeightFieldShape::QHeightFieldShape() = default;
335
336QHeightFieldShape::~QHeightFieldShape()
337{
338 delete m_heightFieldGeometry;
339 if (m_heightField)
340 QQuick3DPhysicsHeightFieldManager::releaseHeightField(m_heightField);
341}
342
343physx::PxGeometry *QHeightFieldShape::getPhysXGeometry()
344{
345 // The geometry points at a PhysX height field that dies with the PxPhysics that created it,
346 // so a new world needs a new geometry even when nothing else is dirty.
347 const auto &s_physx = StaticPhysXObjects::getReference();
348 if (m_dirtyPhysx || m_scaleDirty || !m_heightFieldGeometry
349 || m_physicsGeneration != s_physx.physicsGeneration) {
350 updatePhysXGeometry();
351 }
352 return m_heightFieldGeometry;
353}
354
355void QHeightFieldShape::updatePhysXGeometry()
356{
357 delete m_heightFieldGeometry;
358 m_heightFieldGeometry = nullptr;
359 m_physicsGeneration = StaticPhysXObjects::getReference().physicsGeneration;
360 if (!m_heightField)
361 return;
362
363 auto *hf = m_heightField->heightField();
364 float rows = m_heightField->rows();
365 float cols = m_heightField->columns();
366 updateExtents();
367 if (hf && cols > 1 && rows > 1) {
368 QVector3D scaledExtents = m_extents * sceneScale();
369 const float heightScale = scaledExtents.y() / 0x10000;
370 const float rowScale = scaledExtents.x() / (cols - 1);
371 const float columnScale = scaledExtents.z() / (rows - 1);
372 // 1e-8f/(0.0001f / 65535.0f) mirror PhysX's PX_MIN_HEIGHTFIELD_XZ_SCALE/
373 // PX_MIN_HEIGHTFIELD_Y_SCALE (PxHeightFieldGeometry.h); not used directly since
374 // those macros expand to an unqualified PxReal(), only valid inside namespace physx.
375 if (!qIsFinite(heightScale) || !qIsFinite(rowScale) || !qIsFinite(columnScale)
376 || rowScale < 1e-8f || columnScale < 1e-8f || heightScale < (0.0001f / 65535.0f)) {
377 qWarning() << "HeightFieldShape: extents" << m_extents
378 << "produce an invalid scale, ignoring.";
379 m_dirtyPhysx = false;
380 return;
381 }
382 m_heightFieldGeometry = new physx::PxHeightFieldGeometry(hf, physx::PxMeshGeometryFlags(),
383 heightScale, rowScale,
384 columnScale);
385 m_hfOffset = { -scaledExtents.x() / 2, 0, -scaledExtents.z() / 2 };
386
387 qCDebug(lcQuick3dPhysics) << "created height field geom" << m_heightFieldGeometry << "scale"
388 << scaledExtents << m_heightField->columns()
389 << m_heightField->rows();
390 }
391 m_dirtyPhysx = false;
392}
393
394void QHeightFieldShape::updateExtents()
395{
396 if (!m_heightField || m_extentsSetExplicitly)
397 return;
398 int numRows = m_heightField->rows();
399 int numCols = m_heightField->columns();
400 auto prevExt = m_extents;
401 if (numRows == numCols) {
402 m_extents = { 100, 100, 100 };
403 } else if (numRows < numCols) {
404 float f = float(numRows) / float(numCols);
405 m_extents = { 100.f, 100.f, 100.f * f };
406 } else {
407 float f = float(numCols) / float(numRows);
408 m_extents = { 100.f * f, 100.f, 100.f };
409 }
410 if (m_extents != prevExt) {
411 emit extentsChanged();
412 }
413}
414
415const QUrl &QHeightFieldShape::source() const
416{
417 return m_heightMapSource;
418}
419
420void QHeightFieldShape::setSource(const QUrl &newSource)
421{
422 if (m_heightMapSource == newSource)
423 return;
424 m_heightMapSource = newSource;
425
426 // If we get a new source and our heightfield was from the old source
427 // (meaning it was NOT from an image) we deref
428 if (m_image == nullptr) {
429 QQuick3DPhysicsHeightFieldManager::releaseHeightField(m_heightField);
430 m_heightField = nullptr;
431 }
432
433 // Load new height field only if we don't have image as source
434 if (m_image == nullptr && !newSource.isEmpty()) {
435 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(m_heightMapSource, this);
436 emit needsRebuild(this);
437 }
438
439 m_dirtyPhysx = true;
440 emit sourceChanged();
441}
442
443QQuickImage *QHeightFieldShape::image() const
444{
445 return m_image;
446}
447
448void QHeightFieldShape::setImage(QQuickImage *newImage)
449{
450 if (m_image == newImage)
451 return;
452
453 if (m_image)
454 m_image->disconnect(this);
455
456 m_image = newImage;
457
458 if (m_image != nullptr) {
459 connect(m_image, &QObject::destroyed, this, &QHeightFieldShape::imageDestroyed);
460 connect(m_image, &QQuickImage::paintedGeometryChanged, this,
461 &QHeightFieldShape::imageGeometryChanged);
462 }
463
464 // New image means we get a new heightfield so deref the old one
465 QQuick3DPhysicsHeightFieldManager::releaseHeightField(m_heightField);
466 m_heightField = nullptr;
467
468 if (m_image != nullptr)
469 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(m_image);
470 else if (!m_heightMapSource.isEmpty())
471 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(m_heightMapSource, this);
472
473 m_dirtyPhysx = true;
474 emit needsRebuild(this);
475 emit imageChanged();
476}
477
478void QHeightFieldShape::imageDestroyed(QObject *image)
479{
480 Q_ASSERT(m_image == image);
481 // Set image to null and the old one will be disconnected and dereferenced
482 setImage(nullptr);
483}
484
485void QHeightFieldShape::imageGeometryChanged()
486{
487 Q_ASSERT(m_image);
488 // Using image has precedence so it is safe to assume this is the current source
489 QQuick3DPhysicsHeightFieldManager::releaseHeightField(m_heightField);
490 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(m_image);
491 m_dirtyPhysx = true;
492 emit needsRebuild(this);
493}
494
495const QVector3D &QHeightFieldShape::extents() const
496{
497 return m_extents;
498}
499
500void QHeightFieldShape::setExtents(const QVector3D &newExtents)
501{
502 m_extentsSetExplicitly = true;
503 if (m_extents == newExtents)
504 return;
505 m_extents = newExtents;
506
507 m_dirtyPhysx = true;
508
509 emit needsRebuild(this);
510 emit extentsChanged();
511}
512
static QQuick3DPhysicsHeightField * getHeightField(QQuickImage *source)
static QQuick3DPhysicsHeightField * getHeightField(const QUrl &source, const QObject *contextObject)
static void releaseHeightField(QQuick3DPhysicsHeightField *heightField)
QQuick3DPhysicsHeightField(const QString &qmlSource)
QQuick3DPhysicsHeightField(QQuickImage *image)
void writeSamples(const QImage &heightMap)
physx::PxHeightField * heightField()
void writeCachedHeightField(const QString &filePath, physx::PxDefaultMemoryOutputStream &buf)
#define QT_BEGIN_NAMESPACE
#define QT_END_NAMESPACE
static StaticPhysXObjects & getReference()