| 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(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 | |
| 48 | private: |
| 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 | |
| 61 | class QQuick3DPhysicsHeightFieldManager |
| 62 | { |
| 63 | public: |
| 64 | static QQuick3DPhysicsHeightField *getHeightField(const QUrl &source, |
| 65 | const QObject *contextObject); |
| 66 | static QQuick3DPhysicsHeightField *getHeightField(QQuickImage *source); |
| 67 | static void releaseHeightField(QQuick3DPhysicsHeightField *heightField); |
| 68 | |
| 69 | private: |
| 70 | static QHash<QString, QQuick3DPhysicsHeightField *> heightFieldHash; |
| 71 | static QHash<QQuickImage *, QQuick3DPhysicsHeightField *> heightFieldImageHash; |
| 72 | }; |
| 73 | |
| 74 | QHash<QString, QQuick3DPhysicsHeightField *> QQuick3DPhysicsHeightFieldManager::heightFieldHash; |
| 75 | QHash<QQuickImage *, QQuick3DPhysicsHeightField *> |
| 76 | QQuick3DPhysicsHeightFieldManager::heightFieldImageHash; |
| 77 | |
| 78 | QQuick3DPhysicsHeightField * |
| 79 | QQuick3DPhysicsHeightFieldManager::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 | |
| 95 | QQuick3DPhysicsHeightField *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 | |
| 106 | void 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 | |
| 122 | QQuick3DPhysicsHeightField::QQuick3DPhysicsHeightField(const QString &qmlSource) |
| 123 | : m_sourcePath(qmlSource) |
| 124 | { |
| 125 | } |
| 126 | |
| 127 | QQuick3DPhysicsHeightField::QQuick3DPhysicsHeightField(QQuickImage *image) : m_image(image) { } |
| 128 | |
| 129 | QQuick3DPhysicsHeightField::~QQuick3DPhysicsHeightField() |
| 130 | { |
| 131 | free(ptr: m_samples); |
| 132 | } |
| 133 | |
| 134 | void 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 | |
| 152 | physx::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 | |
| 223 | int QQuick3DPhysicsHeightField::rows() const |
| 224 | { |
| 225 | return m_rows; |
| 226 | } |
| 227 | |
| 228 | int 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 | |
| 293 | QHeightFieldShape::QHeightFieldShape() = default; |
| 294 | |
| 295 | QHeightFieldShape::~QHeightFieldShape() |
| 296 | { |
| 297 | delete m_heightFieldGeometry; |
| 298 | if (m_heightField) |
| 299 | QQuick3DPhysicsHeightFieldManager::releaseHeightField(heightField: m_heightField); |
| 300 | } |
| 301 | |
| 302 | physx::PxGeometry *QHeightFieldShape::getPhysXGeometry() |
| 303 | { |
| 304 | if (m_dirtyPhysx || m_scaleDirty || !m_heightFieldGeometry) { |
| 305 | updatePhysXGeometry(); |
| 306 | } |
| 307 | return m_heightFieldGeometry; |
| 308 | } |
| 309 | |
| 310 | void 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 | |
| 335 | void 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 | |
| 356 | const QUrl &QHeightFieldShape::source() const |
| 357 | { |
| 358 | return m_heightMapSource; |
| 359 | } |
| 360 | |
| 361 | void 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 | |
| 384 | QQuickImage *QHeightFieldShape::image() const |
| 385 | { |
| 386 | return m_image; |
| 387 | } |
| 388 | |
| 389 | void 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 | |
| 419 | void 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 | |
| 426 | void 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 | |
| 436 | const QVector3D &QHeightFieldShape::extents() const |
| 437 | { |
| 438 | return m_extents; |
| 439 | } |
| 440 | |
| 441 | void 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 | |
| 454 | QT_END_NAMESPACE |
| 455 | |