| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "cooking/PxCooking.h" |
| 5 | |
| 6 | #include <QtQuick3DUtils/private/qssgmesh_p.h> |
| 7 | #include <QtQuick3DPhysics/private/qcacheutils_p.h> |
| 8 | |
| 9 | #include <QtCore/QFile> |
| 10 | #include <QtCore/QFileInfo> |
| 11 | #include <QtGui/QImage> |
| 12 | #include <QCommandLineParser> |
| 13 | #include <QScopeGuard> |
| 14 | |
| 15 | #include "PxPhysicsAPI.h" |
| 16 | #include "cooking/PxCooking.h" |
| 17 | |
| 18 | #include <iostream> |
| 19 | |
| 20 | bool tryReadMesh(QFile *file, QSSGMesh::Mesh &mesh) |
| 21 | { |
| 22 | auto device = QSharedPointer<QIODevice>(file); |
| 23 | const quint32 id = 1; |
| 24 | mesh = QSSGMesh::Mesh::loadMesh(device: device.data(), id); |
| 25 | return mesh.isValid(); |
| 26 | } |
| 27 | |
| 28 | bool tryReadImage(const QString &inputPath, QImage &image) |
| 29 | { |
| 30 | image = QImage(inputPath); |
| 31 | return image.format() != QImage::Format_Invalid; |
| 32 | } |
| 33 | |
| 34 | bool cookMeshes(const QString &inputPath, QSSGMesh::Mesh &mesh, physx::PxCooking *cooking) |
| 35 | { |
| 36 | Q_ASSERT(cooking); |
| 37 | |
| 38 | const int vStride = mesh.vertexBuffer().stride; |
| 39 | const int vCount = mesh.vertexBuffer().data.size() / vStride; |
| 40 | const auto *vd = mesh.vertexBuffer().data.constData(); |
| 41 | |
| 42 | const int iStride = mesh.indexBuffer().componentType == QSSGMesh::Mesh::ComponentType::UnsignedInt16 ? 2 : 4; |
| 43 | const int iCount = mesh.indexBuffer().data.size() / iStride; |
| 44 | |
| 45 | int m_posOffset = 0; |
| 46 | |
| 47 | for (auto &v : mesh.vertexBuffer().entries) { |
| 48 | Q_ASSERT(v.componentType == QSSGMesh::Mesh::ComponentType::Float32); |
| 49 | if (v.name == "attr_pos" ) |
| 50 | m_posOffset = v.offset; |
| 51 | } |
| 52 | |
| 53 | { // Triangle mesh |
| 54 | physx::PxTriangleMeshCookingResult::Enum result; |
| 55 | physx::PxTriangleMeshDesc triangleDesc; |
| 56 | triangleDesc.points.count = vCount; |
| 57 | triangleDesc.points.stride = vStride; |
| 58 | triangleDesc.points.data = vd + m_posOffset; |
| 59 | |
| 60 | triangleDesc.flags = {}; //??? physx::PxMeshFlag::eFLIPNORMALS or |
| 61 | // physx::PxMeshFlag::e16_BIT_INDICES |
| 62 | triangleDesc.triangles.count = iCount / 3; |
| 63 | triangleDesc.triangles.stride = iStride * 3; |
| 64 | triangleDesc.triangles.data = mesh.indexBuffer().data.constData(); |
| 65 | |
| 66 | physx::PxDefaultMemoryOutputStream buf; |
| 67 | if (!cooking->cookTriangleMesh(desc: triangleDesc, stream&: buf, condition: &result)) { |
| 68 | std::cerr << "Error: could not cook triangle mesh '" << inputPath.toStdString() << "'." << std::endl; |
| 69 | return false; |
| 70 | } |
| 71 | |
| 72 | auto size = buf.getSize(); |
| 73 | auto *data = buf.getData(); |
| 74 | physx::PxDefaultMemoryInputData input(data, size); |
| 75 | |
| 76 | QString output = QFileInfo(inputPath).baseName() + QString(".cooked.tri" ); |
| 77 | auto outputFile = QFile(output); |
| 78 | |
| 79 | if (!outputFile.open(flags: QIODevice::WriteOnly)) { |
| 80 | std::cerr << "Error: could not open '" << output.toStdString() << "' for writing." << std::endl; |
| 81 | return false; |
| 82 | } |
| 83 | |
| 84 | outputFile.write(data: reinterpret_cast<char *>(buf.getData()), len: buf.getSize()); |
| 85 | outputFile.close(); |
| 86 | |
| 87 | std::cout << "Success: wrote triangle mesh '" << output.toStdString() << "'." << std::endl; |
| 88 | } |
| 89 | |
| 90 | { // Convex mesh |
| 91 | physx::PxConvexMeshCookingResult::Enum result; |
| 92 | QVector<physx::PxVec3> verts; |
| 93 | |
| 94 | for (int i = 0; i < vCount; ++i) { |
| 95 | auto *vp = reinterpret_cast<const QVector3D *>(vd + vStride * i + m_posOffset); |
| 96 | verts << physx::PxVec3 { vp->x(), vp->y(), vp->z() }; |
| 97 | } |
| 98 | |
| 99 | const auto *convexVerts = verts.constData(); |
| 100 | |
| 101 | physx::PxConvexMeshDesc convexDesc; |
| 102 | convexDesc.points.count = vCount; |
| 103 | convexDesc.points.stride = sizeof(physx::PxVec3); |
| 104 | convexDesc.points.data = convexVerts; |
| 105 | convexDesc.flags = physx::PxConvexFlag::eCOMPUTE_CONVEX; |
| 106 | |
| 107 | physx::PxDefaultMemoryOutputStream buf; |
| 108 | if (!cooking->cookConvexMesh(desc: convexDesc, stream&: buf, condition: &result)) { |
| 109 | std::cerr << "Error: could not cook convex mesh '" << inputPath.toStdString() << "'." << std::endl; |
| 110 | return false; |
| 111 | } |
| 112 | |
| 113 | auto size = buf.getSize(); |
| 114 | auto *data = buf.getData(); |
| 115 | physx::PxDefaultMemoryInputData input(data, size); |
| 116 | |
| 117 | QString output = QFileInfo(inputPath).baseName() + QString(".cooked.cvx" ); |
| 118 | auto outputFile = QFile(output); |
| 119 | |
| 120 | if (!outputFile.open(flags: QIODevice::WriteOnly)) { |
| 121 | std::cerr << "Error: could not open '" << output.toStdString() << "' for writing." << std::endl; |
| 122 | return false; |
| 123 | } |
| 124 | |
| 125 | outputFile.write(data: reinterpret_cast<char *>(buf.getData()), len: buf.getSize()); |
| 126 | outputFile.close(); |
| 127 | |
| 128 | std::cout << "Success: wrote convex mesh '" << output.toStdString() << "'." << std::endl; |
| 129 | } |
| 130 | |
| 131 | return true; |
| 132 | } |
| 133 | |
| 134 | bool cookHeightfield(const QString &inputPath, QImage &heightMap, physx::PxCooking *cooking) |
| 135 | { |
| 136 | Q_ASSERT(cooking); |
| 137 | |
| 138 | int numRows = heightMap.height(); |
| 139 | int numCols = heightMap.width(); |
| 140 | |
| 141 | auto samples = reinterpret_cast<physx::PxHeightFieldSample *>(malloc(size: sizeof(physx::PxHeightFieldSample) * (numRows * numCols))); |
| 142 | for (int i = 0; i < numCols; i++) { |
| 143 | for (int j = 0; j < numRows; j++) { |
| 144 | float f = heightMap.pixelColor(x: i, y: j).valueF() - 0.5; |
| 145 | samples[i * numRows + j] = { .height: qint16(0xffff * f), .materialIndex0: 0, .materialIndex1: 0 }; |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | physx::PxHeightFieldDesc hfDesc; |
| 150 | hfDesc.format = physx::PxHeightFieldFormat::eS16_TM; |
| 151 | hfDesc.nbColumns = numRows; |
| 152 | hfDesc.nbRows = numCols; |
| 153 | hfDesc.samples.data = samples; |
| 154 | hfDesc.samples.stride = sizeof(physx::PxHeightFieldSample); |
| 155 | |
| 156 | physx::PxDefaultMemoryOutputStream buf; |
| 157 | if (!(numRows && numCols && cooking->cookHeightField(desc: hfDesc, stream&: buf))) { |
| 158 | std::cerr << "Could not create height field from '" << inputPath.toStdString() << "'." << std::endl; |
| 159 | return false; |
| 160 | } |
| 161 | |
| 162 | QString output = QFileInfo(inputPath).baseName() + QString(".cooked.hf" ); |
| 163 | auto outputFile = QFile(output); |
| 164 | |
| 165 | if (!outputFile.open(flags: QIODevice::WriteOnly)) { |
| 166 | std::cerr << "Could not open '" << output.toStdString() << "' for writing." << std::endl; |
| 167 | return false; |
| 168 | } |
| 169 | |
| 170 | outputFile.write(data: reinterpret_cast<char *>(buf.getData()), len: buf.getSize()); |
| 171 | outputFile.close(); |
| 172 | std::cout << "Success: wrote height field '" << output.toStdString() << "'" << std::endl; |
| 173 | |
| 174 | return true; |
| 175 | } |
| 176 | |
| 177 | int main(int argc, char *argv[]) |
| 178 | { |
| 179 | QCoreApplication app(argc, argv); |
| 180 | QCoreApplication::setApplicationName("cooker" ); |
| 181 | QCoreApplication::setApplicationVersion("6.5.7" ); |
| 182 | |
| 183 | QCommandLineParser parser; |
| 184 | parser.setApplicationDescription( |
| 185 | "A commandline utility for pre-cooking meshes for use with the QtQuick3DPhysics module." ); |
| 186 | parser.addHelpOption(); |
| 187 | parser.addVersionOption(); |
| 188 | parser.addPositionalArgument(name: "input" , |
| 189 | description: "The input file(s). Accepts either a .mesh created by QtQuick3D's balsam" |
| 190 | " or a Qt compatible image file. The output filename will be of the format" |
| 191 | " input.cooked.{cvx/tri/hf}. The filename suffixes .cvx, .tri, and .hf" |
| 192 | " mean it is a convex mesh, a triangle mesh or a heightfield." ); |
| 193 | parser.process(app); |
| 194 | |
| 195 | const QStringList args = parser.positionalArguments(); |
| 196 | if (args.isEmpty()) |
| 197 | parser.showHelp(exitCode: 0); |
| 198 | |
| 199 | physx::PxDefaultErrorCallback defaultErrorCallback; |
| 200 | physx::PxDefaultAllocator defaultAllocatorCallback; |
| 201 | auto foundation = PxCreateFoundation(PX_PHYSICS_VERSION, allocator&: defaultAllocatorCallback, errorCallback&: defaultErrorCallback); |
| 202 | auto cooking = PxCreateCooking(PX_PHYSICS_VERSION, foundation&: *foundation, params: physx::PxCookingParams(physx::PxTolerancesScale())); |
| 203 | auto cleanup = qScopeGuard(f: [&] { |
| 204 | cooking->release(); |
| 205 | foundation->release(); |
| 206 | }); |
| 207 | |
| 208 | for (const QString &inputPath : args) { |
| 209 | QFile *file = new QFile(inputPath); |
| 210 | if (!file->open(flags: QIODevice::ReadOnly)) { |
| 211 | delete file; |
| 212 | std::cerr << "Error: could not open input file '" << inputPath.toStdString() << "'" << std::endl; |
| 213 | return -1; |
| 214 | } |
| 215 | |
| 216 | QImage image; |
| 217 | QSSGMesh::Mesh mesh; |
| 218 | if (tryReadImage(inputPath, image)) { |
| 219 | if (!cookHeightfield(inputPath, heightMap&: image, cooking)) |
| 220 | return -1; |
| 221 | } else if (tryReadMesh(file, mesh)) { |
| 222 | if (!cookMeshes(inputPath, mesh, cooking)) |
| 223 | return -1; |
| 224 | } else { |
| 225 | std::cerr << "Error: failed to read mesh or image from file '" << inputPath.toStdString() << "'" << std::endl; |
| 226 | return -1; |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | return 0; |
| 231 | } |
| 232 | |