fix dynamic bone animations
This commit is contained in:
@@ -7,7 +7,7 @@ import 'package:vector_math/vector_math.dart';
|
||||
|
||||
class AnimationBuilder {
|
||||
final FilamentController controller;
|
||||
BoneAnimationData? BoneAnimationData;
|
||||
// BoneAnimationData? BoneAnimationData;
|
||||
double _frameLengthInMs = 0;
|
||||
double _duration = 0;
|
||||
|
||||
@@ -16,7 +16,7 @@ class AnimationBuilder {
|
||||
double? _interpMorphStartValue;
|
||||
double? _interpMorphEndValue;
|
||||
|
||||
List<BoneAnimationData>? _BoneAnimationDatas = null;
|
||||
// List<BoneAnimationData>? _BoneAnimationDatas = null;
|
||||
|
||||
FilamentEntity asset;
|
||||
String meshName;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:vector_math/vector_math.dart';
|
||||
/// Model class for bone animation frame data.
|
||||
/// To create dynamic/runtime bone animations (as distinct from animations embedded in a glTF asset), create an instance containing the relevant
|
||||
/// data and pass to the [setBoneAnimation] method on a [FilamentController].
|
||||
/// [frameData] is laid out as [locX, locY, locZ, rotW, rotX, rotY, rotZ]
|
||||
///
|
||||
class BoneAnimationData {
|
||||
final String boneName;
|
||||
|
||||
@@ -13,45 +13,64 @@ import 'package:vector_math/vector_math.dart';
|
||||
/// 4) min/max rotation values (corresponding to -1/1 on the blendshape)
|
||||
///
|
||||
|
||||
class Transformation {
|
||||
final Quaternion rotation;
|
||||
late final Vector3 translation;
|
||||
|
||||
Transformation(this.rotation, {Vector3? translation}) {
|
||||
this.translation = translation ?? Vector3.zero();
|
||||
}
|
||||
}
|
||||
|
||||
class BoneDriver {
|
||||
final String bone;
|
||||
final String blendshape;
|
||||
final Map<String, Transformation>
|
||||
transformations; // maps a blendshape key to a Transformation
|
||||
|
||||
late final Vector3 transMin;
|
||||
late final Vector3 transMax;
|
||||
late final Quaternion rotMin;
|
||||
late final Quaternion rotMax;
|
||||
|
||||
BoneDriver(this.bone, this.blendshape, this.rotMin, this.rotMax,
|
||||
Vector3? transMin, Vector3? transMax) {
|
||||
this.transMin = transMin ?? Vector3.zero();
|
||||
this.transMax = transMax ?? Vector3.zero();
|
||||
}
|
||||
|
||||
factory BoneDriver.fromJsonObject(dynamic jsonObject) {
|
||||
return BoneDriver(
|
||||
jsonObject["bone"],
|
||||
jsonObject["blendshape"],
|
||||
Quaternion.fromFloat32List(Float32List.fromList(jsonObject["rotMin"])),
|
||||
Quaternion.fromFloat32List(Float32List.fromList(jsonObject["rotMax"])),
|
||||
Vector3.fromFloat32List(Float32List.fromList(jsonObject["transMin"])),
|
||||
Vector3.fromFloat32List(Float32List.fromList(jsonObject["transMax"])),
|
||||
);
|
||||
}
|
||||
BoneDriver(this.bone, this.transformations);
|
||||
|
||||
//
|
||||
// Accepts a Float32List containing [numFrames] frames of data for a single morph target weight (for efficiency, this must be unravelled to a single contiguous Float32List).
|
||||
// Returns a generator that yields [numFrames] Quaternions, each representing the (weighted) rotation/translation specified by the mapping of this BoneDriver.
|
||||
//
|
||||
Iterable<Quaternion> transform(List<double> morphTargetFrameData) sync* {
|
||||
for (int i = 0; i < morphTargetFrameData.length; i++) {
|
||||
var weight = (morphTargetFrameData[i] / 2) + 0.5;
|
||||
Iterable<Quaternion> transform(
|
||||
Map<String, List<double>> morphTargetFrameData) sync* {
|
||||
assert(setEquals(
|
||||
morphTargetFrameData.keys.toSet(), transformations.keys.toSet()));
|
||||
var numFrames = morphTargetFrameData.values.first.length;
|
||||
assert(morphTargetFrameData.values.every((x) => x.length == numFrames));
|
||||
for (int frameNum = 0; frameNum < numFrames; frameNum++) {
|
||||
var rotations = transformations.keys.map((blendshape) {
|
||||
var weight = morphTargetFrameData[blendshape]![frameNum];
|
||||
var rotation = transformations[blendshape]!.rotation.clone();
|
||||
rotation.x *= weight;
|
||||
rotation.y *= weight;
|
||||
rotation.z *= weight;
|
||||
return rotation;
|
||||
}).toList();
|
||||
|
||||
yield Quaternion(
|
||||
rotMin.x + (weight * (rotMax.x - rotMin.x)),
|
||||
rotMin.y + (weight * (rotMax.y - rotMin.y)),
|
||||
rotMin.z + (weight * (rotMax.z - rotMin.z)),
|
||||
1.0);
|
||||
yield rotations.fold(
|
||||
rotations.first, (Quaternion a, Quaternion b) => a * b);
|
||||
// todo - bone translations
|
||||
}
|
||||
}
|
||||
|
||||
factory BoneDriver.fromJsonObject(dynamic jsonObject) {
|
||||
throw Exception("TODO");
|
||||
// return BoneDriver(
|
||||
// jsonObject["bone"],
|
||||
// Map<String,Transformation>.fromIterable(jsonObject["blendshape"].map((bsName, quats) {
|
||||
// var q = quats.map(())
|
||||
// MapEntry(k,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// }
|
||||
// yield Quaternion(
|
||||
// rotMin.x + (weight * (rotMax.x - rotMin.x)),
|
||||
// rotMin.y + (weight * (rotMax.y - rotMin.y)),
|
||||
// rotMin.z + (weight * (rotMax.z - rotMin.z)),
|
||||
// 1.0);
|
||||
@@ -15,40 +15,52 @@ class DynamicAnimation {
|
||||
final List<BoneAnimationData> boneAnimation;
|
||||
|
||||
factory DynamicAnimation.load(String meshName, String csvPath,
|
||||
{String? boneDriverConfigPath}) {
|
||||
{List<BoneDriver>? boneDrivers,
|
||||
String? boneDriverConfigPath,
|
||||
double? framerate}) {
|
||||
// create a MorphAnimationData instance from the given CSV
|
||||
var llf = _loadLiveLinkFaceCSV(csvPath);
|
||||
var frameLengthInMs = 1000 / (framerate ?? 60.0);
|
||||
var morphNames = llf
|
||||
.item1; //.where((name) => !boneDrivers.any((element) => element.blendshape == name));
|
||||
var morphAnimationData = MorphAnimationData(
|
||||
meshName,
|
||||
llf.item2,
|
||||
morphNames,
|
||||
1000 / 60.0,
|
||||
);
|
||||
var morphAnimationData =
|
||||
MorphAnimationData(meshName, llf.item2, morphNames, frameLengthInMs);
|
||||
|
||||
final boneAnimations = <BoneAnimationData>[];
|
||||
|
||||
// if applicable, load the bone driver config
|
||||
if (boneDriverConfigPath != null) {
|
||||
var boneData = json.decode(File(boneDriverConfigPath).readAsStringSync());
|
||||
// for each driver
|
||||
for (var key in boneData.keys()) {
|
||||
var driver = BoneDriver.fromJsonObject(boneData[key]);
|
||||
if (boneDrivers != null) {
|
||||
throw Exception(
|
||||
"Specify either boneDrivers, or the config path, not both");
|
||||
}
|
||||
boneDrivers = [
|
||||
json
|
||||
.decode(File(boneDriverConfigPath).readAsStringSync())
|
||||
.map(BoneDriver.fromJsonObject)
|
||||
.toList()
|
||||
];
|
||||
}
|
||||
|
||||
// iterate over every bone driver
|
||||
if (boneDrivers != null) {
|
||||
for (var driver in boneDrivers) {
|
||||
// get all frames for the single the blendshape
|
||||
var morphFrameData =
|
||||
morphAnimationData.getData(driver.blendshape).toList();
|
||||
var morphData = driver.transformations
|
||||
.map((String blendshape, Transformation transformation) {
|
||||
return MapEntry(
|
||||
blendshape, morphAnimationData.getData(blendshape).toList());
|
||||
});
|
||||
|
||||
// apply the driver to the blendshape weight
|
||||
var transformedQ = driver.transform(morphFrameData).toList();
|
||||
var transformedQ = driver.transform(morphData).toList();
|
||||
|
||||
// transform the quaternion to a Float32List
|
||||
var transformedF = _quaternionToFloatList(transformedQ);
|
||||
|
||||
// add to the list of boneAnimations
|
||||
boneAnimations.add(BoneAnimationData(
|
||||
driver.bone, meshName, transformedF, 1000.0 / 60.0));
|
||||
driver.bone, meshName, transformedF, frameLengthInMs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +68,11 @@ class DynamicAnimation {
|
||||
}
|
||||
|
||||
static Float32List _quaternionToFloatList(List<Quaternion> quats) {
|
||||
var data = Float32List(quats.length * 4);
|
||||
var data = Float32List(quats.length * 7);
|
||||
int i = 0;
|
||||
for (var quat in quats) {
|
||||
data.addAll([0, 0, 0, quat.w, quat.x, quat.y, quat.z]);
|
||||
data.setRange(i, i + 7, [0, 0, 0, quat.w, quat.x, quat.y, quat.z]);
|
||||
i += 7;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -86,7 +100,7 @@ class DynamicAnimation {
|
||||
// CSVs may contain rows where the "BlendShapeCount" column is set to "0" and/or the weight columns are simply missing.
|
||||
// This can happen when something went wrong while recording via an app (e.g. LiveLinkFace)
|
||||
// Whenever we encounter this type of row, we consider that all weights should be set to zero for that frame.
|
||||
if (numFrameWeights == int.parse(frame[1])) {
|
||||
if (numFrameWeights != int.parse(frame[1])) {
|
||||
_data.addAll(List<double>.filled(numBlendShapes, 0.0));
|
||||
continue;
|
||||
}
|
||||
|
||||
12
lib/animations/live_link_face_bone_driver.dart
Normal file
12
lib/animations/live_link_face_bone_driver.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:polyvox_filament/animations/bone_driver.dart';
|
||||
import 'package:vector_math/vector_math.dart';
|
||||
|
||||
BoneDriver getLiveLinkFaceBoneDrivers(String bone) {
|
||||
return BoneDriver(bone, {
|
||||
"HeadPitch": Transformation(Quaternion.axisAngle(Vector3(1, 0, 0), pi / 2)),
|
||||
"HeadRoll": Transformation(Quaternion.axisAngle(Vector3(0, 0, 1), pi / 2)),
|
||||
"HeadYaw": Transformation(Quaternion.axisAngle(Vector3(0, 1, 0), pi / 2)),
|
||||
});
|
||||
}
|
||||
@@ -24,8 +24,9 @@ class MorphAnimationData {
|
||||
final double frameLengthInMs;
|
||||
|
||||
Iterable<double> getData(String morphName) sync* {
|
||||
int index = morphNames.indexOf(morphName);
|
||||
for (int i = 0; i < numFrames; i++) {
|
||||
yield data[i * numMorphWeights];
|
||||
yield data[(i * numMorphWeights) + index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user