import 'dart:io'; import 'dart:math'; import 'dart:ui'; import 'package:flutter_filament/animations/animation_data.dart'; import 'package:vector_math/vector_math_64.dart'; enum RotationMode { ZYX, XYZ } enum Axes { Filament, Blender } Map _permutations = { RotationMode.XYZ: Matrix3.identity() // RotationMode.ZYX:Matrix3.columns([],[],[]), }; class BVHParser { static Map parseARPRemap(String arpRemapData) { final remap = {}; var data = arpRemapData.split("\n"); for (int i = 0; i < data.length;) { var srcBone = data[i].split("%")[0]; if (srcBone.isNotEmpty && srcBone != "None") { var targetBone = data[i + 1].trim(); remap[targetBone] = srcBone; } i += 5; } return remap; } static BoneAnimationData parse(String data, List meshNames, {Map? remap, RegExp? boneRegex, RotationMode rotationMode = RotationMode.ZYX, Vector3? rootTranslationOffset, axes = Axes.Filament}) { // parse the list/hierarchy of bones final bones = []; double frameLengthInMs = 0.0; var iter = data.split("\n").iterator; while (iter.moveNext()) { final line = iter.current.trim(); if (line.startsWith('ROOT') || line.startsWith('JOINT')) { var bone = line.split(' ')[1]; if (remap?.containsKey(bone) == true) { print("Remapping $bone to ${remap![bone]!}"); bone = remap![bone]!; } bones.add(bone); } else if (line.startsWith('Frame Time:')) { var frameTime = line.split(' ').last.trim(); frameLengthInMs = double.parse(frameTime) * 1000; // Convert to milliseconds print("Got frame time $frameTime frameLengthInMs $frameLengthInMs"); break; } } // filter out any bones that don't match the regexp (if provided) final boneIndices = boneRegex == null ? List.generate(bones.length, (index) => index) : bones.indexed .where((bone) => boneRegex.hasMatch(bone.$2)) .map((bone) => bone.$1); // the remaining lines contain the actual animation data // we assume the first six are LOCX LOCY LOCZ ROTZ ROTY ROTX for the root bone, then ROTZ ROTY ROTX for the remainder final translationData = >[]; final rotationData = >[]; while (iter.moveNext()) { final line = iter.current; if (line.isEmpty) { break; } var parseResult = _parseFrameData(line, axes: axes, rootTranslationOffset: rootTranslationOffset); rotationData.add( boneIndices.map((idx) => parseResult.rotationData[idx]).toList()); translationData.add([parseResult.translationData] + List.filled(bones.length - 1, Vector3.zero())); } return BoneAnimationData(boneIndices.map((idx) => bones[idx]).toList(), meshNames, rotationData, translationData, frameLengthInMs, isModelSpace: true); } static ({List rotationData, Vector3 translationData}) _parseFrameData(String frameLine, {Vector3? rootTranslationOffset, axes = Axes.Filament}) { final frameValues = []; for (final entry in frameLine.split(RegExp(r'\s+'))) { if (entry.isNotEmpty) { frameValues.add(double.parse(entry)); } } // first 3 values are root node position (translation), remainder are ZYX rotatons // this is hardcoded assumption for BVH files generated by momask only; won't work for any other animations in general // Blender exports BVH using same coordinate system (i.e. Z is up, Y points into screen) // but Filament uses Y-up/Z forward late Vector3 rootTranslation; late Vector3 Z, Y, X; if (axes == Axes.Blender) { rootTranslation = Vector3(frameValues[0], frameValues[2], -frameValues[1]); Z = Vector3(0, 1, 0); Y = Vector3(0, 0, -1); X = Vector3(1, 0, 0); } else { rootTranslation = Vector3( frameValues[0], frameValues[1], frameValues[2], ); Z = Vector3(0, 0, 1); Y = Vector3(0, 1, 0); X = Vector3(1, 0, 0); } if (rootTranslationOffset != null) { rootTranslation -= rootTranslationOffset; } List frameData = []; for (int i = 3; i < frameValues.length; i += 3) { var raw = frameValues.sublist(i, i + 3); var z = Quaternion.axisAngle(Z, radians(raw[0])); var y = Quaternion.axisAngle(Y, radians(raw[1])); var x = Quaternion.axisAngle(X, radians(raw[2])); var rotation = z * y * x; frameData.add(rotation.normalized()); } return (rotationData: frameData, translationData: rootTranslation); } static double radians(double degrees) => degrees * (pi / 180.0); }