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