载入中...
搜索中...
未找到
UnitySpriteAnimation.cpp
浏览该文件的文档.
1#include <cmath>
2#include <regex>
3#include <set>
4#include <stdexcept>
9
10namespace eve::asset_import {
11namespace {
12std::string capture(const std::string& text, const std::string& pattern) {
13 std::smatch match;
14 if (!std::regex_search(text, match, std::regex(pattern)))
15 throw std::runtime_error("missing or unsupported sprite metadata: " + pattern);
16 return match[1].str();
17}
18double scalar(const std::string& text, const std::string& pattern) {
19 const auto s = capture(text, pattern);
20 std::size_t used = 0;
21 const double n = std::stod(s, &used);
22 if (used != s.size() || !std::isfinite(n)) throw std::runtime_error("invalid sprite scalar");
23 return n;
24}
25std::string contents(const UnityProjectImportRequest& request, const std::string& path) {
26 const auto& b = request.files.at(path);
27 return {b.begin(), b.end()};
28}
29std::uint32_t big32(const std::vector<std::uint8_t>& b, std::size_t i) {
30 return (std::uint32_t(b[i]) << 24) | (std::uint32_t(b[i + 1]) << 16) | (std::uint32_t(b[i + 2]) << 8) | b[i + 3];
31}
32Value::Object sprite(const UnityProjectImportRequest& request, const UnitySourceAsset& source, const std::string& id) {
33 const auto& png = request.files.at(source.path);
34 if (png.size() < 24 || png[0] != 137 || std::string(png.begin() + 1, png.begin() + 4) != "PNG")
35 throw std::runtime_error("sprite image must be PNG");
36 const double width = big32(png, 16), height = big32(png, 20);
37 const auto meta = contents(request, source.path + ".meta");
38 if (scalar(meta, R"(\n spriteMode: ([^\r\n]+))") != 2)
39 throw std::runtime_error("sprite clip requires explicit multiple-sprite slicing");
40 const auto sheet = capture(meta, R"(\n spriteSheet:([\s\S]*))");
41 const std::regex blocks(
42 R"(\n - serializedVersion: [^\n]+\n name: ([^\r\n]+)\n([\s\S]*?)(?=\n - serializedVersion:|\n outline:|\n spriteID:|\n nameFileIdTable:|$))");
43 std::optional<Value::Object> result;
44 for (std::sregex_iterator it(sheet.begin(), sheet.end(), blocks), end; it != end; ++it) {
45 const auto body = (*it)[2].str();
46 if (capture(body, R"(\n internalID: (-?[0-9]+))") != id) continue;
47 if (result) throw std::runtime_error("duplicate sprite fileID");
48 const double x = scalar(body, R"(\n x: ([^\r\n]+))"), y = scalar(body, R"(\n y: ([^\r\n]+))");
49 const double w = scalar(body, R"(\n width: ([^\r\n]+))"),
50 h = scalar(body, R"(\n height: ([^\r\n]+))");
51 const double alignment = scalar(body, R"(\n alignment: ([^\r\n]+))");
52 double px = .5, py = .5;
53 if (alignment == 9) {
54 px = scalar(body, R"(\n pivot: \{x: ([^,]+),)");
55 py = scalar(body, R"(\n pivot: \{x: [^,]+, y: ([^}]+)\})");
56 } else if (alignment != 0)
57 throw std::runtime_error("only centered/custom sprite pivots supported");
58 const double filter = scalar(meta, R"(\n filterMode: ([^\r\n]+))");
59 if (filter != 0 && filter != 1) throw std::runtime_error("unsupported sprite filtering");
60 if (!std::regex_search(body, std::regex(R"(border: \{x: 0, y: 0, z: 0, w: 0\})")))
61 throw std::runtime_error("sprite borders require nine-slice conversion");
62 const auto imageId = request.package.packageId.child("unity:" + source.guid).child("image:default");
63 auto ref = detail::assetRef(imageId);
64 if (!ref) throw std::runtime_error(ref.error()->message());
65 result = Value::Object{{"image", ref.value().format()},
66 {"name", (*it)[1].str()},
67 {"rect", Value::Array{x, height - y - h, w, h}},
68 {"pivot", Value::Array{px, 1 - py}},
69 {"imageSize", Value::Array{width, height}},
70 {"pixelsPerUnit", scalar(meta, R"(\n spritePixelsToUnits: ([^\r\n]+))")},
71 {"filter", filter == 0 ? "nearest" : "linear"},
72 {"sourceGuid", source.guid},
73 {"sourceFileId", id}};
74 }
75 if (!result) throw std::runtime_error("sprite fileID not found: " + source.guid + "/" + id);
76 return std::move(*result);
77}
78} // namespace
80 const UnitySourceAsset& source) try {
81 const auto text = contents(request, source.path);
82 if (text.find("m_PPtrCurves:") == std::string::npos)
84 DiagnosticCode::Unsupported, "animation has no supported sprite track", source.path, {}, "asset.import"));
85 const auto curves = capture(text, R"(\n m_PPtrCurves:([\s\S]*?)\n m_SampleRate:)");
86 if (!std::regex_search(curves,
87 std::regex(R"(\n attribute: m_Sprite\r?\n path: *\r?\n classID: 212\r?\n)")))
89 Diagnostic::error(DiagnosticCode::Unsupported, "only root SpriteRenderer sprite curves are supported",
90 source.path, {}, "asset.import"));
91 for (const auto field : {"m_RotationCurves", "m_CompressedRotationCurves", "m_EulerCurves", "m_PositionCurves",
92 "m_ScaleCurves", "m_FloatCurves", "m_Events"})
93 if (!std::regex_search(text, std::regex(std::string("\\n ") + field + ": \\[\\]")))
95 "clip contains non-sprite curves or events",
96 source.path, {}, "asset.import"));
97 const std::regex curveStart(R"(\n - curve:)");
98 if (std::distance(std::sregex_iterator(curves.begin(), curves.end(), curveStart), std::sregex_iterator{}) != 1)
100 DiagnosticCode::Unsupported, "multiple sprite tracks are unsupported", source.path, {}, "asset.import"));
101 const double fps = scalar(text, R"(\n m_SampleRate: ([^\r\n]+))");
102 const double duration = scalar(text, R"(\n m_StopTime: ([^\r\n]+))");
103 const double loop = scalar(text, R"(\n m_LoopTime: ([^\r\n]+))");
104 if (fps <= 0 || fps > 1000 || (loop != 0 && loop != 1) || scalar(text, R"(\n m_StartTime: ([^\r\n]+))") != 0)
105 throw std::runtime_error("invalid sprite playback settings");
106 auto indexed = indexUnitySources(request.files, request.limits);
107 if (!indexed) return Result<PreparedAssetImport>::failure(indexed.status());
108 std::map<std::string, UnitySourceAsset> sources;
109 for (const auto& s : indexed.value().assets)
110 if (!s.guid.empty()) sources.emplace(s.guid, s);
111 Value::Array frames;
112 std::set<std::string> images;
113 std::map<std::string, Value::Object> cache;
114 const std::regex key(
115 R"(\n - time: ([^\r\n]+)\r?\n value: \{fileID: (-?[0-9]+), guid: ([0-9a-fA-F]{32}),\s*type: 3\})");
116 for (std::sregex_iterator it(curves.begin(), curves.end(), key), end; it != end; ++it) {
117 if (frames.size() >= request.limits.maximumAssets) throw std::runtime_error("sprite key budget exceeded");
118 const auto guid = unity_detail::foldAscii((*it)[3].str()), id = (*it)[2].str();
119 if (!sources.contains(guid)) throw std::runtime_error("missing sprite GUID " + guid);
120 const auto identity = guid + "/" + id;
121 if (!cache.contains(identity)) cache.emplace(identity, sprite(request, sources.at(guid), id));
122 auto frame = cache.at(identity);
123 const double raw = scalar("t:" + (*it)[1].str(), R"(t:(.*))");
124 const double time = std::round(raw * fps) / fps;
125 if (std::abs(time - raw) > 1e-5) throw std::runtime_error("sprite keys must align to the authored sample rate");
126 frame["time"] = time;
127 images.insert(frame.at("image").asString());
128 frames.emplace_back(std::move(frame));
129 }
130 const std::regex times(R"(\n - time:)");
131 if (frames.size() !=
132 std::size_t(std::distance(std::sregex_iterator(curves.begin(), curves.end(), times), std::sregex_iterator{})))
133 throw std::runtime_error("unresolved or malformed sprite key");
134 Value definition(Value::Object{{"schema", "eve.sprite-animation"},
135 {"schemaVersion", 1},
136 {"duration", duration},
137 {"loop", loop == 1},
138 {"sampleRate", fps},
139 {"frames", std::move(frames)}});
140 auto validated = asset::SpriteAnimationClip::decode(definition);
141 if (!validated) return Result<PreparedAssetImport>::failure(validated.status());
142 auto manifest = detail::baseManifest(request.package, "eve.unity-sprite-animation/1");
143 if (!manifest) return Result<PreparedAssetImport>::failure(manifest.status());
145 out.manifest = std::move(manifest).takeValue();
146 const auto id = request.package.packageId.child("unity:" + source.guid + ":7400000");
147 auto ref = detail::assetRef(id);
148 if (!ref) return Result<PreparedAssetImport>::failure(ref.status());
149 auto json = definition.toJson();
150 if (!json) return Result<PreparedAssetImport>::failure(json.status());
151 std::vector<std::uint8_t> bytes(json.value().begin(), json.value().end());
152 const auto path = "assets/" + id.format() + "/asset.json";
153 out.manifest.assets.push_back({ref.value(),
154 "eve.sprite-animation",
155 SchemaVersion(1),
156 path,
158 {"animation", "sprite", "source:unity"}});
159 out.entries.push_back({path, std::move(bytes)});
160 out.manifest.entrypoints.emplace("default", ref.value());
161 out.sourceMappings.push_back({"7400000", ref.value()});
162 for (const auto& image : images) {
163 auto dependency = AssetRef::parse(image);
164 if (!dependency) return Result<PreparedAssetImport>::failure(dependency.status());
165 out.manifest.dependencies.push_back({ref.value(),
166 dependency.value(),
168 "spriteFrame",
169 {},
170 "eve.image/3",
171 {}});
172 }
173 out.findings.push_back({source.path, "AnimationClip.sprite", ImportDisposition::Translated,
174 "explicit sprite keys, rectangles, pivots, pixel scale, filter and looping preserved"});
175 return Result<PreparedAssetImport>::success(std::move(out));
176} catch (const std::exception& error) {
178 Diagnostic::error(DiagnosticCode::ParseError, error.what(), source.path, {}, "asset.import"));
179}
180} // namespace eve::asset_import
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float duration
const std::string & s
float py
std::string pattern
const GltfImportRequest & request
std::uint32_t key
vk::UniqueImage image
glm::vec3 n
Definition Grass.cpp:63
int h
std::vector< Colorf > px
std::uint32_t height
std::uint32_t width
std::string text
std::uint64_t bytes
MeleePoint3 b
Definition MeleeHit.cpp:41
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::string filter
std::string body
Direct Unity text-serialization adapter for TerrainData and Prefab assets.
const UnitySourceAsset & source
static Result< AssetRef > parse(std::string_view text)
Parse an asset://<canonical UUID> reference.
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
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
static Result< SpriteAnimationClip > decode(const Value &definition)
Decode eve.sprite-animation/1; reject unknown versions, ignore unknown fields.
Id128 child(std::string_view role) const noexcept
Derives a deterministic child UUID for a hierarchical identity.
Definition Identity.h:192
std::string sha256(std::span< const std::uint8_t > bytes)
Sha 256.
Result< asset::EvaManifest > baseManifest(const ImportPackageIdentity &package, std::string_view importer)
Base manifest.
Result< AssetRef > assetRef(PersistentId id)
Asset ref.
std::string foldAscii(std::string value)
Fold ascii.
Result< UnitySourceIndex > indexUnitySources(const UnitySourceFiles &files, const AssetImportLimits &limits)
Validate project paths and .meta identities and index serialized GUID/fileID references.
Result< PreparedAssetImport > prepareUnitySpriteAnimation(const UnityProjectImportRequest &request, const UnitySourceAsset &source)
Convert an explicit SpriteRenderer timeline and referenced metadata into an owning canonical candidat...
detail::StrongUint64< detail::SchemaVersionTag > SchemaVersion
Persistent data-format version; not a runtime replacement generation.
std::vector< EvaAssetEntry > assets
Definition EvaManifest.h:69
std::vector< EvaDependency > dependencies
Definition EvaManifest.h:70
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