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