fromFlatbuffer static method
Constructs a Geometry from a deserialized flatbuffer mesh primitive, choosing UnskinnedGeometry or SkinnedGeometry based on the embedded vertex buffer type.
The vertex buffer must be a multiple of the layout size (48 bytes unskinned, 80 bytes skinned); a partial trailing vertex is dropped with a debug warning.
Implementation
static Geometry fromFlatbuffer(fb.MeshPrimitive fbPrimitive) {
Uint8List vertices;
bool isSkinned =
fbPrimitive.vertices!.runtimeType == fb.SkinnedVertexBuffer;
int perVertexBytes =
isSkinned ? kSkinnedPerVertexSize : kUnskinnedPerVertexSize;
switch (fbPrimitive.vertices!.runtimeType) {
case const (fb.UnskinnedVertexBuffer):
fb.UnskinnedVertexBuffer unskinned =
(fbPrimitive.vertices as fb.UnskinnedVertexBuffer?)!;
vertices = unskinned.vertices! as Uint8List;
case const (fb.SkinnedVertexBuffer):
fb.SkinnedVertexBuffer skinned =
(fbPrimitive.vertices as fb.SkinnedVertexBuffer?)!;
vertices = skinned.vertices! as Uint8List;
default:
throw Exception('Unknown vertex buffer type');
}
if (vertices.length % perVertexBytes != 0) {
debugPrint(
'OH NO: Encountered an vertex buffer of size '
'${vertices.lengthInBytes} bytes, which doesn\'t match the '
'expected multiple of $perVertexBytes bytes. Possible data corruption! '
'Attempting to use a vertex count of ${vertices.length ~/ perVertexBytes}. '
'The last ${vertices.length % perVertexBytes} bytes will be ignored.',
);
}
int vertexCount = vertices.length ~/ perVertexBytes;
gpu.IndexType indexType = fbPrimitive.indices!.type.toIndexType();
Uint8List indices = fbPrimitive.indices!.data! as Uint8List;
Geometry geometry;
switch (fbPrimitive.vertices!.runtimeType) {
case const (fb.UnskinnedVertexBuffer):
geometry = UnskinnedGeometry();
case const (fb.SkinnedVertexBuffer):
geometry = SkinnedGeometry();
default:
throw Exception('Unknown vertex buffer type');
}
// Pre-populate bounds from the flatbuffer when present so
// uploadVertexData can skip the position scan.
//
// For skinned primitives, only the offline-baked
// `skinned_pose_union_aabb` is sound for cull. The static-pose
// `bounds_aabb` is useful for editor-style queries on bind-pose
// extents but would under-cover the mesh once joints animate.
// When the importer didn't bake a pose-union (older `.model`
// files), bounds remain `null` and `Geometry.uploadVertexData`
// skips the auto-scan for skinned geometries — the runtime
// conservatively treats the subtree as always visible.
//
// Unskinned primitives use the (sound) static AABB and sphere
// from the flatbuffer, falling back to a position scan in
// `uploadVertexData` when the importer didn't bake bounds.
final fbAabb =
isSkinned ? fbPrimitive.skinnedPoseUnionAabb : fbPrimitive.boundsAabb;
if (fbAabb != null) {
geometry._localBounds = vm.Aabb3.minMax(
vm.Vector3(fbAabb.min.x, fbAabb.min.y, fbAabb.min.z),
vm.Vector3(fbAabb.max.x, fbAabb.max.y, fbAabb.max.z),
);
}
if (isSkinned) {
if (geometry._localBounds != null) {
// Derive the sphere from the pose-union AABB so it covers the
// same animated extent. The baked `bounds_sphere` is fit to
// the static bind-pose mesh and would be too small.
geometry._localBoundingSphere = _circumscribedSphere(
geometry._localBounds!,
);
}
// No pose-union: leave both bounds null so the runtime treats
// the subtree as always visible.
} else {
final fbSphere = fbPrimitive.boundsSphere;
if (fbSphere != null) {
geometry._localBoundingSphere = vm.Sphere.centerRadius(
vm.Vector3(fbSphere.center.x, fbSphere.center.y, fbSphere.center.z),
fbSphere.radius,
);
}
}
geometry.uploadVertexData(
ByteData.sublistView(vertices),
vertexCount,
ByteData.sublistView(indices),
indexType: indexType,
);
return geometry;
}