Markemp / Cryengine-Converter

A c# program to convert Crytek files to Collada (XML) format
https://www.heffaypresents.com/GitHub/
GNU General Public License v2.0
208 stars 53 forks source link

Animation export support #145

Closed Soreepeong closed 1 year ago

Soreepeong commented 1 year ago

I've been working on ChunkController905 support (implemented this from this), and seems that we can obtain the list of tuples of (animationName, controllerId=bone, time, optional, optional).

The problem is that when I wrote to library_animation in collada file, Blender just won't load the animation. I forgot to commit the wip yesterday so code isn't there (will do that later), but just wanted to ask if you got some pointers on exporting animations, in case you have tried that before.

Markemp commented 1 year ago

I haven't looked at animations before but did dig into the collada docs for it a while back. Blender's implementation of Collada is generally pretty good, but it can get weird some times.

One thing you might want to try is to create a simple model with a couple of bones, add an animation, and then export that file to Collada. It should create the library_animation for you, and then you can compare it to what your animation output looks like to see if there are any differences.

I've been working on adding USD support to the project, which seems like the way forward too. It appears to be the de facto standard. It's been challenging to get it to compile. And I really need to compile it with the /clr option so that I can use the dlls directly in the project (or ideally, make a Nuget package so it's available to everyone). I don't suppose you have any experience compiling c++ projects into the common language runtime? :)

Soreepeong commented 1 year ago

I was trying to integrate FBX SDK with it but lost interest for the day when designing structures for interops, referring to https://github.com/lmcintyre/fbxInterop and https://github.com/TexTools/TT_FBX_Reader . Easiest way would be just dllexporting the functions.

Blender couldn't import the collada file exported from Blender correctly for a complicated one, so I'm starting with this instead: https://gist.github.com/sbarthelemy/610049/a4ee9e6c33af901001aecf1ba61b08a82d4ea40f

Soreepeong commented 1 year ago

It kind of works, but seems that a lot of rotational quats are getting dealt w.r.t. wrong axis. Judging from the connecting part between head and "hair" have distinct border between them, maybe something about model exporting has to be fixed?

https://user-images.githubusercontent.com/3614868/203582998-5653b703-2e43-4dd0-aa77-186cabb48ebb.mp4

I have run the following code, then imported from Blender with another script below.

        var animFile = Model.FromFile(@"...\knuckles.dba");
        var animChunk = (ChunkController_905)animFile.ChunkMap.Values.First();
        using var ms = new FileStream(@"...\knuckles.bin", FileMode.Create, FileAccess.Write);
        // using var ms = new MemoryStream();
        var bw = new BinaryWriter(ms);
        bw.Write(animChunk.Animations.Count);
        foreach (var anim in animChunk.Animations)
        {
            bw.Write(Encoding.UTF8.GetBytes(anim.Name).Length);
            bw.Write(Encoding.UTF8.GetBytes(anim.Name));
            bw.Write(anim.MotionParams.Start);
            bw.Write(anim.MotionParams.End);
            bw.Write(anim.MotionParams.SecsPerTick * anim.MotionParams.TicksPerFrame);
            bw.Write(anim.Controllers.Count);
            foreach (var con in anim.Controllers)
            {
                var boneName = Bones!.BoneList.Single(x => x.ControllerID == con.ControllerID).boneName;
                bw.Write(Encoding.UTF8.GetBytes(boneName).Length);
                bw.Write(Encoding.UTF8.GetBytes(boneName));
                if (con.PosTrack != 0xffffffffu)
                {
                    var kt = animChunk.KeyTimes[(int)con.PosKeyTimeTrack];
                    var kp = animChunk.KeyPositions[(int)con.PosTrack];
                    var count = kt.Count;
                    bw.Write(count);
                    for (var i = 0; i < count; i++)
                    {
                        bw.Write(kt[i]);
                        bw.Write(kp[i].X);
                        bw.Write(kp[i].Y);
                        bw.Write(kp[i].Z);
                    }
                } else
                    bw.Write(0);

                if (con.RotTrack != 0xffffffffu)
                {
                    var rt = animChunk.KeyTimes[(int)con.RotKeyTimeTrack];
                    var rp = animChunk.KeyRotations[(int)con.RotTrack];
                    var count = rt.Count;
                    bw.Write(count);
                    for (var i = 0; i < count; i++)
                    {
                        bw.Write(rt[i]);
                        bw.Write(rp[i].X);
                        bw.Write(rp[i].Y);
                        bw.Write(rp[i].Z);
                        bw.Write(rp[i].W);
                    }
                } else
                    bw.Write(0);
            }
        }
