载入中...
搜索中...
未找到
UnityFbx.cpp
浏览该文件的文档.
4
5#if defined(__EMSCRIPTEN__)
6namespace eve::asset_import {
7Result<PreparedAssetImport> prepareUnityFbx(const UnityProjectImportRequest&, const UnitySourceAsset& source) {
9 "FBX conversion requires the desktop Assimp provider",
10 source.path, {}, "asset.import"));
11}
12} // namespace eve::asset_import
13#else
14#include <assimp/postprocess.h>
15#include <assimp/scene.h>
16#include <xxHash/xxhash.h>
17#include <assimp/IOSystem.hpp>
18#include <assimp/Importer.hpp>
19
20#include <algorithm>
21#include <bit>
22#include <cmath>
23#include <filesystem>
24#include <functional>
25#include <regex>
26#include <set>
27
28namespace eve::asset_import {
29namespace {
30// Third-party IO boundary: the in-memory FBX is the only authorized input.
31class NoExternalIO final : public Assimp::IOSystem {
32public:
33 bool Exists(const char*) const override { return false; }
34 char getOsSeparator() const override { return '/'; }
35 Assimp::IOStream* Open(const char*, const char*) override { return nullptr; }
36 void Close(Assimp::IOStream*) override {}
37};
38
39std::optional<double> setting(const std::string& text, const std::string& name) {
40 std::smatch match;
41 if (!std::regex_search(text, match, std::regex("(?:^|\\n) " + name + ": *([^\\r\\n]+)"))) return {};
42 try {
43 std::size_t end = 0;
44 const auto token = match[1].str();
45 const auto value = std::stod(token, &end);
46 if (end != token.size() || !std::isfinite(value)) return {};
47 return value;
48 } catch (...) {
49 return {};
50 }
51}
52
53std::set<std::int64_t> referencedMeshIds(const UnityProjectImportRequest& request,
54 std::string_view sourceGuid) {
55 std::set<std::int64_t> result;
56 const std::regex reference("m_Mesh: *\\{fileID: *(-?[0-9]+), *guid: *" +
57 std::string(sourceGuid) + R"(, *type: *[23]\})",
58 std::regex::icase);
59 for (const auto& [path, bytes] : request.files) {
60 if (unity_detail::foldAscii(std::filesystem::path(path).extension().string()) != ".prefab" || bytes.empty())
61 continue;
62 const std::string text(bytes.begin(), bytes.end());
63 for (std::sregex_iterator match(text.begin(), text.end(), reference), end; match != end; ++match) {
64 try {
65 std::size_t parsed = 0;
66 const auto id = std::stoll((*match)[1].str(), &parsed);
67 if (parsed == static_cast<std::size_t>((*match)[1].length()) && id != 0) result.emplace(id);
68 } catch (...) {
69 }
70 }
71 }
72 return result;
73}
74
75void put32(std::vector<std::uint8_t>& bytes, std::uint32_t value) {
76 for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(std::uint8_t(value >> shift));
77}
78
79Result<PreparedAssetImport> meshImport(const UnityProjectImportRequest& request, const UnitySourceAsset& source,
80 const aiMesh& mesh, std::int64_t fileId, std::optional<unsigned> submesh,
81 float scale) {
82 if (!mesh.HasPositions() || !mesh.HasNormals() || !mesh.HasTextureCoords(0) || mesh.HasBones() ||
83 mesh.mNumAnimMeshes)
86 "FBX static mesh requires positions, imported normals and UV0; skin/morph conversion is unavailable",
87 source.path, {}, "asset.import"));
88 std::uint64_t vertexFloats = 6;
89 for (unsigned set = 0; set < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++set)
90 if (mesh.HasTextureCoords(set)) vertexFloats += 2;
91 if (mesh.HasTangentsAndBitangents()) vertexFloats += 4;
92 for (unsigned set = 0; set < AI_MAX_NUMBER_OF_COLOR_SETS; ++set)
93 if (mesh.HasVertexColors(set)) vertexFloats += 4;
95 std::uint64_t(mesh.mNumFaces) * 3 > request.limits.maximumIndicesPerPrimitive ||
96 vertexFloats > request.limits.maximumDecodedBytes / 4 / mesh.mNumVertices ||
97 std::uint64_t(mesh.mNumVertices) * vertexFloats * 4 + std::uint64_t(mesh.mNumFaces) * 12 >
100 DiagnosticCode::InvalidArgument, "FBX mesh exceeds budget", source.path, {}, "asset.import"));
101 std::vector<std::uint8_t> buffer;
102 auto scalar = [&](float value) { put32(buffer, std::bit_cast<std::uint32_t>(value)); };
103 // Unity's FBX importer reflects X. The canonical right-handed basis then reflects Z.
104 for (unsigned i = 0; i < mesh.mNumVertices; ++i) {
105 scalar(-mesh.mVertices[i].x * scale);
106 scalar(mesh.mVertices[i].y * scale);
107 scalar(-mesh.mVertices[i].z * scale);
108 }
109 const auto normalOffset = buffer.size();
110 for (unsigned i = 0; i < mesh.mNumVertices; ++i) {
111 scalar(-mesh.mNormals[i].x);
112 scalar(mesh.mNormals[i].y);
113 scalar(-mesh.mNormals[i].z);
114 }
115 Value::Array views, accessors;
116 auto stream = [&](std::size_t offset, std::size_t length, std::uint64_t count, const char* type, int component) {
117 const auto index = std::int64_t(views.size());
118 views.emplace_back(Value::Object{{"buffer", Value(std::int64_t(0))},
119 {"byteOffset", Value(std::int64_t(offset))},
120 {"byteLength", Value(std::int64_t(length))}});
121 accessors.emplace_back(Value::Object{{"bufferView", Value(index)},
122 {"componentType", Value(std::int64_t(component))},
123 {"count", Value(std::int64_t(count))},
124 {"type", Value(type)}});
125 };
126 stream(0, normalOffset, mesh.mNumVertices, "VEC3", 5126);
127 stream(normalOffset, buffer.size() - normalOffset, mesh.mNumVertices, "VEC3", 5126);
128 Value::Object attributes{{"POSITION", Value(std::int64_t(0))}, {"NORMAL", Value(std::int64_t(1))}};
129 for (unsigned set = 0; set < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++set) {
130 if (!mesh.HasTextureCoords(set)) continue;
131 const auto offset = buffer.size();
132 for (unsigned i = 0; i < mesh.mNumVertices; ++i) {
133 // Canonical image rows/glTF UV origin are top-left; Unity mesh UV origin is bottom-left.
134 scalar(mesh.mTextureCoords[set][i].x);
135 scalar(1.f - mesh.mTextureCoords[set][i].y);
136 }
137 const auto accessor = std::int64_t(accessors.size());
138 stream(offset, buffer.size() - offset, mesh.mNumVertices, "VEC2", 5126);
139 attributes["TEXCOORD_" + std::to_string(set)] = Value(accessor);
140 }
141 if (mesh.HasTangentsAndBitangents()) {
142 const auto offset = buffer.size();
143 for (unsigned i = 0; i < mesh.mNumVertices; ++i) {
144 scalar(-mesh.mTangents[i].x);
145 scalar(mesh.mTangents[i].y);
146 scalar(-mesh.mTangents[i].z);
147 const auto& n = mesh.mNormals[i];
148 const auto& t = mesh.mTangents[i];
149 const auto& b = mesh.mBitangents[i];
150 const auto handedness =
151 ((n.y * t.z - n.z * t.y) * b.x + (n.z * t.x - n.x * t.z) * b.y + (n.x * t.y - n.y * t.x) * b.z) < 0.f
152 ? -1.f
153 : 1.f;
154 scalar(handedness);
155 }
156 const auto accessor = std::int64_t(accessors.size());
157 stream(offset, buffer.size() - offset, mesh.mNumVertices, "VEC4", 5126);
158 attributes["TANGENT"] = Value(accessor);
159 }
160 for (unsigned set = 0; set < AI_MAX_NUMBER_OF_COLOR_SETS; ++set) {
161 if (!mesh.HasVertexColors(set)) continue;
162 const auto offset = buffer.size();
163 for (unsigned i = 0; i < mesh.mNumVertices; ++i) {
164 scalar(mesh.mColors[set][i].r);
165 scalar(mesh.mColors[set][i].g);
166 scalar(mesh.mColors[set][i].b);
167 scalar(mesh.mColors[set][i].a);
168 }
169 const auto accessor = std::int64_t(accessors.size());
170 stream(offset, buffer.size() - offset, mesh.mNumVertices, "VEC4", 5126);
171 attributes["COLOR_" + std::to_string(set)] = Value(accessor);
172 }
173 const auto indexOffset = buffer.size();
174 for (unsigned i = 0; i < mesh.mNumFaces; ++i) {
175 if (mesh.mFaces[i].mNumIndices != 3)
177 DiagnosticCode::Unsupported, "FBX contains non-triangle primitives", source.path, {}, "asset.import"));
178 for (unsigned j = 0; j < 3; ++j) put32(buffer, mesh.mFaces[i].mIndices[j]);
179 }
180 const auto indexAccessor = std::int64_t(accessors.size());
181 stream(indexOffset, buffer.size() - indexOffset, std::uint64_t(mesh.mNumFaces) * 3, "SCALAR", 5125);
182 Value primitive(Value::Object{{"attributes", Value(std::move(attributes))}, {"indices", Value(indexAccessor)}});
183 Value document(Value::Object{
184 {"asset", Value(Value::Object{{"version", Value("2.0")}})},
185 {"buffers", Value(Value::Array{Value(Value::Object{{"uri", Value("mesh.bin")},
186 {"byteLength", Value(std::int64_t(buffer.size()))}})})},
187 {"bufferViews", Value(std::move(views))},
188 {"accessors", Value(std::move(accessors))},
189 {"meshes",
190 Value(Value::Array{Value(Value::Object{{"primitives", Value(Value::Array{std::move(primitive)})}})})}});
191 auto json = document.toJson();
192 if (!json) return Result<PreparedAssetImport>::failure(json.status());
193 auto identity = request.package;
194 std::string identityKey = "unity:" + source.guid + ":" + std::to_string(fileId);
195 if (submesh) identityKey += ":submesh:" + std::to_string(*submesh);
196 identity.packageId = identity.packageId.child(identityKey);
197 auto result = prepareGltfImport({identity,
198 source.path,
199 {json.value().begin(), json.value().end()},
200 {{"mesh.bin", std::move(buffer)}},
201 request.limits});
202 if (!result) return result;
203 for (auto& mapping : result.value().sourceMappings)
204 mapping.sourceObject = std::to_string(fileId) +
205 (submesh ? "/submesh/" + std::to_string(*submesh) : std::string{});
206 return result;
207}
208} // namespace
209
211 const auto& input = request.files.at(source.path);
212 const auto& metadata = request.files.at(source.path + ".meta");
213 const std::string meta(metadata.begin(), metadata.end());
214 const auto scale = setting(meta, "globalScale");
215 const auto generationSetting = setting(meta, "fileIdsGeneration");
216 const auto generation = generationSetting.value_or(1.0);
217 if ((generation != 1 && generation != 2) || !scale || *scale <= 0 || setting(meta, "useFileScale") != 1 ||
218 setting(meta, "normalImportMode") != 0 || setting(meta, "swapUVChannels") != 0)
221 "FBX requires supported file IDs, positive scale, file units, imported normals and original UV0",
222 source.path, {}, "asset.import"));
223 if (input.empty() || input.size() > request.limits.maximumSourceBytes)
225 DiagnosticCode::InvalidArgument, "FBX source exceeds budget", source.path, {}, "asset.import"));
226 Assimp::Importer importer;
227 importer.SetIOHandler(new NoExternalIO());
228 const auto* scene = importer.ReadFileFromMemory(input.data(), input.size(),
229 aiProcess_Triangulate | aiProcess_CalcTangentSpace, "fbx");
230 if (!scene || !scene->mRootNode)
232 Diagnostic::error(DiagnosticCode::ParseError, importer.GetErrorString(), source.path, {}, "asset.import"));
233 if (scene->mNumAnimations || scene->mNumMeshes > request.limits.maximumAssets)
235 "FBX animation or mesh budget is unsupported",
236 source.path, {}, "asset.import"));
237 std::uint64_t meshBytes = 0;
238 for (unsigned i = 0; i < scene->mNumMeshes; ++i) {
239 const auto& mesh = *scene->mMeshes[i];
240 const auto bytes = std::uint64_t(mesh.mNumVertices) * 32 + std::uint64_t(mesh.mNumFaces) * 12;
243 "aggregate FBX mesh budget exceeded",
244 source.path, {}, "asset.import"));
245 meshBytes += bytes;
246 }
247 double units = 0;
248 float floatUnits = 0;
249 if (scene->mMetaData) {
250 if (!scene->mMetaData->Get("UnitScaleFactor", units) && scene->mMetaData->Get("UnitScaleFactor", floatUnits))
251 units = floatUnits;
252 }
253 const double factor = units * 0.01 * *scale;
254 if (!std::isfinite(factor) || factor <= 0 || factor > std::numeric_limits<float>::max())
256 DiagnosticCode::Unsupported, "FBX file unit scale is unavailable", source.path, {}, "asset.import"));
257 auto manifest = detail::baseManifest(request.package, "eve.unity-fbx/1");
258 if (!manifest) return Result<PreparedAssetImport>::failure(manifest.status());
260 out.manifest = std::move(manifest).takeValue();
261 const auto referencedIds = generation == 1 ? referencedMeshIds(request, source.guid) : std::set<std::int64_t>{};
262 std::uint32_t meshNodeCount = 0;
263 std::function<void(const aiNode&)> countMeshNodes = [&](const aiNode& node) {
264 if (node.mNumMeshes) ++meshNodeCount;
265 for (unsigned index = 0; index < node.mNumChildren; ++index) countMeshNodes(*node.mChildren[index]);
266 };
267 countMeshNodes(*scene->mRootNode);
268 if (generation == 1 && meshNodeCount > 1 &&
269 std::any_of(referencedIds.begin(), referencedIds.end(), [](std::int64_t id) {
270 return id < 4300000 || id >= 4400000 || ((id - 4300000) & 1) != 0;
271 }))
274 "legacy FBX has non-indexed mesh references that cannot be mapped to multiple nodes",
275 source.path, {}, "asset.import"));
276 std::set<std::string> names;
277 std::set<std::int64_t> ids;
278 std::uint32_t nodeCount = 0;
279 std::function<Result<void>(const aiNode&, unsigned)> visit = [&](const aiNode& node,
280 unsigned depth) -> Result<void> {
281 if (++nodeCount > request.limits.maximumAssets || depth > 256)
283 DiagnosticCode::InvalidArgument, "FBX hierarchy exceeds budget", source.path, {}, "asset.import"));
284 if (node.mNumMeshes) {
285 const std::string name(node.mName.C_Str());
286 if (name.empty() || !names.insert(name).second)
288 "FBX requires unique mesh-bearing node names",
289 source.path, {}, "asset.import"));
290 std::int64_t id = 0;
291 if (generation == 2) {
292 const std::string key = "Type:Mesh->" + name + "0";
293 id = std::bit_cast<std::int64_t>(std::uint64_t(XXH64(key.data(), key.size(), 0)));
294 } else if (meshNodeCount == 1 && referencedIds.size() == 1) {
295 id = *referencedIds.begin();
296 } else {
297 id = std::int64_t(4300000) + std::int64_t(node.mMeshes[0]) * 2;
298 }
299 if (id == 0 || !ids.insert(id).second)
301 DiagnosticCode::Conflict, "FBX subasset identity collision", source.path, {}, "asset.import"));
302 for (unsigned slot = 0; slot < node.mNumMeshes; ++slot) {
303 if (node.mMeshes[slot] >= scene->mNumMeshes)
305 DiagnosticCode::ParseError, "FBX mesh index is invalid", source.path, {}, "asset.import"));
306 const auto submesh = node.mNumMeshes > 1 ? std::optional<unsigned>(slot) : std::nullopt;
307 auto converted = meshImport(request, source, *scene->mMeshes[node.mMeshes[slot]], id, submesh,
308 float(factor));
309 if (!converted) return Result<void>::failure(converted.status());
310 auto& part = converted.value();
311 const std::string entrypoint = std::to_string(id) +
312 (submesh ? "/submesh/" + std::to_string(*submesh) : std::string{});
313 out.manifest.entrypoints.emplace(entrypoint, part.manifest.assets.front().asset);
314 for (auto& asset : part.manifest.assets) out.manifest.assets.push_back(std::move(asset));
315 for (auto& entry : part.entries)
316 if (entry.path.starts_with("assets/")) out.entries.push_back(std::move(entry));
317 for (auto& mapping : part.sourceMappings) out.sourceMappings.push_back(std::move(mapping));
318 }
319 out.findings.push_back(
320 {source.path, "FBX.mesh:" + std::to_string(id), ImportDisposition::Translated, name});
321 }
322 for (unsigned i = 0; i < node.mNumChildren; ++i) {
323 auto result = visit(*node.mChildren[i], depth + 1);
324 if (!result) return result;
325 }
326 return Result<void>::success();
327 };
328 auto traversed = visit(*scene->mRootNode, 0);
329 if (!traversed) return Result<PreparedAssetImport>::failure(traversed.status());
330 if (out.manifest.assets.empty())
332 DiagnosticCode::Unsupported, "FBX contains no static meshes", source.path, {}, "asset.import"));
333 out.findings.push_back({source.path, "FBX.vertexStreams", ImportDisposition::Translated,
334 "positions, imported normals, tangents, vertex colors and all available UV sets retained"});
335 return Result<PreparedAssetImport>::success(std::move(out));
336}
337} // namespace eve::asset_import
338#endif
double value
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
float length
Definition CaveMesh.cpp:94
EvpackChunkInput input
Definition Evpack.cpp:170
const GltfImportRequest & request
std::uint32_t key
glm::uvec4 ids
glm::vec3 n
Definition Grass.cpp:63
std::string text
size_t offset
std::array< float, 3 > scale
std::uint64_t bytes
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
std::string path
Definition PlayHost.cpp:110
std::uint32_t generation
std::weak_ptr< PrimitiveScene > scene
float begin
float t
Mesh * mesh
uint64_t token
const RoadNode * node
std::string string
std::uint32_t count
std::vector< UnitCandidate > units
std::int64_t fileId
Direct Unity text-serialization adapter for TerrainData and Prefab assets.
uint32_t index
const UnitySourceAsset & source
std::uint32_t depth
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
std::string extension(std::string_view name)
Extension.
Result< asset::EvaManifest > baseManifest(const ImportPackageIdentity &package, std::string_view importer)
Base manifest.
void put32(std::vector< std::uint8_t > &bytes, std::uint32_t value)
Append little-endian uint32 to owned storage.
std::string foldAscii(std::string value)
Fold ascii.
Result< PreparedAssetImport > prepareUnityFbx(const UnityProjectImportRequest &request, const UnitySourceAsset &source)
Convert a static FBX with explicit Unity importer settings and hashed subasset identities.
Definition UnityFbx.cpp:210
EVENGINE_API_PLATFORM Result< PreparedAssetImport > prepareGltfImport(const GltfImportRequest &request)
Decode triangle primitives from glTF 2.0 into canonical typed mesh blobs.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
std::vector< EvaAssetEntry > assets
Definition EvaManifest.h:69
std::map< std::string, AssetRef > entrypoints
Definition EvaManifest.h:71
Owning, validated source archive candidate not yet published to disk/database.
std::vector< ImportFinding > findings
std::vector< asset::EvaArchiveEntry > entries
std::vector< ImportSourceMapping > sourceMappings
In-memory Unity project slice; keys are normalized project-relative paths.
Immutable-by-convention owning source index entry; no borrowed source addresses.
Definition UnitySource.h:38