140 lines
4.8 KiB
Dart
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);
|
|
}
|