import dataclasses
import struct
import typing
import pickle

import bpy
import mathutils

@dataclasses.dataclass
class MyVector:
    x: float
    y: float
    z: float

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(*struct.unpack("<3f", fp.read(12)))

    @property
    def vector(self):
        return mathutils.Vector((self.x, self.y, self.z))

@dataclasses.dataclass
class MyQuaternion:
    x: float
    y: float
    z: float
    w: float

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(*struct.unpack("<4f", fp.read(16)))

    @property
    def quaternion(self):
        x,y,z,w=self.x,self.y,self.z,self.w
        return mathutils.Quaternion((w,x,y,z))

@dataclasses.dataclass
class BoneAnimation:
    name: str
    translation: list[tuple[float, MyVector]]
    rotation: list[tuple[float, MyQuaternion]]

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(
            name=fp.read(int.from_bytes(fp.read(4), "little")).decode("utf-8"),
            translation=[(*struct.unpack("<f", fp.read(4)), MyVector.from_io(fp))
                         for _ in range(int.from_bytes(fp.read(4), "little"))],
            rotation=[(*struct.unpack("<f", fp.read(4)), MyQuaternion.from_io(fp))
                      for _ in range(int.from_bytes(fp.read(4), "little"))]
        )

@dataclasses.dataclass
class Animation:
    name: str
    start: int
    end: int
    secs_per_frame: float
    bones: list[BoneAnimation]

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(
            name=fp.read(int.from_bytes(fp.read(4), "little")).decode("utf-8"),
            start=int.from_bytes(fp.read(4), "little"),
            end=int.from_bytes(fp.read(4), "little"),
            secs_per_frame=struct.unpack("<f", fp.read(4))[0],
            bones=[BoneAnimation.from_io(fp) for _ in range(int.from_bytes(fp.read(4), "little"))],
        )

@dataclasses.dataclass
class BinaryFile:
    animations: list[Animation]

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(animations=[Animation.from_io(fp) for _ in range(int.from_bytes(fp.read(4), "little"))])

fpath = r"...\knuckles.bin"
anims = BinaryFile.from_io(open(fpath, "rb"))
pickle.dump(anims.animations[128], open(fpath + ".pkl", "wb"))
anims = pickle.load(open(fpath + ".pkl", "rb"))

print(anims.name)

max_keyframe = anims.end - anims.start + 1
bpy.data.scenes[0].frame_start = 1
bpy.data.scenes[0].frame_end = max_keyframe

for action in bpy.data.actions:
    bpy.data.actions.remove(action)

for boneanim in anims.bones:
    bone = bpy.data.objects["Armature"].pose.bones[boneanim.name]

    bone.location = mathutils.Vector()
    bone.rotation_quaternion = mathutils.Quaternion()
    # continue

    translations: list[MyVector | None] = [None] * (max_keyframe + 1)
    rotations: list[MyQuaternion | None] = [None] * (max_keyframe + 1)

    for t, q in boneanim.rotation:
        rotations[int(t) - anims.start] = q

    for t, tr in boneanim.translation:
        translations[int(t) - anims.start] = tr

    bone.location = mathutils.Vector()
    bone.rotation_quaternion = mathutils.Quaternion()

    for i, (tr, ro) in enumerate(zip(translations, rotations)):
        if tr and False:  # later
            bone.location = tr.vector
            bone.keyframe_insert("location", frame=i, group=f"{boneanim.name}")

        if ro:
            bone.rotation_quaternion = ro.quaternion
            bone.keyframe_insert("rotation_quaternion", frame=i, group=f"{boneanim.name}")
Markemp commented 1 year ago

