载入中...
搜索中...
未找到
GltfAnimation.cpp
浏览该文件的文档.
4
5#include <bit>
6#include <cmath>
7#include <limits>
8#include <numeric>
9#include <set>
10
12namespace {
13// Local parser unwinding only. The public boundary returns a checked Result;
14// callers never observe a partially prepared archive.
15struct Rejection {
17 std::string message;
18};
19void require(bool condition, std::string message, DiagnosticCode code = DiagnosticCode::ParseError) {
20 if (!condition) throw Rejection{code, std::move(message)};
21}
22const Value::Object& object(const Value& value) {
23 const auto* result = value.getIf<Value::Object>();
24 require(result != nullptr, "glTF skeletal field must be an object");
25 return *result;
26}
27const Value::Object& object(const Value* value) {
28 require(value != nullptr, "required glTF skeletal object is missing");
29 return object(*value);
30}
31const Value::Array& array(const Value* value) {
32 const auto* result = value ? value->getIf<Value::Array>() : nullptr;
33 require(result != nullptr, "glTF skeletal field must be an array");
34 return *result;
35}
36std::uint32_t index(const Value* value, std::size_t bound) {
37 require(value && value->isInt64() && value->asInt() >= 0 && std::uint64_t(value->asInt()) < bound,
38 "glTF skeletal index is out of range");
39 return static_cast<std::uint32_t>(value->asInt());
40}
41float number(const Value& value) {
42 require(value.isInt64() || value.isDouble(), "glTF transform component must be numeric");
43 const auto result = static_cast<float>(value.isInt64() ? double(value.asInt()) : value.asDouble());
44 require(std::isfinite(result), "glTF transform component must be finite");
45 return result;
46}
47std::string name(const Value::Object& value, std::string fallback, const AssetImportLimits& limits) {
48 const auto* field = member(value, "name");
49 if (!field) return fallback;
50 require(field->isString() && field->asString().size() <= limits.maximumStringBytes,
51 "glTF name exceeds string budget");
52 return field->asString();
53}
54void putString(std::vector<std::uint8_t>& bytes, const std::string& text) {
55 put32(bytes, static_cast<std::uint32_t>(text.size()));
56 bytes.insert(bytes.end(), text.begin(), text.end());
57}
58Accessor accessor(const Value::Object& root, const std::vector<std::span<const std::uint8_t>>& buffers,
59 const Value* value, const AssetImportLimits& limits) {
60 const auto i = index(value, array(member(root, "accessors")).size());
61 auto parsed = accessorAt(root, buffers, i, limits);
62 if (!parsed) throw Rejection{DiagnosticCode::ParseError, "invalid glTF skeletal accessor " + std::to_string(i)};
63 return std::move(parsed).takeValue();
64}
65float component(const Accessor& a, std::uint32_t row, std::uint32_t column) {
66 auto value = readFloat(a, row, column);
67 if (!value) throw Rejection{DiagnosticCode::ParseError, "invalid glTF skeletal FLOAT component"};
68 return value.value();
69}
70std::uint32_t integerComponent(const Accessor& a, std::uint32_t row, std::uint32_t column) {
71 require(a.componentType == 5121 || a.componentType == 5123, "JOINTS must use unsigned bytes or shorts");
72 auto offset = a.offset + row * a.stride + column * componentSize(a.componentType);
73 return a.componentType == 5121 ? a.buffer[offset]
74 : std::uint32_t(a.buffer[offset]) | (std::uint32_t(a.buffer[offset + 1]) << 8);
75}
77 auto ref = detail::assetRef(id);
78 require(ref.ok(), "invalid generated asset identity");
79 return std::move(ref).takeValue();
80}
81void publish(const GltfImportRequest& request, PreparedAssetImport& output, const AssetRef& ref, std::string type,
82 Value::Object definition, std::vector<std::uint8_t> blob, std::string source) {
83 require(output.manifest.assets.size() < request.limits.maximumAssets, "glTF asset budget exceeded");
84 std::uint64_t total = blob.size();
85 for (const auto& entry : output.entries) {
86 require(entry.bytes.size() <= request.limits.maximumDecodedBytes &&
87 total <= request.limits.maximumDecodedBytes - entry.bytes.size(),
88 "glTF decoded budget exceeded");
89 total += entry.bytes.size();
90 }
91 require(total <= request.limits.maximumDecodedBytes, "glTF decoded budget exceeded");
92 const auto base = "assets/" + ref.id().format() + "/";
93 definition["schema"] = Value(type);
94 definition["schemaVersion"] = Value(std::int64_t(1));
95 definition["blob"] = Value(base + "data.bin");
96 auto json = Value(std::move(definition)).toJson();
97 require(json.ok(), "cannot encode canonical skeletal definition");
98 std::vector<std::uint8_t> bytes(json.value().begin(), json.value().end());
99 output.manifest.assets.push_back(
100 {ref, type, SchemaVersion(1), base + "asset.json", detail::sha256(bytes), {"source:gltf2"}});
101 output.entries.push_back({base + "asset.json", std::move(bytes)});
102 output.entries.push_back({base + "data.bin", std::move(blob)});
103 output.sourceMappings.push_back({source, ref});
104 output.findings.push_back({request.sourceName, source, ImportDisposition::Translated, "converted to " + type});
105}
106void dependency(PreparedAssetImport& output, const AssetRef& from, const AssetRef& to, std::string role,
107 std::string type) {
108 output.manifest.dependencies.push_back(
109 {from, to, asset::EvaDependencyKind::RuntimeRequired, std::move(role), {}, std::move(type)});
110}
111
112void append(const GltfImportRequest& request, const Value::Object& root,
113 const std::vector<std::span<const std::uint8_t>>& buffers, PreparedAssetImport& output) {
114 const auto* skinValue = member(root, "skins");
115 const auto* animationValue = member(root, "animations");
116 if (!skinValue && !animationValue) return;
117 const auto& nodes = array(member(root, "nodes"));
118 require(!nodes.empty() && nodes.size() <= request.limits.maximumAssets, "glTF node budget exceeded");
119 std::vector<int> parents(nodes.size(), -1), mapped(nodes.size(), -1);
120 for (std::size_t n = 0; n < nodes.size(); ++n) {
121 const auto& node = object(nodes[n]);
122 require(!member(node, "matrix"), "skeletal matrix nodes require TRS conversion", DiagnosticCode::Unsupported);
123 if (auto* children = member(node, "children"))
124 for (const auto& child : array(children)) {
125 const auto c = index(&child, nodes.size());
126 require(c != n && parents[c] == -1, "glTF nodes contain a cycle or multiple parents");
127 parents[c] = static_cast<int>(n);
128 }
129 }
130 // Breadth-first order includes non-joint ancestors. Bind and animated local
131 // transforms therefore retain their original parent space without baking.
132 std::vector<std::uint32_t> order;
133 for (std::uint32_t n = 0; n < nodes.size(); ++n)
134 if (parents[n] == -1) order.push_back(n);
135 for (std::size_t cursor = 0; cursor < order.size(); ++cursor) {
136 auto n = order[cursor];
137 mapped[n] = static_cast<int>(cursor);
138 if (auto* children = member(object(nodes[n]), "children"))
139 for (const auto& child : array(children)) order.push_back(index(&child, nodes.size()));
140 }
141 require(order.size() == nodes.size(), "glTF node hierarchy contains a cycle");
142 const auto skeleton = reference(request.package.packageId.child("gltf:skeleton"));
143 std::vector<std::uint8_t> skeletonBytes{'E', 'V', 'S', 'K', 'E', 'L', 0, 1};
144 put32(skeletonBytes, static_cast<std::uint32_t>(nodes.size()));
145 for (auto n : order) {
146 const auto& node = object(nodes[n]);
147 const auto boneName = name(node, "node_" + std::to_string(n), request.limits);
148 const std::uint64_t boneBytes = 48 + boneName.size();
149 require(boneBytes <= request.limits.maximumDecodedBytes &&
150 skeletonBytes.size() <= request.limits.maximumDecodedBytes - boneBytes,
151 "skeleton decoded budget exceeded");
152 put32(skeletonBytes, parents[n] < 0 ? std::uint32_t(-1) : std::uint32_t(mapped[parents[n]]));
153 putString(skeletonBytes, boneName);
154 for (const auto& field : {"translation", "rotation", "scale"}) {
155 const int count = std::string_view(field) == "rotation" ? 4 : 3;
156 const auto* value = member(node, field);
157 if (value) require(array(value).size() == std::size_t(count), "invalid glTF TRS shape");
158 if (value && count == 4) {
159 double length = 0;
160 for (const auto& part : array(value)) {
161 const auto x = number(part);
162 length += double(x) * x;
163 }
164 require(std::abs(length - 1.0) < 0.001, "bind rotation must be a unit quaternion");
165 }
166 for (int c = 0; c < count; ++c) {
167 const float fallback = std::string_view(field) == "scale" || c == 3 ? 1.f : 0.f;
168 putFloat(skeletonBytes, value ? number(array(value)[c]) : fallback);
169 }
170 }
171 }
172 publish(request, output, skeleton, "eve.skeleton",
173 {{"boneCount", Value(std::int64_t(nodes.size()))},
174 {"coordinateSystem", Value("right-handed-x-right-y-up-minus-z-forward")},
175 {"lengthUnit", Value("meter")}},
176 std::move(skeletonBytes), "nodes");
177
178 if (skinValue) {
179 const auto& skins = array(skinValue);
180 const auto& meshes = array(member(root, "meshes"));
181 for (std::uint32_t n = 0; n < nodes.size(); ++n) {
182 const auto& node = object(nodes[n]);
183 if (!member(node, "skin")) continue;
184 const auto skinIndex = index(member(node, "skin"), skins.size());
185 const auto meshIndex = index(member(node, "mesh"), meshes.size());
186 const auto& skin = object(skins[skinIndex]);
187 const auto& joints = array(member(skin, "joints"));
188 require(!joints.empty() && joints.size() <= nodes.size(), "invalid glTF joint count");
189 std::set<std::uint32_t> unique;
190 for (const auto& j : joints) require(unique.insert(index(&j, nodes.size())).second, "duplicate glTF joint");
191 std::optional<Accessor> inverse;
192 if (auto* ibm = member(skin, "inverseBindMatrices")) {
193 inverse = accessor(root, buffers, ibm, request.limits);
194 require(inverse->components == 16 && inverse->componentType == 5126 && inverse->count == joints.size(),
195 "invalid inverse bind matrices");
196 }
197 const auto& primitives = array(member(object(meshes[meshIndex]), "primitives"));
198 for (std::uint32_t p = 0; p < primitives.size(); ++p) {
199 const auto& primitive = object(primitives[p]);
200 const auto& attrs = object(member(primitive, "attributes"));
201 auto position = accessor(root, buffers, member(attrs, "POSITION"), request.limits);
202 std::vector<std::pair<Accessor, Accessor>> sets;
203 for (std::uint32_t set = 0;; ++set) {
204 const auto* j = member(attrs, "JOINTS_" + std::to_string(set));
205 const auto* w = member(attrs, "WEIGHTS_" + std::to_string(set));
206 if (!j && !w) break;
207 require(j && w && set < 64, "incomplete or excessive glTF influence sets");
208 auto ja = accessor(root, buffers, j, request.limits),
209 wa = accessor(root, buffers, w, request.limits);
210 require(ja.components == 4 && wa.components == 4 && ja.count == position.count &&
211 wa.count == position.count,
212 "glTF skin attributes must match vertices");
213 require(!ja.normalized, "JOINTS cannot be normalized");
214 require((wa.componentType == 5126 && !wa.normalized) ||
215 ((wa.componentType == 5121 || wa.componentType == 5123) && wa.normalized),
216 "WEIGHTS must use FLOAT or normalized unsigned bytes/shorts");
217 sets.emplace_back(ja, wa);
218 }
219 require(!sets.empty(), "skinned primitive has no joint weights");
220 for (const auto& [key, value] : attrs) {
221 (void)value;
222 if (key.starts_with("JOINTS_") || key.starts_with("WEIGHTS_")) {
223 bool found = false;
224 for (std::size_t s = 0; s < sets.size(); ++s)
225 found |= key == "JOINTS_" + std::to_string(s) || key == "WEIGHTS_" + std::to_string(s);
226 require(found, "glTF influence sets must be contiguous");
227 }
228 }
229 const auto influences = static_cast<std::uint32_t>(sets.size() * 4);
230 require(std::uint64_t(position.count) * influences * 8 + joints.size() * 68 + 24 <=
231 request.limits.maximumDecodedBytes,
232 "skin decoded budget exceeded");
233 std::vector<std::uint8_t> bytes{'E', 'V', 'S', 'K', 'I', 'N', 0, 1};
234 put32(bytes, position.count);
235 put32(bytes, static_cast<std::uint32_t>(joints.size()));
236 put32(bytes, influences);
237 put32(bytes, static_cast<std::uint32_t>(mapped[n]));
238 for (std::uint32_t j = 0; j < joints.size(); ++j) {
239 put32(bytes, static_cast<std::uint32_t>(mapped[index(&joints[j], nodes.size())]));
240 for (std::uint32_t c = 0; c < 16; ++c)
241 putFloat(bytes, inverse ? component(*inverse, j, c) : (c % 5 == 0 ? 1.f : 0.f));
242 }
243 std::uint32_t normalizedVertices = 0;
244 for (std::uint32_t v = 0; v < position.count; ++v) {
245 const auto rowStart = bytes.size();
246 float sum = 0;
247 for (const auto& [j, w] : sets)
248 for (std::uint32_t c = 0; c < 4; ++c) {
249 auto joint = integerComponent(j, v, c);
250 auto weight = w.componentType == 5126 ? component(w, v, c)
251 : float(integerComponent(w, v, c)) /
252 (w.componentType == 5121 ? 255.f : 65535.f);
253 require(joint < joints.size() && weight >= 0, "invalid glTF joint index or weight");
254 sum += weight;
255 put32(bytes, joint);
257 }
258 require(std::isfinite(sum) && sum > 0, "glTF vertex weights must have a positive finite sum");
259 if (std::abs(sum - 1.f) > 1e-6f) {
260 ++normalizedVertices;
261 for (std::uint32_t i = 0; i < influences; ++i) {
262 const auto offset = rowStart + i * 8 + 4;
263 const auto normalized = std::bit_cast<float>(little32(bytes, offset)) / sum;
264 const auto bits = std::bit_cast<std::uint32_t>(normalized);
265 for (int byte = 0; byte < 4; ++byte)
266 bytes[offset + byte] = static_cast<std::uint8_t>(bits >> (byte * 8));
267 }
268 }
269 }
270 auto meshRef = reference(request.package.packageId.child("gltf:mesh:" + std::to_string(meshIndex) +
271 ":primitive:" + std::to_string(p)));
272 const auto source = "nodes[" + std::to_string(n) + "].skin.primitives[" + std::to_string(p) + "]";
273 auto skinRef = reference(request.package.packageId.child("gltf:" + source));
274 if (normalizedVertices)
275 output.findings.push_back({request.sourceName, source + ".weights", ImportDisposition::Baked,
276 "renormalized all influence sets for " +
277 std::to_string(normalizedVertices) +
278 " vertices with non-unit source weight sums"});
279 publish(request, output, skinRef, "eve.skin",
280 {{"skeleton", Value(skeleton.format())},
281 {"mesh", Value(meshRef.format())},
282 {"vertexCount", Value(std::int64_t(position.count))},
283 {"jointCount", Value(std::int64_t(joints.size()))},
284 {"influencesPerVertex", Value(std::int64_t(influences))}},
285 std::move(bytes), source);
286 dependency(output, skinRef, skeleton, "skeleton", "eve.skeleton/1");
287 dependency(output, skinRef, meshRef, "mesh", "eve.mesh/2");
288 output.manifest.entrypoints.emplace("skin:" + std::to_string(n) + ":" + std::to_string(p), skinRef);
289 }
290 }
291 }
292 if (!animationValue) return;
293 const auto& animations = array(animationValue);
294 for (std::uint32_t a = 0; a < animations.size(); ++a) {
295 const auto& animation = object(animations[a]);
296 const auto& samplers = array(member(animation, "samplers"));
297 const auto& channels = array(member(animation, "channels"));
298 struct Track {
299 std::array<std::vector<std::uint8_t>, 3> keys;
300 std::array<std::uint32_t, 3> counts{};
301 };
302 std::map<std::uint32_t, Track> tracks;
303 float duration = 0;
304 float minimumStep = std::numeric_limits<float>::max();
305 std::uint64_t decodedBytes = 28;
306 for (const auto& c : channels) {
307 const auto& channel = object(c);
308 const auto& target = object(member(channel, "target"));
309 auto node = index(member(target, "node"), nodes.size());
310 const auto* path = member(target, "path");
311 require(path && path->isString(), "animation target path is missing");
312 const auto& pathName = path->asString();
313 int kind = pathName == "translation" ? 0 : pathName == "rotation" ? 1 : pathName == "scale" ? 2 : -1;
314 require(kind >= 0, "animation target " + pathName + " is unsupported", DiagnosticCode::Unsupported);
315 const auto& sampler = object(samplers[index(member(channel, "sampler"), samplers.size())]);
316 if (auto* interpolation = member(sampler, "interpolation"))
317 require(interpolation->isString() && interpolation->asString() == "LINEAR",
318 "only LINEAR skeletal interpolation is supported", DiagnosticCode::Unsupported);
319 auto input = accessor(root, buffers, member(sampler, "input"), request.limits);
320 auto values = accessor(root, buffers, member(sampler, "output"), request.limits);
321 require(input.components == 1 && input.count == values.count && values.components == (kind == 1 ? 4u : 3u),
322 "invalid animation sampler shape");
323 auto& track = tracks[static_cast<std::uint32_t>(mapped[node])];
324 require(track.counts[kind] == 0, "duplicate animation channel target");
325 const auto size = std::uint64_t(input.count) * (values.components + 1) * 4;
326 require(
327 size <= request.limits.maximumDecodedBytes && decodedBytes <= request.limits.maximumDecodedBytes - size,
328 "animation decoded budget exceeded");
329 decodedBytes += size;
330 track.counts[kind] = input.count;
331 float previous = -1;
332 for (std::uint32_t key = 0; key < input.count; ++key) {
333 auto time = component(input, key, 0);
334 require(time >= 0 && time > previous, "animation times must be strictly increasing and non-negative");
335 if (key > 0) minimumStep = std::min(minimumStep, time - previous);
336 previous = time;
337 duration = std::max(duration, time);
338 putFloat(track.keys[kind], time);
339 if (kind == 1) {
340 double length = 0;
341 for (std::uint32_t i = 0; i < 4; ++i) {
342 auto x = component(values, key, i);
343 length += double(x) * x;
344 }
345 require(std::abs(length - 1.0) < 0.001, "animation rotation must be a unit quaternion");
346 }
347 for (std::uint32_t componentIndex = 0; componentIndex < values.components; ++componentIndex)
348 putFloat(track.keys[kind], component(values, key, componentIndex));
349 }
350 }
351 std::vector<std::uint8_t> bytes{'E', 'V', 'A', 'N', 'I', 'M', 0, 1};
352 const float rate = minimumStep == std::numeric_limits<float>::max() ? 1.f : 1.f / minimumStep;
353 require(std::isfinite(rate), "animation sample rate is not representable");
356 put32(bytes, 0);
357 put32(bytes, 0);
358 put32(bytes, static_cast<std::uint32_t>(tracks.size()));
359 for (auto& [bone, track] : tracks) {
360 put32(bytes, bone);
361 for (auto count : track.counts) put32(bytes, count);
362 for (const auto& keys : track.keys) bytes.insert(bytes.end(), keys.begin(), keys.end());
363 }
364 const auto source = "animations[" + std::to_string(a) + "]";
365 const auto ref = reference(request.package.packageId.child("gltf:" + source));
366 publish(request, output, ref, "eve.animation-clip",
367 {{"name", Value(name(animation, source, request.limits))},
368 {"durationSeconds", Value(double(duration))},
369 {"sampleRate", Value(double(rate))},
370 {"loop", Value(false)},
371 {"skeleton", Value(skeleton.format())}},
372 std::move(bytes), source);
373 dependency(output, ref, skeleton, "skeleton", "eve.skeleton/1");
374 output.manifest.entrypoints.emplace("animation:" + std::to_string(a), ref);
375 }
376}
377} // namespace
378
380 const std::vector<std::span<const std::uint8_t>>& buffers, PreparedAssetImport& output) {
381 try {
382 append(request, root, buffers, output);
383 } catch (const Rejection& rejected) {
385 Diagnostic::error(rejected.code, rejected.message, request.sourceName, {}, "asset.import"));
386 }
387 return Result<void>::success();
388}
389} // namespace eve::asset_import::gltf
LogicalId target
double value
float w
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float duration
std::map< std::string, std::vector< Key >, std::less<> > channels
int root
Definition AnimSmr.cpp:119
std::string output
std::string from
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
const std::string & s
std::unordered_map< std::string, QuestRuntime > entries
float length
Definition CaveMesh.cpp:94
glm::vec4 p[6]
eve::Value condition
int column
std::map< std::string, Var > values
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
DiagnosticCode code
std::vector< std::uint8_t > blob
const GltfImportRequest & request
std::uint32_t key
vk::UniqueSampler sampler
glm::vec3 n
Definition Grass.cpp:63
float v
std::int32_t second
std::int32_t c
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::string text
TokenKind kind
size_t offset
std::vector< Track > tracks
std::array< float, 3 > position
std::uint32_t bone
std::uint64_t bytes
std::string name
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
std::vector< BvhNode > nodes
graphics::Canvas * previous
std::string path
Definition PlayHost.cpp:110
float begin
const RoadNode * node
double number
bool found
std::uint32_t count
std::size_t cursor
Json object
std::vector< TileLayer::Tileset::Animation > animations
int children
Definition TreeMesh.cpp:295
float size
Definition TreeMesh.cpp:156
float(ui::Theme::* member)[4]
uint32_t index
const UnitySourceAsset & source
const AssetImportLimits & limits
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 sha256(std::span< const std::uint8_t > bytes)
Sha 256.
Result< AssetRef > assetRef(PersistentId id)
Asset ref.
Result< Accessor > accessorAt(const Value::Object &root, const std::vector< std::span< const std::uint8_t > > &buffers, std::uint64_t accessorIndex, const AssetImportLimits &limits)
Validate an accessor retaining only buffer spans borrowed for this import.
Result< float > readFloat(const Accessor &accessor, std::uint32_t element, std::uint32_t component)
Read one bounded finite FLOAT component.
void put32(std::vector< std::uint8_t > &bytes, std::uint32_t value)
Append little-endian uint32 to owned storage.
std::uint32_t little32(std::span< const std::uint8_t > bytes, std::size_t offset)
Read a uint32 after the caller has bounded the byte range.
Result< void > appendAnimation(const GltfImportRequest &request, const Value::Object &root, const std::vector< std::span< const std::uint8_t > > &buffers, PreparedAssetImport &output)
Append owning canonical skeleton, skin and clip assets to an unpublished candidate.
std::uint32_t componentSize(std::uint32_t type)
Return encoded component byte size, or zero for an unknown type.
void putFloat(std::vector< std::uint8_t > &bytes, float value)
Append IEEE FLOAT to owned storage.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
Result< int > publish(Heightmap &target, std::vector< float > candidate)
Publish.
const EditorValue * field(const EditorValue &value, const char *name)
std::vector< int64_t > attrs(const Node &n, const char *key, std::vector< int64_t > fallback)
Attrs.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:635
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:679
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
StrongUuid< detail::PersistentIdTag > PersistentId
Stable instance identity for persistence, networking and process boundaries.
Definition Identity.h:254
detail::StrongUint64< detail::SchemaVersionTag > SchemaVersion
Persistent data-format version; not a runtime replacement generation.
Untrusted .gltf/.glb input plus explicitly supplied external URI bytes.
Owning, validated source archive candidate not yet published to disk/database.
glm::uvec4 counts