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 | |
28 | QT_BEGIN_NAMESPACE |
29 | |
30 | // TODO: Unify with QQuick3DPhysicsMeshManager??? It's the same basic logic, |
31 | // but we're using images instead of meshes. |
32 | |
33 | class QQuick3DPhysicsHeightField |
34 | { |
35 | public: |
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 | |
47 | private: |
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 | |
56 | class QQuick3DPhysicsHeightFieldManager |
57 | { |
58 | public: |
59 | static QQuick3DPhysicsHeightField *getHeightField(const QUrl &source, |
60 | const QObject *contextObject); |
61 | static void releaseHeightField(QQuick3DPhysicsHeightField *heightField); |
62 | |
63 | private: |
64 | static QHash<QString, QQuick3DPhysicsHeightField *> heightFieldHash; |
65 | }; |
66 | |
67 | QHash<QString, QQuick3DPhysicsHeightField *> QQuick3DPhysicsHeightFieldManager::heightFieldHash; |
68 | |
69 | QQuick3DPhysicsHeightField * |
70 | QQuick3DPhysicsHeightFieldManager::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 | |
86 | void 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 | |
98 | QQuick3DPhysicsHeightField::QQuick3DPhysicsHeightField(const QString &qmlSource) |
99 | : m_sourcePath(qmlSource) |
100 | { |
101 | } |
102 | |
103 | QQuick3DPhysicsHeightField::~QQuick3DPhysicsHeightField() |
104 | { |
105 | free(ptr: m_samples); |
106 | } |
107 | |
108 | physx::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 | |
132 | physx::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 | |
185 | int QQuick3DPhysicsHeightField::rows() const |
186 | { |
187 | return m_rows; |
188 | } |
189 | |
190 | int 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 | |
238 | QHeightFieldShape::QHeightFieldShape() = default; |
239 | |
240 | QHeightFieldShape::~QHeightFieldShape() |
241 | { |
242 | delete m_heightFieldGeometry; |
243 | if (m_heightField) |
244 | QQuick3DPhysicsHeightFieldManager::releaseHeightField(heightField: m_heightField); |
245 | } |
246 | |
247 | physx::PxGeometry *QHeightFieldShape::getPhysXGeometry() |
248 | { |
249 | if (m_dirtyPhysx || m_scaleDirty || !m_heightFieldGeometry) { |
250 | updatePhysXGeometry(); |
251 | } |
252 | return m_heightFieldGeometry; |
253 | } |
254 | |
255 | void 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 | |
280 | void 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 | |
301 | const QUrl &QHeightFieldShape::source() const |
302 | { |
303 | return m_heightMapSource; |
304 | } |
305 | |
306 | void 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 | |
320 | const QVector3D &QHeightFieldShape::extents() const |
321 | { |
322 | return m_extents; |
323 | } |
324 | |
325 | void 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 | |
338 | QT_END_NAMESPACE |
339 | |