I am fairly confident that cgf-converter doesn't handle rotations correctly. It just happens to get it wrong in just the right way where it works most of the time, but I think I'm still missing something. :(

I'm going to poke around in this space too after the holidays and see if I can help you with the animations. That would be a huge win! Thank you for doing so much ground work on this.

Soreepeong commented 1 year ago

I'm actually implementing a Blender exporter that exports a python script that shall be run inside Blender - got mesh import done for now, and immediately got stuck on what info from the chunks are the "head" and "tail". Anyhow exporting a script would definitely be easier than making a Blender save file ourselves!

Soreepeong commented 1 year ago

https://github.com/Soreepeong/Markemp-Cryengine-Converter/tree/chunkcontroller905

wiiu-things.zip

Animation quat directions are... strange. Just sharing what I've done so far if you feel like tackling at it at a later time!

Markemp commented 1 year ago

Oof... this is complicated. Your 905 controller is pretty amazing. I'm having a hard time figuring out all the animation basics, but still plugging away at this.

Soreepeong commented 1 year ago

I pretty much let it go for the time being; I can get the animation to play okay if all vertices are manually relocated every animation frame but couldn't figure out how to export it into values that blender or gltf would accept correctly.

Markemp commented 1 year ago

Do you mind if I import your updates into my project and do some testing?

Soreepeong commented 1 year ago

Of course not! Feel free to go ahead.

stixsti commented 1 year ago

Hello, I was curious if there were any updates to this as exporting Cryengine animations is really exciting to me!

Markemp commented 1 year ago

Been sidetracked on a couple of other projects. It's still on my high priority list though!

Dizcordum commented 1 year ago

Waiting patiently

Markemp commented 1 year ago

I know. :) Been busy with a few other projects. It's still next on my list though.

Dizcordum commented 1 year ago

I know. :) Been busy with a few other projects. It's still next on my list though.

.caf animations convert is musthave

Soreepeong commented 1 year ago

Just in case - do let me know if any of my code is confusing while working on it!

Dizcordum commented 1 year ago

Any update on animations support? : )

Soreepeong commented 1 year ago

It's done; you just have to compile it yourself until a new release happens. Also note that it has a high chance of not working if not for Rise of Lyric.

Dizcordum commented 1 year ago

It kind of works, but seems that a lot of rotational quats are getting dealt w.r.t. wrong axis. Judging from the connecting part between head and "hair" have distinct border between them, maybe something about model exporting has to be fixed?

I have run the following code, then imported from Blender with another script below.

        var animFile = Model.FromFile(@"...\knuckles.dba");
        var animChunk = (ChunkController_905)animFile.ChunkMap.Values.First();
        using var ms = new FileStream(@"...\knuckles.bin", FileMode.Create, FileAccess.Write);
        // using var ms = new MemoryStream();
        var bw = new BinaryWriter(ms);
        bw.Write(animChunk.Animations.Count);
        foreach (var anim in animChunk.Animations)
        {
            bw.Write(Encoding.UTF8.GetBytes(anim.Name).Length);
            bw.Write(Encoding.UTF8.GetBytes(anim.Name));
            bw.Write(anim.MotionParams.Start);
            bw.Write(anim.MotionParams.End);
            bw.Write(anim.MotionParams.SecsPerTick * anim.MotionParams.TicksPerFrame);
            bw.Write(anim.Controllers.Count);
            foreach (var con in anim.Controllers)
            {
                var boneName = Bones!.BoneList.Single(x => x.ControllerID == con.ControllerID).boneName;
                bw.Write(Encoding.UTF8.GetBytes(boneName).Length);
                bw.Write(Encoding.UTF8.GetBytes(boneName));
                if (con.PosTrack != 0xffffffffu)
                {
                    var kt = animChunk.KeyTimes[(int)con.PosKeyTimeTrack];
                    var kp = animChunk.KeyPositions[(int)con.PosTrack];
                    var count = kt.Count;
                    bw.Write(count);
                    for (var i = 0; i < count; i++)
                    {
                        bw.Write(kt[i]);
                        bw.Write(kp[i].X);
                        bw.Write(kp[i].Y);
                        bw.Write(kp[i].Z);
                    }
                } else
                    bw.Write(0);

                if (con.RotTrack != 0xffffffffu)
                {
                    var rt = animChunk.KeyTimes[(int)con.RotKeyTimeTrack];
                    var rp = animChunk.KeyRotations[(int)con.RotTrack];
                    var count = rt.Count;
                    bw.Write(count);
                    for (var i = 0; i < count; i++)
                    {
                        bw.Write(rt[i]);
                        bw.Write(rp[i].X);
                        bw.Write(rp[i].Y);
                        bw.Write(rp[i].Z);
                        bw.Write(rp[i].W);
                    }
                } else
                    bw.Write(0);
            }
        }
