Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 77 additions & 4 deletions core/unity_import.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,50 @@ void bakeLayout(const YNode& rt, float scaleX, float scaleY, UIElement& e) {
}
}

// ---- Model internal IDs ----

uint64_t xxh64(std::string_view s, uint64_t seed) {
constexpr uint64_t P1 = 11400714785074694791ull, P2 = 14029467366897019727ull, P3 = 1609587929392839161ull,
P4 = 9650029242287828579ull, P5 = 2870177450012600261ull;
auto rotl = [](uint64_t x, int r) { return (x << r) | (x >> (64 - r)); };
auto le = [&](size_t at, int bytes) {
uint64_t v = 0;
for (int b = 0; b < bytes; ++b) v |= uint64_t(uint8_t(s[at + size_t(b)])) << (8 * b);
return v;
};
auto round = [&](uint64_t acc, uint64_t in) { return rotl(acc + in * P2, 31) * P1; };
const size_t n = s.size();
size_t p = 0;
uint64_t h;
if (n >= 32) {
uint64_t v[4] = {seed + P1 + P2, seed + P2, seed, seed - P1};
for (; p + 32 <= n; p += 32)
for (int k = 0; k < 4; ++k) v[k] = round(v[k], le(p + size_t(k) * 8, 8));
h = rotl(v[0], 1) + rotl(v[1], 7) + rotl(v[2], 12) + rotl(v[3], 18);
for (uint64_t x : v) h = (h ^ round(0, x)) * P1 + P4;
} else {
h = seed + P5;
}
h += n;
for (; p + 8 <= n; p += 8) h = rotl(h ^ round(0, le(p, 8)), 27) * P1 + P4;
if (p + 4 <= n) {
h = rotl(h ^ (le(p, 4) * P1), 23) * P2 + P3;
p += 4;
}
for (; p < n; ++p) h = rotl(h ^ (uint64_t(uint8_t(s[p])) * P5), 11) * P1;
h ^= h >> 33;
h *= P2;
h ^= h >> 29;
h *= P3;
h ^= h >> 32;
return h;
}

long long modelObjectId(std::string_view type, std::string_view path, int n) {
const std::string key = "Type:" + std::string(type) + "->" + std::string(path) + std::to_string(n);
return (long long)xxh64(key);
}

// ---- The importer ----

