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