import dataclasses
import struct
import typing
import pickle

import bpy
import mathutils

@dataclasses.dataclass
class MyVector:
    x: float
    y: float
    z: float

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(*struct.unpack("<3f", fp.read(12)))

    @property
    def vector(self):
        return mathutils.Vector((self.x, self.y, self.z))

@dataclasses.dataclass
class MyQuaternion:
    x: float
    y: float
    z: float
    w: float

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(*struct.unpack("<4f", fp.read(16)))

    @property
    def quaternion(self):
        x,y,z,w=self.x,self.y,self.z,self.w
        return mathutils.Quaternion((w,x,y,z))

@dataclasses.dataclass
class BoneAnimation:
    name: str
    translation: list[tuple[float, MyVector]]
    rotation: list[tuple[float, MyQuaternion]]

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(
            name=fp.read(int.from_bytes(fp.read(4), "little")).decode("utf-8"),
            translation=[(*struct.unpack("<f", fp.read(4)), MyVector.from_io(fp))
                         for _ in range(int.from_bytes(fp.read(4), "little"))],
            rotation=[(*struct.unpack("<f", fp.read(4)), MyQuaternion.from_io(fp))
                      for _ in range(int.from_bytes(fp.read(4), "little"))]
        )

@dataclasses.dataclass
class Animation:
    name: str
    start: int
    end: int
    secs_per_frame: float
    bones: list[BoneAnimation]

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(
            name=fp.read(int.from_bytes(fp.read(4), "little")).decode("utf-8"),
            start=int.from_bytes(fp.read(4), "little"),
            end=int.from_bytes(fp.read(4), "little"),
            secs_per_frame=struct.unpack("<f", fp.read(4))[0],
            bones=[BoneAnimation.from_io(fp) for _ in range(int.from_bytes(fp.read(4), "little"))],
        )

@dataclasses.dataclass
class BinaryFile:
    animations: list[Animation]

    @classmethod
    def from_io(cls, fp: typing.IO[bytes]):
        return cls(animations=[Animation.from_io(fp) for _ in range(int.from_bytes(fp.read(4), "little"))])

fpath = r"...\knuckles.bin"
anims = BinaryFile.from_io(open(fpath, "rb"))
pickle.dump(anims.animations[128], open(fpath + ".pkl", "wb"))
anims = pickle.load(open(fpath + ".pkl", "rb"))

print(anims.name)

max_keyframe = anims.end - anims.start + 1
bpy.data.scenes[0].frame_start = 1
bpy.data.scenes[0].frame_end = max_keyframe

for action in bpy.data.actions:
    bpy.data.actions.remove(action)

for boneanim in anims.bones:
    bone = bpy.data.objects["Armature"].pose.bones[boneanim.name]

    bone.location = mathutils.Vector()
    bone.rotation_quaternion = mathutils.Quaternion()
    # continue

    translations: list[MyVector | None] = [None] * (max_keyframe + 1)
    rotations: list[MyQuaternion | None] = [None] * (max_keyframe + 1)

    for t, q in boneanim.rotation:
        rotations[int(t) - anims.start] = q

    for t, tr in boneanim.translation:
        translations[int(t) - anims.start] = tr

    bone.location = mathutils.Vector()
    bone.rotation_quaternion = mathutils.Quaternion()

    for i, (tr, ro) in enumerate(zip(translations, rotations)):
        if tr and False:  # later
            bone.location = tr.vector
            bone.keyframe_insert("location", frame=i, group=f"{boneanim.name}")

        if ro:
            bone.rotation_quaternion = ro.quaternion
            bone.keyframe_insert("rotation_quaternion", frame=i, group=f"{boneanim.name}")

you mean this code? Or the animations branch

Soreepeong commented 1 year ago

The master branch.

Markemp commented 1 year ago

I'm trying to make sure it doesn't break any existing models, but there are some compatibility issues right now. For example, I can no longer convert .chr files from MWO if they have animation models, since the animations for MWO don't work yet with the changes brought in. I'm holding off on a new release until I can get the compatibility issues fixed.

Sorry for the delay on this. I'm as eager to get the new release out there as anyone, but it does take some time.