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();
38
39 void ref() { ++refCount; }
40 int deref() { return --refCount; }
41 physx::PxHeightFieldSample *getSamples();
42 physx::PxHeightField *heightField();
43
44 int rows() const;
45 int columns() const;
46
47private:
48 QString m_sourcePath;
49 physx::PxHeightFieldSample *m_samples = nullptr;
50 physx::PxHeightField *m_heightField = nullptr;
51 int m_rows = 0;
52 int m_columns = 0;
53 int refCount = 0;
54};
55
56class QQuick3DPhysicsHeightFieldManager
57{
58public:
59 static QQuick3DPhysicsHeightField *getHeightField(const QUrl &source,
60 const QObject *contextObject);
61 static void releaseHeightField(QQuick3DPhysicsHeightField *heightField);
62
63private:
64 static QHash<QString, QQuick3DPhysicsHeightField *> heightFieldHash;
65};
66
67QHash<QString, QQuick3DPhysicsHeightField *> QQuick3DPhysicsHeightFieldManager::heightFieldHash;
68
69QQuick3DPhysicsHeightField *
70QQuick3DPhysicsHeightFieldManager::getHeightField(const QUrl &source, const QObject *contextObject)
71{
72 const QQmlContext *context = qmlContext(contextObject);
73
74 const auto resolvedUrl = context ? context->resolvedUrl(source) : source;
75 const auto qmlSource = QQmlFile::urlToLocalFileOrQrc(resolvedUrl);
76
77 auto *heightField = heightFieldHash.value(key: qmlSource);
78 if (!heightField) {
79 heightField = new QQuick3DPhysicsHeightField(qmlSource);
80 heightFieldHash[qmlSource] = heightField;
81 }
82 heightField->ref();
83 return heightField;
84}
85
86void QQuick3DPhysicsHeightFieldManager::releaseHeightField(QQuick3DPhysicsHeightField *heightField)
87{
88 if (heightField->deref() == 0) {
89 qCDebug(lcQuick3dPhysics()) << "deleting height field" << heightField;
90 erase_if(hash&: heightFieldHash,
91 pred: [heightField](std::pair<const QString &, QQuick3DPhysicsHeightField *&> h) {
92 return h.second == heightField;
93 });
94 delete heightField;
95 }
96}
97
98QQuick3DPhysicsHeightField::QQuick3DPhysicsHeightField(const QString &qmlSource)
99 : m_sourcePath(qmlSource)
100{
101}
102
103QQuick3DPhysicsHeightField::~QQuick3DPhysicsHeightField()
104{
105 free(ptr: m_samples);
106}
107
108physx::PxHeightFieldSample *QQuick3DPhysicsHeightField::getSamples()
109{
110 if (!m_samples && !m_sourcePath.isEmpty()) {
111 QImage heightMap(m_sourcePath);
112
113 m_rows = heightMap.height();
114 m_columns = heightMap.width();
115 int numRows = m_rows;
116 int numCols = m_columns;
117
118 auto samples = reinterpret_cast<physx::PxHeightFieldSample *>(
119 malloc(size: sizeof(physx::PxHeightFieldSample) * (numRows * numCols)));
120 for (int i = 0; i < numCols; i++)
121 for (int j = 0; j < numRows; j++) {
122 float f = heightMap.pixelColor(x: i, y: j).valueF() - 0.5;
123 // qDebug() << i << j << f;
124 samples[i * numRows + j] = { .height: qint16(0xffff * f), .materialIndex0: 0,
125 .materialIndex1: 0 }; //{qint16(i%3*2 + j), 0, 0};
126 }
127 m_samples = samples;
128 }
129 return m_samples;
130}
131
132physx::PxHeightField *QQuick3DPhysicsHeightField::heightField()
133{
134 if (m_heightField)
135 return m_heightField;
136
137 physx::PxPhysics *thePhysics = QPhysicsWorld::getPhysics();
138 if (thePhysics == nullptr)
139 return nullptr;
140
141 m_heightField = QCacheUtils::readCachedHeightField(filePath: m_sourcePath, physics&: *thePhysics);
142 if (m_heightField != nullptr) {
143 m_rows = m_heightField->getNbRows();
144 m_columns = m_heightField->getNbColumns();
145 return m_heightField;
146 }
147
148 m_heightField = QCacheUtils::readCookedHeightField(filePath: m_sourcePath, physics&: *thePhysics);
149 if (m_heightField != nullptr) {
150 m_rows = m_heightField->getNbRows();
151 m_columns = m_heightField->getNbColumns();
152 return m_heightField;
153 }
154
155 getSamples();
156 int numRows = m_rows;
157 int numCols = m_columns;
158 auto samples = m_samples;
159
160 physx::PxHeightFieldDesc hfDesc;
161 hfDesc.format = physx::PxHeightFieldFormat::eS16_TM;
162 hfDesc.nbColumns = numRows;
163 hfDesc.nbRows = numCols;
164 hfDesc.samples.data = samples;
165 hfDesc.samples.stride = sizeof(physx::PxHeightFieldSample);
166
167 physx::PxDefaultMemoryOutputStream buf;
168
169 const auto cooking = QPhysicsWorld::getCooking();
170 if (numRows && numCols && cooking && cooking->cookHeightField(desc: hfDesc, stream&: buf)) {
171 auto size = buf.getSize();
172 auto *data = buf.getData();
173 physx::PxDefaultMemoryInputData input(data, size);
174 m_heightField = thePhysics->createHeightField(stream&: input);
175 qCDebug(lcQuick3dPhysics) << "created height field" << m_heightField << numCols << numRows
176 << "from" << m_sourcePath;
177 QCacheUtils::writeCachedHeightField(filePath: m_sourcePath, buf);
178 } else {
179 qCWarning(lcQuick3dPhysics) << "Could not create height field from" << m_sourcePath;
180 }
181
182 return m_heightField;
183}
184
185int QQuick3DPhysicsHeightField::rows() const
186{
187 return m_rows;
188}
189
190int QQuick3DPhysicsHeightField::columns() const
191{
192 return m_columns;
193}
194
195/*!
196 \qmltype HeightFieldShape
197 \inqmlmodule QtQuick3D.Physics
198 \inherits CollisionShape
199 \since 6.4
200 \brief A collision shape where the elevation is defined by a height map.
201
202 The HeightFieldShape type defines a physical surface where the height is determined by
203 the \l {QColor#The HSV Color Model}{value} of the pixels of the \l {source} image. The
204 x-axis of the image is mapped to the positive x-axis of the scene, and the y-axis of the
205 image is mapped to the negative z-axis of the scene. A typical use case is to represent
206 natural terrain.
207
208 Objects that are controlled by the physics simulation cannot use HeightFieldShape: It can only
209 be used with \l StaticRigidBody and \l {DynamicRigidBody::isKinematic}{kinematic bodies}.
210
211 \l [QtQuick3D]{HeightFieldGeometry}{QtQuick3D.Helpers.HeightFieldGeometry} is API compatible
212 with the HeightFieldShape type, and can be used to show the height field visually. To
213 improve performance, use a lower resolution version of the height map for the HeightFieldShape:
214 As long as the \l{extents} and the image aspect ratio are the same, the physics body and the
215 visual item will overlap.
216
217 \sa {Qt Quick 3D Physics Shapes and Bodies}{Shapes and Bodies overview documentation}
218*/
219
220/*!
221 \qmlproperty vector3d HeightFieldShape::extents
222 This property defines the extents of the height field. The default value
223 is \c{(100, 100, 100)} when the heightMap is square. If the heightMap is
224 non-square, the default value is reduced along the x- or z-axis, so the height
225 field will keep the aspect ratio of the image.
226*/
227
228/*!
229 \qmlproperty url HeightFieldShape::source
230 This property defines the location of the heightMap file.
231
232 Internally, HeightFieldShape converts the height map image to an optimized data structure. This
233 conversion can be done in advance. See the \l{Qt Quick 3D Physics Cooking}{cooking overview
234 documentation} for details.
235
236*/
237
238QHeightFieldShape::QHeightFieldShape() = default;
239
240QHeightFieldShape::~QHeightFieldShape()
241{
242 delete m_heightFieldGeometry;
243 if (m_heightField)
244 QQuick3DPhysicsHeightFieldManager::releaseHeightField(heightField: m_heightField);
245}
246
247physx::PxGeometry *QHeightFieldShape::getPhysXGeometry()
248{
249 if (m_dirtyPhysx || m_scaleDirty || !m_heightFieldGeometry) {
250 updatePhysXGeometry();
251 }
252 return m_heightFieldGeometry;
253}
254
255void QHeightFieldShape::updatePhysXGeometry()
256{
257 delete m_heightFieldGeometry;
258 m_heightFieldGeometry = nullptr;
259 if (!m_heightField)
260 return;
261
262 auto *hf = m_heightField->heightField();
263 float rows = m_heightField->rows();
264 float cols = m_heightField->columns();
265 updateExtents();
266 if (hf && cols > 1 && rows > 1) {
267 QVector3D scaledExtents = m_extents * sceneScale();
268 m_heightFieldGeometry = new physx::PxHeightFieldGeometry(
269 hf, physx::PxMeshGeometryFlags(), scaledExtents.y() / 0x10000,
270 scaledExtents.x() / (cols - 1), scaledExtents.z() / (rows - 1));
271 m_hfOffset = { -scaledExtents.x() / 2, 0, -scaledExtents.z() / 2 };
272
273 qCDebug(lcQuick3dPhysics) << "created height field geom" << m_heightFieldGeometry << "scale"
274 << scaledExtents << m_heightField->columns()
275 << m_heightField->rows();
276 }
277 m_dirtyPhysx = false;
278}
279
280void QHeightFieldShape::updateExtents()
281{
282 if (!m_heightField || m_extentsSetExplicitly)
283 return;
284 int numRows = m_heightField->rows();
285 int numCols = m_heightField->columns();
286 auto prevExt = m_extents;
287 if (numRows == numCols) {
288 m_extents = { 100, 100, 100 };
289 } else if (numRows < numCols) {
290 float f = float(numRows) / float(numCols);
291 m_extents = { 100.f, 100.f, 100.f * f };
292 } else {
293 float f = float(numCols) / float(numRows);
294 m_extents = { 100.f * f, 100.f, 100.f };
295 }
296 if (m_extents != prevExt) {
297 emit extentsChanged();
298 }
299}
300
301const QUrl &QHeightFieldShape::source() const
302{
303 return m_heightMapSource;
304}
305
306void QHeightFieldShape::setSource(const QUrl &newSource)
307{
308 if (m_heightMapSource == newSource)
309 return;
310 m_heightMapSource = newSource;
311
312 m_heightField = QQuick3DPhysicsHeightFieldManager::getHeightField(source: m_heightMapSource, contextObject: this);
313
314 m_dirtyPhysx = true;
315
316 emit needsRebuild(this);
317 emit sourceChanged();
318}
319
320const QVector3D &QHeightFieldShape::extents() const
321{
322 return m_extents;
323}
324
325void QHeightFieldShape::setExtents(const QVector3D &newExtents)
326{
327 m_extentsSetExplicitly = true;
328 if (m_extents == newExtents)
329 return;
330 m_extents = newExtents;
331
332 m_dirtyPhysx = true;
333
334 emit needsRebuild(this);
335 emit extentsChanged();
336}
337
338QT_END_NAMESPACE
339

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