namespace {
Expand Down Expand Up @@ -1034,6 +1078,7 @@ struct ModelAsset {
std::string error;
// internal fileID -> (class, name) from the model's .meta
std::map<long long, std::pair<int, std::string>> ids;
bool preserveHierarchy = false; // the .meta keeps a lone top-level node below the root
};

// The internal IDs of a model's .meta: internalIDToNameTable (Unity 2019.3+)
Expand Down Expand Up @@ -1401,11 +1446,14 @@ Importer::Unit Importer::instantiateModel(const Ref& source, const YNode& mod) {
std::vector<int> nodeObj;
if (ma && ma->error.empty()) {
const ImportedModel& im = ma->model;
const bool single = im.nodes.size() == 1;
// Unity makes a lone top-level node the prefab root.
int tops = 0;
for (const ImportedNode& n : im.nodes) tops += n.parent < 0;
const bool single = tops == 1 && !ma->preserveHierarchy;
for (size_t k = 0; k < im.nodes.size(); ++k) {
const ImportedNode& n = im.nodes[k];
int o = root;
if (!single) {
if (!single || n.parent >= 0) {
o = int(m_objs.size());
m_objs.emplace_back();
m_objs.back().name = n.name;
Expand Down Expand Up @@ -1437,6 +1485,26 @@ Importer::Unit Importer::instantiateModel(const Ref& source, const YNode& mod) {
break;
}
}
// Unity 2019.3+ leaves that table empty: the IDs are hashes of each
// object's path in the model. Where the table names an ID, it wins.
std::map<std::string, int> taken;
auto hashed = [&](const std::string& path, int o) {
for (const char* t : {"GameObject", "Transform", "MeshFilter", "MeshRenderer", "SkinnedMeshRenderer"}) {
const std::string p = std::strcmp(t, "GameObject") == 0 ? path : path + "/" + t;
u.byFid.emplace(modelObjectId(t, p, taken[std::string(t) + "->" + p]++), o);
}
};
std::vector<std::string> paths(im.nodes.size());
hashed("//RootNode/root", root);
for (size_t k = 0; k < im.nodes.size(); ++k) {
const ImportedNode& n = im.nodes[k];
if (single && n.parent < 0) {
paths[k] = "//RootNode/root";
continue;
}
paths[k] = (n.parent >= 0 ? paths[size_t(n.parent)] : std::string("//RootNode/root")) + "/" + n.name;
hashed(paths[k], nodeObj[k]);
}
}
for (const YNode& m : mod["m_Modifications"].seq) {
const Ref target = refOf(m["target"]);
Expand Down Expand Up @@ -1555,7 +1623,10 @@ ModelAsset* Importer::model(const std::string& guid, const std::string& path) {
// Unity's model import scale, when the .meta sets one.
for (const YDoc& d : parseYaml(readText(fs::path(src) += ".meta")))
if (const YNode* mi = d.body.find("ModelImporter"))
if (const YNode* meshes = mi->find("meshes")) s.scale = float((*meshes)["globalScale"].num(1));
if (const YNode* meshes = mi->find("meshes")) {
s.scale = float((*meshes)["globalScale"].num(1));
ma->preserveHierarchy = (*meshes)["preserveHierarchy"].flag(false);
}
saveImportSettings(copy, s);
ma->model = importModel(copy, m_dest, s);
ma->ids = metaIds(fs::path(src) += ".meta");
Expand Down Expand Up @@ -2107,8 +2178,10 @@ void Importer::convertObject(int idx, const std::string& parentPath, std::vector
o.active = u.active;
o.transform = u.t;
const UComp *exp = nullptr, *mf = nullptr, *mr = nullptr;
bool skinExp = false; // its mesh is reported as not imported yet
std::vector<std::string> leftOut;
for (const UComp& c : u.comps) {
skinExp |= c.classId == 114 && c.cls == "PSXSkinnedObjectExporter";
if (c.classId == 33) mf = &c;
else if (c.classId == 23) mr = &c;
else if (c.classId == 114 && c.cls == "PSXObjectExporter") exp = &c;
Expand Down Expand Up @@ -2208,7 +2281,7 @@ void Importer::convertObject(int idx, const std::string& parentPath, std::vector
}
const std::string lua = luaOf(field("luaFile", "LuaFile"), path, "script");
if (!lua.empty()) o.script = ScriptComponent{lua, {}};
} else if ((mf && !refOf((*mf->body)["m_Mesh"]).null()) || !u.modelMesh.empty()) {
} else if (!skinExp && ((mf && !refOf((*mf->body)["m_Mesh"]).null()) || !u.modelMesh.empty())) {
item(ReportItem::LeftOut, path, "has a mesh but no PSXObjectExporter, so Unity did not export it either; it is kept without its mesh");
}
if (!leftOut.empty()) {
Expand Down
13 changes: 13 additions & 0 deletions core/unity_import.hh
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@ struct Primitive {
std::optional<Primitive> builtinMesh(long long fileId);
constexpr const char* kBuiltinGuid = "0000000000000000e000000000000000";

// ---- Model internal IDs ----

// XXH64 of `bytes`.
uint64_t xxh64(std::string_view bytes, uint64_t seed = 0);
// The fileID Unity 2019.3+ gives an object inside a model it imported (its
// .meta's internalIDToNameTable is then empty): XXH64 of the UTF-8
// "Type:<type>-><path><n>" as a signed 64-bit value, <n> counting the
// earlier objects of the model with the same type and path. The prefab
// root's path is "//RootNode/root", a node's is its parent's + "/<name>"
// (a model's single top-level node is the root), a component's is its
// object's + "/<type>": "Type:Transform->//RootNode/root/Arm/Transform0".
long long modelObjectId(std::string_view type, std::string_view path, int n = 0);

// ---- UI ----

// A RectTransform body (m_AnchorMin/Max, m_AnchoredPosition, m_SizeDelta,
Expand Down
55 changes: 55 additions & 0 deletions src/editor/tests/editor_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12715,6 +12715,60 @@ void testUnityPrefab() {
CHECK(lamp2 && lamp2->children.size() == 2);
}

// Exporters added to models placed in a scene (#223): Unity 6 leaves the
// model's internalIDToNameTable empty, the scene names the model's objects
// by hashed IDs. Fixture: tests/fixtures/unity-models, saved by Unity 6000.3.
void testUnityModelIds() {
using namespace splash::unity;
using namespace unitytest;
CHECK(xxh64("") == 0xEF46DB3751D8E999ull);
CHECK(xxh64("abc") == 0x44BC2CF5AD770999ull);
// IDs Unity wrote: the root and its Transform, a node and its renderer
// (URP's SandBoxModel.fbx), nested, a lone top-level node's child, skinned.
CHECK(modelObjectId("GameObject", "//RootNode/root") == 919132149155446097LL);
CHECK(modelObjectId("Transform", "//RootNode/root/Transform") == -8679921383154817045LL);
CHECK(modelObjectId("GameObject", "//RootNode/root/Circle2") == 3650834566929780429LL);
CHECK(modelObjectId("MeshRenderer", "//RootNode/root/Circle2/MeshRenderer") == 5912129206897006233LL);
CHECK(modelObjectId("GameObject", "//RootNode/root/Base/Arm") == -1537605290578348017LL);
CHECK(modelObjectId("GameObject", "//RootNode/root/Tip") == -8469990867830010200LL);
CHECK(modelObjectId("SkinnedMeshRenderer", "//RootNode/root/Body/SkinnedMeshRenderer") == 1630794972795428178LL);

const fs::path proj = fs::path(SPLASHEDIT_SOURCE_DIR) / "tests" / "fixtures" / "unity-models";
const fs::path dest = g_outDir / "unity-model-ids";
fs::remove_all(dest);
fs::create_directories(dest);
ImportReport r;
const splash::Scene s = convertScene(proj, indexGuids(proj), proj / "Assets" / "Scenes" / "models.unity", dest, r);
std::function<const splash::Object*(const std::vector<splash::Object>&, const std::string&)> at =
[&](const std::vector<splash::Object>& objs, const std::string& path) -> const splash::Object* {
const size_t slash = path.find('/');
const splash::Object* o = findObj(objs, path.substr(0, slash));
return !o || slash == std::string::npos ? o : at(o->children, path.substr(slash + 1));
};
auto meshed = [&](const char* path) {
const splash::Object* o = at(s.objects, path);
return o && o->mesh && !o->mesh->mesh.empty();
};
const char* noExporter = "has a mesh but no PSXObjectExporter";
// solo.fbx: its one top-level node is the root, its child under it.
CHECK(meshed("solo") && meshed("solo/Tip"));
CHECK(at(s.objects, "solo") && at(s.objects, "solo")->children.size() == 1);
// multi.fbx: a nested node and a top-level one carry the added exporter.
CHECK(meshed("multi/Base/Arm") && meshed("multi/Lamp"));
CHECK(!meshed("multi/Base") && !meshed("multi/Base/Arm/Hand"));
CHECK(hasItem(r, ReportItem::LeftOut, "multi/Base", noExporter) && hasItem(r, ReportItem::LeftOut, "multi/Base/Arm/Hand", noExporter));
// rig.fbx: the skinned child holds its PSXSkinnedObjectExporter.
CHECK(meshed("rig/Prop") && at(s.objects, "rig/Body"));
CHECK(hasItem(r, ReportItem::Later, "rig/Body", "PSXSkinnedObjectExporter"));
for (const char* p : {"solo", "solo/Tip", "multi", "multi/Base/Arm", "multi/Lamp", "rig/Prop", "rig/Body"})
CHECK(!hasItem(r, ReportItem::LeftOut, p, noExporter));
// The instances' root overrides land on the root.
CHECK(at(s.objects, "rig") && at(s.objects, "rig")->transform.position.x == 6);
CHECK(at(s.objects, "multi") && at(s.objects, "multi")->transform.position.x == 3);
if (g_failures)
for (const auto& it : r.items) std::fprintf(stderr, " item %d %s: %s\n", int(it.kind), it.object.c_str(), it.what.c_str());
}

void testUnityPrimitives() {
using namespace splash::unity;
auto outward = [](const splash::Mesh& m) {
Expand Down Expand Up @@ -13310,6 +13364,7 @@ int main(int argc, char** argv) {
testUnityGuidIndex();
testUnityHierarchy();
testUnityPrefab();
testUnityModelIds();
testUnityPrimitives();
testUnityReport();
testUnityUi();
Expand Down
Binary file not shown.
110 changes: 110 additions & 0 deletions tests/fixtures/unity-models/Assets/Models/multi.fbx.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added tests/fixtures/unity-models/Assets/Models/rig.fbx
Binary file not shown.
Loading
Loading