mannequinGlb function

Uint8List mannequinGlb({
  1. List<double> body = const [0.85, 0.6, 0.35, 1],
  2. List<double> clothes = const [0.25, 0.35, 0.6, 1],
})

A box mannequin about 1.8 m tall, standing on y = 0 and facing +Z (glTF axes), skinned to 16 joints with an idle and a walk clip. Made in code, so it is free to use anywhere (CC0).

Joints: hips (the root), spine, chest, head, and per side (.L, .R): upper_arm, forearm, hand, thigh, shin, foot.

Implementation

Uint8List mannequinGlb({
  List<double> body = const [0.85, 0.6, 0.35, 1],
  List<double> clothes = const [0.25, 0.35, 0.6, 1],
}) {
  final w = GlbWriter();

  // Joints: name, parent, bind position (model space).
  final joints = <(String, int, List<double>)>[
    ('hips', -1, [0, 0.95, 0]),
    ('spine', 0, [0, 1.1, 0]),
    ('chest', 1, [0, 1.35, 0]),
    ('head', 2, [0, 1.58, 0]),
  ];
  for (final (side, x) in [('L', 1.0), ('R', -1.0)]) {
    final chest = 2, hips = 0;
    final shoulder = joints.length;
    joints.add(('upper_arm.$side', chest, [0.2 * x, 1.45, 0]));
    joints.add(('forearm.$side', shoulder, [0.2 * x, 1.18, 0]));
    joints.add(('hand.$side', shoulder + 1, [0.2 * x, 0.93, 0]));
    final thigh = joints.length;
    joints.add(('thigh.$side', hips, [0.1 * x, 0.92, 0]));
    joints.add(('shin.$side', thigh, [0.1 * x, 0.5, 0]));
    joints.add(('foot.$side', thigh + 1, [0.1 * x, 0.08, 0]));
  }
  int joint(String name) => joints.indexWhere((j) => j.$1 == name);

  // Boxes: joint, centre, size, colour (0 skin, 1 clothes).
  final boxes = <(int, List<double>, List<double>, int)>[
    (joint('hips'), [0, 0.98, 0], [0.32, 0.18, 0.2], 1),
    (joint('spine'), [0, 1.2, 0], [0.3, 0.22, 0.18], 1),
    (joint('chest'), [0, 1.4, 0], [0.38, 0.2, 0.22], 1),
    (joint('head'), [0, 1.7, 0.01], [0.2, 0.24, 0.22], 0),
  ];
  for (final (side, x) in [('L', 1.0), ('R', -1.0)]) {
    boxes
      ..add((
        joint('upper_arm.$side'),
        [0.2 * x, 1.32, 0],
        [0.09, 0.26, 0.09],
        1,
      ))
      ..add((joint('forearm.$side'), [0.2 * x, 1.06, 0], [0.08, 0.24, 0.08], 0))
      ..add((joint('hand.$side'), [0.2 * x, 0.87, 0], [0.07, 0.1, 0.05], 0))
      ..add((joint('thigh.$side'), [0.1 * x, 0.71, 0], [0.13, 0.42, 0.14], 1))
      ..add((joint('shin.$side'), [0.1 * x, 0.3, 0], [0.1, 0.4, 0.11], 1))
      ..add((joint('foot.$side'), [0.1 * x, 0.04, 0.05], [0.1, 0.08, 0.22], 0));
  }

  final primitives = <Map<String, dynamic>>[];
  final materials = [
    w.addMaterial({
      'name': 'skin',
      'pbrMetallicRoughness': {
        'baseColorFactor': body,
        'metallicFactor': 0,
        'roughnessFactor': 0.7,
      },
    }),
    w.addMaterial({
      'name': 'clothes',
      'pbrMetallicRoughness': {
        'baseColorFactor': clothes,
        'metallicFactor': 0,
        'roughnessFactor': 0.85,
      },
    }),
  ];
  for (var m = 0; m < 2; m++) {
    final positions = <double>[],
        normals = <double>[],
        js = <int>[],
        ws = <double>[],
        indices = <int>[];
    for (final (j, centre, size, material) in boxes) {
      if (material != m) continue;
      _box(centre, size, positions, normals, indices);
      for (var v = 0; v < 24; v++) {
        js.addAll([j, 0, 0, 0]);
        ws.addAll([1, 0, 0, 0]);
      }
    }
    primitives.add({
      'attributes': {
        'POSITION': w.addFloats(positions, 'VEC3', withBounds: true),
        'NORMAL': w.addFloats(normals, 'VEC3'),
        'JOINTS_0': w.addInts(js, 'VEC4', 5121),
        'WEIGHTS_0': w.addFloats(ws, 'VEC4'),
      },
      'indices': w.addInts(indices, 'SCALAR', 5123, indices: true),
      'material': materials[m],
    });
  }
  final mesh = w.addMesh(primitives, name: 'Mannequin');

  // Nodes, children first so parents can list them.
  final nodes = List<int>.filled(joints.length, -1);
  final children = List.generate(joints.length, (_) => <int>[]);
  for (var j = 0; j < joints.length; j++) {
    if (joints[j].$2 >= 0) children[joints[j].$2].add(j);
  }
  void make(int j) {
    for (final c in children[j]) {
      make(c);
    }
    final (name, parent, at) = joints[j];
    final from = parent < 0 ? const [0.0, 0.0, 0.0] : joints[parent].$3;
    nodes[j] = w.addNode({
      'name': name,
      'translation': [at[0] - from[0], at[1] - from[1], at[2] - from[2]],
      if (children[j].isNotEmpty)
        'children': [for (final c in children[j]) nodes[c]],
    });
  }

  make(0);
  final ibm = <double>[];
  for (final (_, _, at) in joints) {
    ibm.addAll([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -at[0], -at[1], -at[2], 1]);
  }
  w.addSkin({
    'joints': nodes,
    'inverseBindMatrices': w.addFloats(ibm, 'MAT4'),
    'skeleton': nodes[0],
  });
  final figure = w.addNode({'name': 'Mannequin', 'mesh': mesh, 'skin': 0});
  w.setScene([figure, nodes[0]]);

  _walk(w, nodes, joint, joints);
  _idle(w, nodes, joint, joints);
  return w.toGlb();
}