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 | |