This thread is a continuation of , so be sure to read that one first. I'll skip most of the stuff I've already talked about in that thread that overlaps with this one.
BSK2OZZ creates a skeleton in the ozz format from a BSK. It will also create a bind-pose animation in the ozz format to verify it. BAN2OZZ takes the next step of using the generated ozz skeleton, and then will retarget a BAN animation to an ozz animation compatible with the skeleton. There are naturally some issues that result from this process.
First, the ozz viewer uses OpenGL coordinates, while Silkroad uses DirectX coordinates. You'll notice the animations are using the wrong hand as a result.
Second, the animation logic the ozz viewer uses doesn't match Silkroad's animation engine. This means the timing for the animation is way off. In my screenshot previews, I cranked the time factor up to 5 as well as the animation speed to 5 times faster, and it was still slower than what you see in Silkroad.
Third, the viewer is simply loading the skeleton and applying the animation directly to the skeleton, so there's no interpolation between poses, since only 1 animation is being used relative to the bind-pose itself. This is subtle but if you look at how the character looks going from an idle or combat idle animation into using a skill, the transition movements between those poses won't match what you see in the viewer because the animation is starting from it's first frame and not doing any transitioning.
Included in the zip is the ozz animation for 'chinawoman_vendor01.ban' and the 'chinawoman_skel.bsk' ozz skeleton generated from BSK2OZZ. Like before, only minimal testing has been done to verify I'm on the right track of processing the data and getting stuff displaying.
Usage now requires two arguments (so drag and drop won't work nicely anymore). The first argument should be the ozz skeleton BSK2OZZ created. The second parameter should be the BAN file to target to the animation to. No testing has been done trying to target animations to skeletons that aren't inherently supported, but the code should give a warning if an animation references a bone not in the skeleton.
Source Only
This code is pretty similar to BSK2OZZ's, as I've replaced the BSK processing code with BAN processing code, and made the necessary changes to build an animation instead of a skeleton. The wiki page was used to get the BAN format.
#include "ozz/animation/offline/raw_skeleton.h"
#include "ozz/animation/offline/skeleton_builder.h"
#include "ozz/animation/runtime/skeleton.h"
#include "ozz/animation/offline/animation_builder.h"
#include "ozz/animation/offline/raw_animation.h"
#include "ozz/animation/runtime/animation.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/io/archive.h"
#include <windows.h>
#include <shlwapi.h>
#include <cstdlib>
#include <cstdio>
#include <ostream>
#include <map>
int main_wrapper(int argc, char* argv[])
{
if (argc != 3)
{
std::printf("Usage: BSK2OZZ <path to skeleton ozz> <path to ban>\n");
return EXIT_FAILURE;
}
// We do this to allow people to drop BSK files onto the exe, and have the output go to the folder
// where the exe is, rather than the path where the source file is (which is what Windows does by default)
char cwd[MAX_PATH + 1] = {};
_snprintf_s(cwd, MAX_PATH, "%s", argv[0]);
PathRemoveFileSpecA(cwd);
std::printf("Setting CurrentDirectory to '%s'\n", cwd);
SetCurrentDirectoryA(cwd);
// To make it easier to view the skeleton/animation, we'll just overwrite the files the viewer demo uses.
// Ideally, we'd integrate this program into the viewer itself as an importer plugin, so we can directly
// view from BSK+BAN, but first we needed to get this stuff working (as this is a minimal version of another project)
CreateDirectoryA("media", nullptr);
std::string skelFileName = argv[1];
std::printf("Now loading the ozz skeleton \"%s\"...\n", skelFileName.c_str());
ozz::io::File file(skelFileName.c_str(), "rb");
if (!file.opened())
{
std::printf("\t[ERROR] Could not open the file '%s' for binary reading\n", skelFileName.c_str());
return EXIT_FAILURE;
}
ozz::io::IArchive archive(&file);
if (!archive.TestTag<ozz::animation::Skeleton>())
{
std::printf("\t[ERROR] Archive doesn't contain the expected object type.\n");
return EXIT_FAILURE;
}
ozz::animation::Skeleton skel;
archive >> skel;
std::printf("\tLoaded!\n\n");
std::map<ozz::string, int> boneNameToJointIndex;
// Debug these values, but we need this information to build the animation correctly next
// OZZ requires the bone:animation joint mapping to be 1:1, so we have to do more work than perhaps other
// libraries.
std::printf("Skeleton's Joint Indices\n");
auto ji = 0;
for (auto name : skel.joint_names())
{
boneNameToJointIndex[name] = ji;
std::printf("\t[%i] %s\r\n", ji, name);
++ji;
}
std::printf("\n");
std::string banFileName = argv[2];
std::printf("Now processing the animation file: \"%s\"\n", banFileName.c_str());
// Open the file
FILE* fi = nullptr;
fopen_s(&fi, banFileName.c_str(), "rb");
if (!fi)
{
std::printf("\t[ERROR] Could not open the file for binary reading [error = %i]\n", errno);
return EXIT_FAILURE;
}
// Get the file size
fseek(fi, 0, SEEK_END);
const auto position = ftell(fi);
if (position == -1)
{
std::printf("\t[ERROR] 'ftell' failed [error = %i]\n", errno);
fclose(fi);
return EXIT_FAILURE;
}
// Reset the stream position to the beginning
rewind(fi);
// Allocate memory for the file
std::vector<uint8_t> bytes;
bytes.resize(position);
std::printf("\tFile size is %zi bytes\n", bytes.size());
// Read the file bytes
const auto read = fread_s(bytes.data(), bytes.size(), 1, bytes.size(), fi);
if (read != bytes.size())
{
std::printf("\t[ERROR] Could only read %zi / %zi bytes [error = %i]\n", read, bytes.size(), errno);
fclose(fi);
return EXIT_FAILURE;
}
fclose(fi);
std::printf("=> Success!\n\n");
// Start processing the file's bytes
// All this code is trash, because I don't want to waste time writing a bunch of wrapper code for this.
// We don't want to crash the program in Release mode with the raw memory accesses, so the error checking
// bloat is to make sure people can at least figure out what went wrong with a model so it can be looked
// into if it's a program issue or not.
size_t idx = 0;
// Process 'header'
if (idx + 12 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'header' cannot be processed.\n");
return EXIT_FAILURE;
}
uint8_t expectedHeader[12] =
{
0x4A, 0x4D, 0x58, 0x56, 0x42, 0x41, 0x4E, 0x20, 0x30, 0x31, 0x30, 0x32,
};
if (memcmp(&bytes[idx], expectedHeader, 12) != 0)
{
std::printf("\t[ERROR] Unexpected file data; 'header' has an unsupported value\n");
return EXIT_FAILURE;
}
idx += 12;
// Creates a RawAnimation.
ozz::animation::offline::RawAnimation raw_animation;
raw_animation.tracks.resize(boneNameToJointIndex.size());
// Process 'Int0'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'unkInt0' cannot be processed\n");
return EXIT_FAILURE;
}
auto unkInt0 = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tunkInt0 = %i\n", unkInt0);
// Process 'Int1'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'unkInt1' cannot be processed\n");
return EXIT_FAILURE;
}
auto unkInt1 = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tunkInt1 = %i\n", unkInt1);
// Process 'nameLength'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'nameLength' cannot be processed\n");
return EXIT_FAILURE;
}
auto nameLength = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tnameLength = %i\n", nameLength);
// Process 'name'
if (idx + nameLength > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'name' cannot be processed\n");
return EXIT_FAILURE;
}
auto name = ozz::string(reinterpret_cast<char*>(&bytes[idx]),
reinterpret_cast<char*>(&bytes[idx + nameLength]));
idx += nameLength;
std::printf("\tname = '%s'\n", name.c_str());
// Process 'Duration'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'duration' cannot be processed\n");
return EXIT_FAILURE;
}
auto duration = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tduration = %i\n", duration);
// Process 'FramesPerSecond'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'framesPerSecond' cannot be processed\n");
return EXIT_FAILURE;
}
auto framesPerSecond = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tframesPerSecond = %i\n", framesPerSecond);
raw_animation.duration = static_cast<float>(duration) / static_cast<float>(framesPerSecond);
// Process 'type'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'type' cannot be processed\n");
return EXIT_FAILURE;
}
auto type = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\ttype = %i\n", type);
// Process 'keyframeTimeCount'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'keyframeTimeCount' cannot be processed\n");
return EXIT_FAILURE;
}
auto keyframeTimeCount = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tkeyframeTimeCount = %i\n", keyframeTimeCount);
std::vector<uint32_t> keyframeTimes;
for (uint32_t keyframeTimeIndex = 0; keyframeTimeIndex < keyframeTimeCount; ++keyframeTimeIndex)
{
// Process 'keyframeTime'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'keyframeTime' cannot be processed\n");
return EXIT_FAILURE;
}
auto keyframeTime = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\t\tkeyframeTime = %i\n", keyframeTime);
keyframeTimes.push_back(keyframeTime);
}
// The duration of the animation has to be >= the last key frame time, so update the duration if
// the key frame lands past the duration originally set.
auto dt = static_cast<float>(keyframeTimes.back()) / static_cast<float>(framesPerSecond);
if (dt > raw_animation.duration)
raw_animation.duration = dt;
// Process 'animatedBoneCount'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'animatedBoneCount' cannot be processed\n");
return EXIT_FAILURE;
}
auto animatedBoneCount = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tanimatedBoneCount = %i\n", animatedBoneCount);
std::map<ozz::string, std::vector<std::pair<ozz::math::Quaternion, ozz::math::Float3>>> animatedBones;
for (uint32_t animatedBoneIndex = 0; animatedBoneIndex < animatedBoneCount; ++animatedBoneIndex)
{
std::printf("\t[%i]\n", animatedBoneIndex);
// Process 'boneNameLength'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t\t[ERROR] Unexpected file format; 'boneNameLength' cannot be processed\n");
return EXIT_FAILURE;
}
auto boneNameLength = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\t\tboneNameLength = %i\n", nameLength);
// Process 'boneName'
if (idx + boneNameLength > bytes.size())
{
std::printf("\t\t[ERROR] Unexpected file format; 'boneName' cannot be processed\n");
return EXIT_FAILURE;
}
auto boneName = ozz::string(reinterpret_cast<char*>(&bytes[idx]),
reinterpret_cast<char*>(&bytes[idx + boneNameLength]));
idx += boneNameLength;
std::printf("\t\tboneName = '%s'\n", boneName.c_str());
// Process 'keyframeCount'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t\t[ERROR] Unexpected file format; 'keyframeCount' cannot be processed\n");
return EXIT_FAILURE;
}
auto keyframeCount = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\t\tkeyframeCount = %i\n", keyframeCount);
auto& animatedBone = animatedBones[boneName];
for (uint32_t keyframeIndex = 0; keyframeIndex < keyframeCount; ++keyframeIndex)
{
std::printf("\t\t[%i]\n", keyframeIndex);
// Process 'Rotation'
ozz::math::Quaternion rotation;
if (idx + sizeof(float) * 4 > bytes.size())
{
std::printf("\t\t\t[ERROR] Unexpected file format; 'Rotation' cannot be processed\n");
return EXIT_FAILURE;
}
rotation.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotation.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotation.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotation.w = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\t\t\trotationX = %f\n", rotation.x);
std::printf("\t\t\trotationY = %f\n", rotation.y);
std::printf("\t\t\trotationZ = %f\n", rotation.z);
std::printf("\t\t\trotationW = %f\n", rotation.w);
// Process 'Translation'
ozz::math::Float3 translation;
if (idx + sizeof(float) * 3 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'Translation' cannot be processed\n");
return EXIT_FAILURE;
}
translation.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translation.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translation.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\t\t\ttranslationX = %f\n", translation.x);
std::printf("\t\t\ttranslationY = %f\n", translation.y);
std::printf("\t\t\ttranslationZ = %f\n", translation.z);
std::printf("\n");
animatedBone.emplace_back(std::make_pair(rotation, translation));
}
}
// Finally make sure all bytes are accounted for
if (idx != bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; only %zi / %zi bytes were processed\n", idx, bytes.size());
return EXIT_FAILURE;
}
std::printf("\n");
// Now build the animation
for (auto& kvp : animatedBones)
{
auto itr = boneNameToJointIndex.find(kvp.first);
if (itr == boneNameToJointIndex.end())
{
std::printf("[WARNING] Bone '%s' in animation was not found in the skeleton. Skipping it, but animation oddities might arise.\n", kvp.first.c_str());
continue;
}
auto& track = raw_animation.tracks[itr->second];
for (idx = 0; idx < kvp.second.size(); ++idx)
{
const auto& pair = kvp.second[idx];
float time = static_cast<float>(keyframeTimes[idx]) / static_cast<float>(framesPerSecond);
track.rotations.push_back({ time, pair.first });
track.translations.push_back({ time, pair.second });
}
}
// Test for animation validity. These are the errors that could invalidate
// an animation:
// 1. Animation duration is less than 0.
// 2. Keyframes' are not sorted in a strict ascending order.
// 3. Keyframes' are not within [0, duration] range.
if (!raw_animation.Validate())
{
std::printf("\t[ERROR] Raw animation validation failed!\r\n");
return EXIT_FAILURE;
}
std::string aOutFileName = "media/animation.ozz";
// Creates a AnimationBuilder instance.
ozz::animation::offline::AnimationBuilder aBuilder;
// Executes the builder on the previously prepared RawAnimation, which returns
// a new runtime animation instance.
// This operation will fail and return an empty unique_ptr if the RawAnimation
// isn't valid.
ozz::unique_ptr<ozz::animation::Animation> animation = aBuilder(raw_animation);
if (!animation)
{
std::printf("\t[ERROR] Animation building failed\n");
return EXIT_FAILURE;
}
// save
{
std::printf("\tNow saving the animation...\n");
ozz::io::File file(aOutFileName.c_str(), "wb");
if (!file.opened())
{
std::printf("\t[ERROR] Could not open the file '%s' for binary writing\n", aOutFileName.c_str());
return EXIT_FAILURE;
}
ozz::io::OArchive archive(&file);
archive << *animation;
std::printf("\tSaved!\n");
}
// verify
{
std::printf("\tNow verifying the animation...\n");
ozz::io::File file(aOutFileName.c_str(), "rb");
if (!file.opened())
{
std::printf("\t[ERROR] Could not open the file '%s' for binary reading\n", aOutFileName.c_str());
return EXIT_FAILURE;
}
ozz::io::IArchive archive(&file);
if (!archive.TestTag<ozz::animation::Animation>())
{
std::printf("\t[ERROR] Archive doesn't contain the expected object type.\n");
return EXIT_FAILURE;
}
ozz::animation::Animation anim;
archive >> anim;
std::printf("\tVerified!\n");
}
std::printf("=> Success!\n\n");
return EXIT_SUCCESS;
}
int main(int argc, char* argv[])
{
auto result = main_wrapper(argc, argv);
// Try to pause on exit when we run from explorer.exe and not the cmdline
// https://devblogs.microsoft.com/oldnewthing/20160125-00/?p=92922
// https://docs.microsoft.com/en-us/windows/console/getconsoleprocesslist (function usage changed since blogpost)
DWORD pids[16] = {};
auto count = GetConsoleProcessList(pids, 16);
if (count == 1)
system("pause");
return result;
}
Screenshots
NOTE: I had to convert from mp4 to gif and then optimize the gif again to try and get the files small enough to upload to epvp img. As a result, they're low quality, but should at least give you a rough idea of what the program shows.
Source is included, but I've also bundled the ozz SDK's demo viewer program for convenience (which has 1 false positive) as well as a pre-compiled converter that requires the vs2019 runtime to be installed. Compiling from source should easily work as-is though with no problems (compile in x64 Release). If you want to recompile OZZ SDK stuff though, you'll have a bit more work to do (hence why I'm just using their official SDK package and not running from source)