extractMeshData method

MeshData extractMeshData({
  1. Matrix4? transform,
})

This subtree's geometry flattened into one snapshot, with every descendant transform baked into the vertices.

transform places the result. The default identity leaves the data in this node's local space, which is the frame a collider attached to this node expects; pass globalTransform for world space instead, which is the frame for a collider on a node that has none of its own.

Baking matters because physics does not simulate scale, so a collider can never pick up a scale from the graph the way a mesh does. Pick the frame so that whatever the collider ends up on carries only a translation and a rotation. Runtime glTF import is the case to watch, since fromGlbAsset roots its model under the source handedness flip, which has to go into the vertices rather than onto the collider:

final model = await Node.fromGlbAsset('assets/ground.glb');
final ground = Node(name: 'ground')..add(model);
ground.addComponent(
  Collider(
    shape: model
        .extractMeshData(transform: model.localTransform)
        .toTriMeshShape(),
  ),
);

Scenes from loadScene carry that flip in their vertices already, so there the default frame is the one to use.

An attribute missing from any one primitive is dropped from the whole result, since the merged mesh carries a single attribute set. Skinned geometry contributes its bind pose.

Throws a StateError when the subtree holds no triangles, or holds geometry this cannot read: caller-managed vertex buffers (Geometry.isReadable is false), non-triangle primitives, or instanced meshes, none of which can be flattened into a single mesh.

Implementation

MeshData extractMeshData({Matrix4? transform}) {
  final parts = <MeshData>[];
  _collectMeshData(transform ?? Matrix4.identity(), parts);
  if (parts.isEmpty) {
    throw StateError(
      'Node "$name" has no triangle geometry in its subtree to extract',
    );
  }
  return MeshData.merge(_reduceToSharedAttributes(parts));
}