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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtquick3dphysics/src/quick3dphysics/qheightfieldshape.cpp