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 m_heightFieldGeometry = new physx::PxHeightFieldGeometry(
339 hf, physx::PxMeshGeometryFlags(), scaledExtents.y() / 0x10000,
340 scaledExtents.x() / (cols - 1), scaledExtents.z() / (rows - 1));
341 m_hfOffset = { -scaledExtents.x() / 2, 0, -scaledExtents.z() / 2 };
342
343 qCDebug(lcQuick3dPhysics) << "created height field geom" << m_heightFieldGeometry << "scale"
344 << scaledExtents << m_heightField->columns()
345 << m_heightField->rows();
346 }
347 m_dirtyPhysx = false;
348}
349
350void QHeightFieldShape::updateExtents()
351{
352 if (!m_heightField || m_extentsSetExplicitly)
353 return;
354 int numRows = m_heightField->rows();
355 int numCols = m_heightField->columns();
356 auto prevExt = m_extents;
357 if (numRows == numCols) {
358 m_extents = { 100, 100, 100 };
359 } else if (numRows < numCols) {
360 float f = float(numRows) / float(numCols);
361 m_extents = { 100.f, 100.f, 100.f * f };
362 } else {
363 float f = float(numCols) / float(numRows);
364 m_extents = { 100.f * f, 100.f, 100.f };
365 }
366 if (m_extents != prevExt) {
367 emit extentsChanged();
368 }
369}
370
371const QUrl &QHeightFieldShape::source() const
372{
373 return m_heightMapSource;
374}
375
376void QHeightFieldShape::setSource(const QUrl &newSource)
377{
378 if (m_heightMapSource == newSource)
379 return;
380 m_heightMapSource = newSource;
381
382 // If we get a new source and our heightfield was from the old source
383 // (meaning it was NOT from an image) we deref
384 if (m_image == nullptr) {
385 QQuick3DPhysicsHeightFieldManager::releaseHeightField(m_heightField);
386 m_heightField = nullptr;
387 }
388
389 // Load new height field only if we don't have image as source
390 if (m_image == nullptr && !newSource.isEmpty()) {
391 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(m_heightMapSource, this);
392 emit needsRebuild(this);
393 }
394
395 m_dirtyPhysx = true;
396 emit sourceChanged();
397}
398
399QQuickImage *QHeightFieldShape::image() const
400{
401 return m_image;
402}
403
404void QHeightFieldShape::setImage(QQuickImage *newImage)
405{
406 if (m_image == newImage)
407 return;
408
409 if (m_image)
410 m_image->disconnect(this);
411
412 m_image = newImage;
413
414 if (m_image != nullptr) {
415 connect(m_image, &QObject::destroyed, this, &QHeightFieldShape::imageDestroyed);
416 connect(m_image, &QQuickImage::paintedGeometryChanged, this,
417 &QHeightFieldShape::imageGeometryChanged);
418 }
419
420 // New image means we get a new heightfield so deref the old one
421 QQuick3DPhysicsHeightFieldManager::releaseHeightField(m_heightField);
422 m_heightField = nullptr;
423
424 if (m_image != nullptr)
425 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(m_image);
426 else if (!m_heightMapSource.isEmpty())
427 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(m_heightMapSource, this);
428
429 m_dirtyPhysx = true;
430 emit needsRebuild(this);
431 emit imageChanged();
432}
433
434void QHeightFieldShape::imageDestroyed(QObject *image)
435{
436 Q_ASSERT(m_image == image);
437 // Set image to null and the old one will be disconnected and dereferenced
438 setImage(nullptr);
439}
440
441void QHeightFieldShape::imageGeometryChanged()
442{
443 Q_ASSERT(m_image);
444 // Using image has precedence so it is safe to assume this is the current source
445 QQuick3DPhysicsHeightFieldManager::releaseHeightField(m_heightField);
446 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(m_image);
447 m_dirtyPhysx = true;
448 emit needsRebuild(this);
449}
450
451const QVector3D &QHeightFieldShape::extents() const
452{
453 return m_extents;
454}
455
456void QHeightFieldShape::setExtents(const QVector3D &newExtents)
457{
458 m_extentsSetExplicitly = true;
459 if (m_extents == newExtents)
460 return;
461 m_extents = newExtents;
462
463 m_dirtyPhysx = true;
464
465 emit needsRebuild(this);
466 emit extentsChanged();
467}
468
469QT_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.