Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ // Security note: This file reads and cooks meshes but since it is a tool it is safe
++
++ #include "cooking/PxCooking.h"
++
++ #include <QtQuick3DUtils/private/qssgmesh_p.h>
++ #include <QtQuick3DPhysics/private/qcacheutils_p.h>
++
++ #include <QtCore/QFile>
++ #include <QtCore/QFileInfo>
++ #include <QtGui/QImage>
++ #include <QCommandLineParser>
++ #include <QScopeGuard>
++
++ #include "PxPhysicsAPI.h"
++ #include "cooking/PxCooking.h"
++
++ #include <iostream>
++
++ bool tryReadMesh(QFile *file, QSSGMesh::Mesh &mesh)
++ {
++ auto device = QSharedPointer<QIODevice>(file);
++ const quint32 id = 1;
++ mesh = QSSGMesh::Mesh::loadMesh(device.data(), id);
++ return mesh.isValid();
++ }
++
++ bool tryReadImage(const QString &inputPath, QImage &image)
++ {
++ image = QImage(inputPath);
++ return image.format() != QImage::Format_Invalid;
++ }
++
++ bool cookMeshes(const QString &inputPath, QSSGMesh::Mesh &mesh,
++ const physx::PxCookingParams &cookingParams)
++ {
++
++ const int vStride = mesh.vertexBuffer().stride;
++ const int vCount = mesh.vertexBuffer().data.size() / vStride;
++ const auto *vd = mesh.vertexBuffer().data.constData();
++
++ const int iStride = mesh.indexBuffer().componentType == QSSGMesh::Mesh::ComponentType::UnsignedInt16 ? 2 : 4;
++ const int iCount = mesh.indexBuffer().data.size() / iStride;
++
++ int m_posOffset = 0;
++
++ for (auto &v : mesh.vertexBuffer().entries) {
++ Q_ASSERT(v.componentType == QSSGMesh::Mesh::ComponentType::Float32);
++ if (v.name == "attr_pos")
++ m_posOffset = v.offset;
++ }
++
++ { // Triangle mesh
++ physx::PxTriangleMeshCookingResult::Enum result;
++ physx::PxTriangleMeshDesc triangleDesc;
++ triangleDesc.points.count = vCount;
++ triangleDesc.points.stride = vStride;
++ triangleDesc.points.data = vd + m_posOffset;
++
++ triangleDesc.flags = {}; //??? physx::PxMeshFlag::eFLIPNORMALS or
++ // physx::PxMeshFlag::e16_BIT_INDICES
++ triangleDesc.triangles.count = iCount / 3;
++ triangleDesc.triangles.stride = iStride * 3;
++ triangleDesc.triangles.data = mesh.indexBuffer().data.constData();
++
++ physx::PxDefaultMemoryOutputStream buf;
++ if (!::PxCookTriangleMesh(cookingParams, triangleDesc, buf, &result)) {
++ std::cerr << "Error: could not cook triangle mesh '" << inputPath.toStdString() << "'." << std::endl;
++ return false;
++ }
++
++ auto size = buf.getSize();
++ auto *data = buf.getData();
++ physx::PxDefaultMemoryInputData input(data, size);
++
++ QString output = QFileInfo(inputPath).baseName() + QString(".cooked.tri");
++ auto outputFile = QFile(output);
++
++ if (!outputFile.open(QIODevice::WriteOnly)) {
++ std::cerr << "Error: could not open '" << output.toStdString() << "' for writing." << std::endl;
++ return false;
++ }
++
++ outputFile.write(reinterpret_cast<char *>(buf.getData()), buf.getSize());
++ outputFile.close();
++
++ std::cout << "Success: wrote triangle mesh '" << output.toStdString() << "'." << std::endl;
++ }
++
++ { // Convex mesh
++ physx::PxConvexMeshCookingResult::Enum result;
++ QVector<physx::PxVec3> verts;
++
++ for (int i = 0; i < vCount; ++i) {
++ auto *vp = reinterpret_cast<const QVector3D *>(vd + vStride * i + m_posOffset);
++ verts << physx::PxVec3 { vp->x(), vp->y(), vp->z() };
++ }
++
++ const auto *convexVerts = verts.constData();
++
++ physx::PxConvexMeshDesc convexDesc;
++ convexDesc.points.count = vCount;
++ convexDesc.points.stride = sizeof(physx::PxVec3);
++ convexDesc.points.data = convexVerts;
++ convexDesc.flags = physx::PxConvexFlag::eCOMPUTE_CONVEX;
++
++ physx::PxDefaultMemoryOutputStream buf;
++ if (!::PxCookConvexMesh(cookingParams, convexDesc, buf, &result)) {
++ std::cerr << "Error: could not cook convex mesh '" << inputPath.toStdString() << "'." << std::endl;
++ return false;
++ }
++
++ auto size = buf.getSize();
++ auto *data = buf.getData();
++ physx::PxDefaultMemoryInputData input(data, size);
++
++ QString output = QFileInfo(inputPath).baseName() + QString(".cooked.cvx");
++ auto outputFile = QFile(output);
++
++ if (!outputFile.open(QIODevice::WriteOnly)) {
++ std::cerr << "Error: could not open '" << output.toStdString() << "' for writing." << std::endl;
++ return false;
++ }
++
++ outputFile.write(reinterpret_cast<char *>(buf.getData()), buf.getSize());
++ outputFile.close();
++
++ std::cout << "Success: wrote convex mesh '" << output.toStdString() << "'." << std::endl;
++ }
++
++ return true;
++ }
++
++ bool cookHeightfield(const QString &inputPath, QImage &heightMap)
++ {
++ int numRows = heightMap.height();
++ int numCols = heightMap.width();
++
++ auto samples = reinterpret_cast<physx::PxHeightFieldSample *>(malloc(sizeof(physx::PxHeightFieldSample) * (numRows * numCols)));
++ for (int i = 0; i < numCols; i++) {
++ for (int j = 0; j < numRows; j++) {
++ float f = heightMap.pixelColor(i, j).valueF() - 0.5;
++ samples[i * numRows + j] = { qint16(0xffff * f), 0, 0 };
++ }
++ }
++
++ physx::PxHeightFieldDesc hfDesc;
++ hfDesc.format = physx::PxHeightFieldFormat::eS16_TM;
++ hfDesc.nbColumns = numRows;
++ hfDesc.nbRows = numCols;
++ hfDesc.samples.data = samples;
++ hfDesc.samples.stride = sizeof(physx::PxHeightFieldSample);
++
++ physx::PxDefaultMemoryOutputStream buf;
++ if (!(numRows && numCols && ::PxCookHeightField(hfDesc, buf))) {
++ std::cerr << "Could not create height field from '" << inputPath.toStdString() << "'." << std::endl;
++ return false;
++ }
++
++ QString output = QFileInfo(inputPath).baseName() + QString(".cooked.hf");
++ auto outputFile = QFile(output);
++
++ if (!outputFile.open(QIODevice::WriteOnly)) {
++ std::cerr << "Could not open '" << output.toStdString() << "' for writing." << std::endl;
++ return false;
++ }
++
++ outputFile.write(reinterpret_cast<char *>(buf.getData()), buf.getSize());
++ outputFile.close();
++ std::cout << "Success: wrote height field '" << output.toStdString() << "'" << std::endl;
++
++ return true;
++ }
++
++ int main(int argc, char *argv[])
++ {
++ QCoreApplication app(argc, argv);
++ QCoreApplication::setApplicationName("cooker");
++ QCoreApplication::setApplicationVersion("6.5.7");
++
++ QCommandLineParser parser;
++ parser.setApplicationDescription(
++ "A commandline utility for pre-cooking meshes for use with the QtQuick3DPhysics module.");
++ parser.addHelpOption();
++ parser.addVersionOption();
++ parser.addPositionalArgument("input",
++ "The input file(s). Accepts either a .mesh created by QtQuick3D's balsam"
++ " or a Qt compatible image file. The output filename will be of the format"
++ " input.cooked.{cvx/tri/hf}. The filename suffixes .cvx, .tri, and .hf"
++ " mean it is a convex mesh, a triangle mesh or a heightfield.");
++ parser.process(app);
++
++ const QStringList args = parser.positionalArguments();
++ if (args.isEmpty())
++ parser.showHelp(0);
++
++ physx::PxDefaultErrorCallback defaultErrorCallback;
++ physx::PxDefaultAllocator defaultAllocatorCallback;
++ auto foundation = PxCreateFoundation(PX_PHYSICS_VERSION, defaultAllocatorCallback, defaultErrorCallback);
++ auto cleanup = qScopeGuard([&] {
++ foundation->release();
++ });
++
++ const physx::PxCookingParams cookingParams { physx::PxTolerancesScale() };
++
++ for (const QString &inputPath : args) {
++ QFile *file = new QFile(inputPath);
++ if (!file->open(QIODevice::ReadOnly)) {
++ delete file;
++ std::cerr << "Error: could not open input file '" << inputPath.toStdString() << "'" << std::endl;
++ return -1;
++ }
++
++ QImage image;
++ QSSGMesh::Mesh mesh;
++ if (tryReadImage(inputPath, image)) {
++ if (!cookHeightfield(inputPath, image))
++ return -1;
++ } else if (tryReadMesh(file, mesh)) {
++ if (!cookMeshes(inputPath, mesh, cookingParams))
++ return -1;
++ } else {
++ std::cerr << "Error: failed to read mesh or image from file '" << inputPath.toStdString() << "'" << std::endl;
++ return -1;
++ }
++ }
++
++ return 0;
++ }