Files
cup_edit/lib/animations/bvh.dart
2024-02-02 18:17:40 +08:00

140 lines
4.8 KiB
Dart

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<RotationMode, Matrix3> _permutations = {
RotationMode.XYZ: Matrix3.identity()
// RotationMode.ZYX:Matrix3.columns([],[],[]),
};
class BVHParser {
static Map<String, String> parseARPRemap(String arpRemapData) {
final remap = <String, String>{};
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<String> meshNames,
{Map<String, String>? remap,
RegExp? boneRegex,
RotationMode rotationMode = RotationMode.ZYX,
Vector3? rootTranslationOffset,
axes = Axes.Filament}) {
// parse the list/hierarchy of bones
final bones = <String>[];
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<int>.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 = <List<Vector3>>[];
final rotationData = <List<Quaternion>>[];
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(<Vector3>[parseResult.translationData] +
List<Vector3>.filled(bones.length - 1, Vector3.zero()));
}
return BoneAnimationData(boneIndices.map((idx) => bones[idx]).toList(),
meshNames, rotationData, translationData, frameLengthInMs,
isModelSpace: true);
}
static ({List<Quaternion> rotationData, Vector3 translationData})
_parseFrameData(String frameLine,
{Vector3? rootTranslationOffset, axes = Axes.Filament}) {
final frameValues = <double>[];
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<Quaternion> 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);
}