WARNING: This is not a tool that has any practical use for most people. It's nothing new really, only building on what's already been released. It's a project I'm sharing for anyone who likes reading about this stuff (developer experiences) or possibly getting some new ideas for working with different things. It's a long ways away from being useful for the advancement of anything, so you can skip this thread if that's what you're after.
About
3d modeling and animation stuff has always been an extremely weak point for me. Perhaps it has to do with all the math, but I've just not really done much with this stuff over the years. As I continue to learn new things and my interest in game design and development grows, my motivation for finally learning this stuff and being able to work with it has started to pickup.
This project is my first attempt to make sense of these topics. Since I'm a programmer, I started out by wanting to find a library to programmatically work with skeletal animations. I know typically people go straight into writing importers/exporters for their 3d modeling program of choice (blender, max, maya, etc...), but I just don't have enough experience working with that stuff to start there. So instead, I wanted to find another way to visualize the 3d model data in a way that I could easily debug and understand code wise.
I stumbled upon the , and it seemed to be exactly what I was looking for to get my started.
Quote:
ozz-animation is an open source c++ 3d skeletal animation library. It provides runtime character animation functionalities (sampling, blending...), with the toolset to import major DCC formats (Collada, Fbx, glTF...). It proposes a low-level renderer and game-engine agnostic implementation, focusing on performance and memory constraints with a data-oriented design.
The best part is that they have howtos that do exactly what I wanted (these are linked in the source file), which was to build a skeleton and animation at runtime. Since there's also a simple program to visualize the data, most of the work required for the task is simply converting the data in Silkroad's formats to OZZ's formats.
While that sounds pretty simple, the task turned out to be a bit more tricky due to my inexperience working with this stuff. My biggest roadblock was understanding exactly what ozz wanted when it came to using their API, given that Silkroad's data doesn't use a format that works as-is in ozz. OZZ requires a 1:1 matching of skeleton joints to animation joint tracks, so my initial attempts to load a character and play an animation just failed because the number of 'tracks' in BAN files don't match the number of 'bones' in BSK files.
I decided it'd be much simpler to just figure out a way to generate the bind pose animation from the skeleton's bind pose (as I'd have my 1:1 bone/track mapping then). My first project used C# to quickly and effortlessly parse the BSK data, and then I code generated the C++ code that I'd run using the ozz sdk to generate the ozz files. This sort of worked, but my bind pose was wrong. After trying to work it out over a week, I realized I needed to re-generate the skeleton using the correct joint order ozz creates. I had this nasty circular dependency where I need to use the program to generate a skeleton's bind pose animation, then dump the joint indices from that skeleton, only to generate the animation again with re-ordered joints. It's as convoluted as it sounds.
I then decided to just rewrite the BSK parser in C++, so I can build the skeleton at runtime, and then build the correct bind pose animation without jumping through any extra hoops. That is what this project does, although the C++ is super ugly because I just wanted something simple that works to know I'm actually on the right track in working with this data.
I decided I wanted to share this dev project and my experiences because I myself struggle making sense of a lot of this stuff, so I'm sure others will as well. I have a lot of experience doing data format interop though, so the idea of finding a library like ozz and getting the bsk data working in it is something I'm used to. There's not much code or projects to work from though, so despite this being a terrible C++ project (I'm really unhappy with my code, but I just don't want to try and make it better when I won't be using this anymore) I hope perhaps it can be of use to someone else.
This project is only a small step in the direction of what needs to be done. Using my other project, I'm able to load BAN files and play them, but I have to jump through the hoops of getting the data compatible with ozz first. As a result, I'll have to extend this project to also load a BAN file, and then re-build the animation per-skeleton to be able to view it. I don't think you typically should have to do that though, so it'd probably be better just getting this stuff loading in a 3d modeling program instead.
I myself don't have any desire to rebuild a 3d modeling pipeline compatible for Silkroad. Even if someone made the tools and released them, there's the obvious problem that comes up with 3d artists having their work copy/pasted once the files are used in a server due to them being compatible with every other server since they're using Joymax's system. A lot of people have expressed demotivation from trying to do much in this area for this simple reason.
In that regards, as well as my own interests in what it takes to create a custom Silkroad client, it makes more sense to understand using this 3d modeling and animation stuff in a custom library like ozz. While it's a bit theoretical, if you had custom animations exported to ozz, and then integrated the ozz sdk into the client to build the bsk/ban objects in memory from rather than loading from a file (in a properly protected dll), you could certainly increase the amount of work it takes for other people to use your skeleton/animations.
If someone made an in-memory bsk/ban dumping tool though, it might all be for naught, so that's why it's probably a better idea to look into the custom client approach, despite it being a much larger task. Much like the problem with PK2 files, there's only so much you can do to protect assets when you're maintaining compatibility with the original game.
I'm only mentioning some of these topics to answer the question of "why not just make tools for Blender". A few people have already made such private tools, but those tools don't really solve the biggest problem with wanting to create a bunch of new content for Silkroad if everyone will be able to easily use it and the creators don't really get any period of exclusivity having it.
I myself don't think Silkroad has a content problem. I can understand how a lot of people would love to see new areas, monsters, changed skill animations, etc.. so it doesn't look the same, but if the underlying mechanics of the game are going to stay the same (as is the case with pure content updates), then it's all just the same to me. Going out and killing mangnyangs is the same experience as going out and killing whatever max cap level monster there is. I can get why people love having dance animations, but come on, it doesn't affect the game one bit. People are still going to want to bot up their SP and gear, and I think that's a bigger problem than how stale the content of the game is from being so old.
Anyways though, I'm more interested in understanding how all this stuff works and how to build a new content pipeline. No promises on if I keep going with this and create the tools to make new animations and stuff. As I mentioned before, it's a super weak topic for me, and I still have a lot to learn with 3d modeling and animation stuff. I would like to find new and interesting ways to work with this stuff though, so I'll be on the lookout for other libraries like ozz to play with.
Source Only
If you only want to look at the ugly C++ code that is used to convert from the BSK data to OZZ's data, here's the file.
// [NOTES]
// * Usage
// Run 'BSK2OZZ.exe' from the commandline and pass in a fully quoted path to a BSK file you want to convert
// Alternatively, drag and drop a BSK file onto 'BSK2OZZ.exe' to convert it
// Run 'sample_playback.exe' to load and view the generated skeleton/bind pose animation
// * OZZ 0.13.0 SDK is used:
// https://github.com/guillaumeblanc/ozz-animation/releases/tag/0.13.0
// * BSK processing based off of:
// https://github.com/DummkopfOfHachtenduden/SilkroadDoc/wiki/JMXVBSK
// * OZZ code based off of:
// https://github.com/guillaumeblanc/ozz-animation/blob/master/howtos/custom_skeleton_importer.cc
// https://github.com/guillaumeblanc/ozz-animation/blob/master/howtos/custom_animation_importer.cc
// * Compile/Run in 'Release' mode for the 'x64' platform. Optimizations have been disabled to be able
// to somewhat debug (as the sdk doesn't include debug versions, and building from source is complex)
// * The path './ozz-0.13.0-windows64-vs2019/include' was added to the "Additional Include Directories" setting
// * The path './ozz-0.13.0-windows64-vs2019/lib' was added to the "Additional Library Directories" setting
// * The following files have been added to the "Additional Dependencies" setting:
// 'ozz_animation_fbx_r.lib', 'ozz_animation_offline_r.lib', 'ozz_animation_r.lib',
// 'ozz_animation_tools_r.lib', 'ozz_base_r.lib', 'ozz_geometry_r.lib', 'ozz_options_r.lib',
// 'Shlwapi.lib'
// * The precompiled 'bin\samples\playbacksample_playback.exe' that comes with the ozz SDK can be used to
// view the generated skeleton and bind-pose animation. Replace the files in the 'media' folder of the
// 'sample_playback' example with the files generated from this program (or just use the bundled exe).
// * This code is an extremely ugly mix of C/C++ conventions because just using C++ is more straightforward
// (yet more painful) than writing a C# interop library for the OZZ SDK. This is just a dev project to
// better understand 3d modeling stuff, so I'm not going to waste time trying to make overly complicated
// C++ code that is more clean and up to modern standards. Feel free to simplify/rewrite it using modern
// C++ stuff if you want to build around the tool (as I'm not).
// * Not many models have been tested yet, so there might be bugs. The viewer does not show bone names either,
// so it's a bit hard to eye unknown models for issues, especially since no meshes are being loaded. The
// included skeleton is 'Data\prim\skel\mob\roc\roc.bsk'. Coordinate conversion from DirectX to OpenGL has
// not been done either, but since this is just a viewer, it doesn't matter much.
// * 3d modeling and animation stuff is not trivial. Creating a good workflow and the right tools/plugins
// is a very time time consuming and complex task. It is possible though, but a lot of intermediate work
// needs to be done to fully understand the intricacies required. This project is just a step in that direction.
#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>
// This functor is required to iterate joints in a skeleton, as it's senseless to copy/paste
// the iteration code from the header only to modify it for our needs. This specific functor
// will locate a specific joint in the skeleton.
class SkeletonJointFunctor
{
public:
const ozz::animation::offline::RawSkeleton::Joint* Joint;
const ozz::string& JointName;
const ozz::string& ParentJointName;
public:
SkeletonJointFunctor(const ozz::string& jointName, const ozz::string& parentJointName) :
Joint(nullptr), JointName(jointName), ParentJointName(parentJointName)
{
}
void operator()(const ozz::animation::offline::RawSkeleton::Joint& joint,
const ozz::animation::offline::RawSkeleton::Joint* _parent)
{
// First match the joint name (easy)
if (joint.name != JointName)
return;
// Second, make sure the parent name matches, in the case we have bones with the same name, but
// different parents (shouldn't happen normally, but multiple roots are supported so that might trigger it)
if (ParentJointName.empty())
{
// No parent joint name is set, so only check the name if there is a valid parent
if (_parent)
{
if (_parent->name.empty())
Joint = &joint;
}
else
{
// Otherwise, it's a match since no parent and no parent joint name specified
Joint = &joint;
}
}
else
{
// Parent joint name is set, so parent also has to be set to check the name
if (_parent)
{
if (_parent->name == ParentJointName)
Joint = &joint;
}
}
}
};
// This functor is required to iterate joints in a skeleton, as it's senseless to copy/paste
// the iteration code from the header only to modify it for our needs. This specific functor
// will build a raw animation from the skeleton's bind pose.
class AnimationJointFunctor
{
private:
ozz::animation::offline::RawAnimation& RawAnimation;
const std::map<ozz::string, int>& BoneNameToJointIndex;
bool& HasError;
public:
AnimationJointFunctor(
ozz::animation::offline::RawAnimation& rawAnimation,
const std::map<ozz::string, int>& boneNameToJointIndex,
bool& hasError) :
RawAnimation(rawAnimation), BoneNameToJointIndex(boneNameToJointIndex), HasError(hasError)
{
}
void operator()(const ozz::animation::offline::RawSkeleton::Joint& joint,
const ozz::animation::offline::RawSkeleton::Joint* _parent)
{
const auto itr = BoneNameToJointIndex.find(joint.name);
if (itr == BoneNameToJointIndex.end())
{
// This should never happen since we're building the skeleton and then iterating the skeleton to build the animation
std::printf("\t[ERROR] Could not find the index of a joint named '%s' in the skeleton.", joint.name.c_str());
HasError = true;
return;
}
if (itr->second >= static_cast<int>(RawAnimation.tracks.size()))
{
// This should never happen since we're building the skeleton and then iterating the skeleton to build the animation
std::printf("\t[ERROR] The joint index exceeds the animation's track size");
HasError = true;
return;
}
auto& track = RawAnimation.tracks[itr->second];
track.rotations.push_back({ 0, joint.transform.rotation });
track.translations.push_back({ 0, joint.transform.translation });
track.scales.push_back({ 0, joint.transform.scale });
}
};
int main_wrapper(int argc, char* argv[])
{
if (argc != 2)
{
std::printf("Usage: BSK2OZZ <path to bsk>\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/bind pose, 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, but first we needed to get this stuff working (as this is a minimal version of another project)
CreateDirectoryA("media", nullptr);
std::string fileName = argv[1];
std::printf("Now processing the file: \"%s\"\n", fileName.c_str());
// Open the file
FILE* fi = nullptr;
fopen_s(&fi, fileName.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);
// 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, 0x53, 0x4B, 0x20, 0x30, 0x31, 0x30, 0x31,
};
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;
ozz::animation::offline::RawSkeleton raw_skeleton;
// Process 'subPrimCount'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'subPrimCount' cannot be processed\n");
return EXIT_FAILURE;
}
auto subPrimCount = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tsubPrimCount = %i\n", subPrimCount);
std::printf("\n");
int rootCount = 0;
for (uint32_t subPrimIdx = 0; subPrimIdx < subPrimCount; ++subPrimIdx)
{
std::printf("[%i]\n", subPrimIdx);
// Process 'boneType'
if (idx + sizeof(uint8_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'boneType' cannot be processed\n");
return EXIT_FAILURE;
}
uint8_t boneType = bytes[idx]; // 0 = CPrimBone, 1 = CPrimDummy
++idx;
std::printf("\tboneType = %i\n", boneType);
// Not sure if this changes anything or not, so going to just trigger an error for this model until
// it can be confirmed what needs to be done with it.
if (boneType == 1)
{
std::printf("\t[ERROR] Unexpected file data; 'boneType' has an unsupported value of %i\n", boneType);
return EXIT_FAILURE;
}
// Process 'boneNameLength'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\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("\tboneNameLength = %i\n", boneNameLength);
// Process 'boneName'
if (idx + boneNameLength > bytes.size())
{
std::printf("\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("\tboneName = '%s'\n", boneName.c_str());
// Process 'parentBoneNameLength'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'parentBoneNameLength' cannot be processed\n");
return EXIT_FAILURE;
}
auto parentBoneNameLength = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tparentBoneNameLength = %i\n", parentBoneNameLength);
// Process 'parentBoneName'
if (idx + parentBoneNameLength > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'parentBoneName' cannot be processed\n");
return EXIT_FAILURE;
}
auto parentBoneName = ozz::string(reinterpret_cast<char*>(&bytes[idx]),
reinterpret_cast<char*>(&bytes[idx + parentBoneNameLength]));
idx += parentBoneNameLength;
std::printf("\tparentBoneName = '%s'\n", parentBoneName.c_str());
// This will be the current joint we're loading from the bsk
ozz::animation::offline::RawSkeleton::Joint* joint = nullptr;
// This is our root bone if there's no parent
if (parentBoneName.empty())
{
raw_skeleton.roots.emplace_back(ozz::animation::offline::RawSkeleton::Joint());
joint = &raw_skeleton.roots[rootCount];
++rootCount;
std::printf("\t\t=> IsRoot [%i total roots]\n", rootCount);
}
else
{
// The joint should already exist if it's not a root joint, as it has to be a child of a previous joint
// (which creates its children's placeholders)
SkeletonJointFunctor jf(boneName, parentBoneName);
ozz::animation::offline::IterateJointsBF<SkeletonJointFunctor&>(raw_skeleton, jf);
joint = const_cast<ozz::animation::offline::RawSkeleton::Joint*>(jf.Joint);
}
if (!joint)
{
std::printf("\t[ERROR] Unexpected file data; Joint '%s:%s' was not found\n",
boneName.c_str(), parentBoneName.c_str());
return EXIT_FAILURE;
}
joint->name = boneName;
// NOTE: Since ozz::math structs are not POD types, don't cast it directly and instead read
// into each field. We're in x64 land and to be sure we don't run into alignment issues or other
// undefined behavior bugs, we can't use the hacks that might otherwise work most of the time in x86.
// Process 'rotationToParent'
ozz::math::Quaternion rotationToParent;
if (idx + sizeof(float) * 4 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'rotationToParent' cannot be processed\n");
return EXIT_FAILURE;
}
rotationToParent.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToParent.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToParent.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToParent.w = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\trotationToParentX = %f\n", rotationToParent.x);
std::printf("\trotationToParentY = %f\n", rotationToParent.y);
std::printf("\trotationToParentZ = %f\n", rotationToParent.z);
std::printf("\trotationToParentW = %f\n", rotationToParent.w);
// Process 'translationToParent'
ozz::math::Float3 translationToParent;
if (idx + sizeof(float) * 3 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'translationToParent' cannot be processed\n");
return EXIT_FAILURE;
}
translationToParent.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToParent.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToParent.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\ttranslationToParentX = %f\n", translationToParent.x);
std::printf("\ttranslationToParentY = %f\n", translationToParent.y);
std::printf("\ttranslationToParentZ = %f\n", translationToParent.z);
// Now we can set the transform of the joint
joint->transform.translation = translationToParent;
joint->transform.rotation = rotationToParent;
joint->transform.scale = ozz::math::Float3(1.0f, 1.0f, 1.0f);
// Process 'rotationToOrigin'
ozz::math::Quaternion rotationToOrigin;
if (idx + sizeof(float) * 4 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'rotationToOrigin' cannot be processed\n");
return EXIT_FAILURE;
}
rotationToOrigin.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToOrigin.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToOrigin.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToOrigin.w = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\trotationToOriginX = %f\n", rotationToOrigin.x);
std::printf("\trotationToOriginY = %f\n", rotationToOrigin.y);
std::printf("\trotationToOriginZ = %f\n", rotationToOrigin.z);
std::printf("\trotationToOriginW = %f\n", rotationToOrigin.w);
// Process 'translationToOrigin'
ozz::math::Float3 translationToOrigin;
if (idx + sizeof(float) * 3 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'translationToOrigin' cannot be processed\n");
return EXIT_FAILURE;
}
translationToOrigin.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToOrigin.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToOrigin.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\ttranslationToOriginX = %f\n", translationToOrigin.x);
std::printf("\ttranslationToOriginY = %f\n", translationToOrigin.y);
std::printf("\ttranslationToOriginZ = %f\n", translationToOrigin.z);
// Process 'rotationToLocal'
ozz::math::Quaternion rotationToLocal;
if (idx + sizeof(float) * 4 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'rotationToLocal' cannot be processed\n");
return EXIT_FAILURE;
}
rotationToLocal.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToLocal.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToLocal.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToLocal.w = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\trotationToLocalX = %f\n", rotationToLocal.x);
std::printf("\trotationToLocalY = %f\n", rotationToLocal.y);
std::printf("\trotationToLocalZ = %f\n", rotationToLocal.z);
std::printf("\trotationToLocalW = %f\n", rotationToLocal.w);
// Process 'translationToLocal'
ozz::math::Float3 translationToLocal;
if (idx + sizeof(float) * 3 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'translationToLocal' cannot be processed\n");
return EXIT_FAILURE;
}
translationToLocal.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToLocal.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToLocal.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\ttranslationToLocalX = %f\n", translationToLocal.x);
std::printf("\ttranslationToLocalY = %f\n", translationToLocal.y);
std::printf("\ttranslationToLocalZ = %f\n", translationToLocal.z);
// Process 'childBoneCount'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'childBoneCount' cannot be processed\n");
return EXIT_FAILURE;
}
auto childBoneCount = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tchildBoneCount = %i\n", childBoneCount);
joint->children.resize(childBoneCount);
std::vector<ozz::string> childBones;
for (uint32_t childBoneIdx = 0; childBoneIdx < childBoneCount; ++childBoneIdx)
{
// Process 'childBoneNameLength'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t\t[ERROR] Unexpected file format; 'childBoneNameLength' cannot be processed\n");
return EXIT_FAILURE;
}
auto childBoneNameLength = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\t\tchildBoneNameLength = %i\n", childBoneNameLength);
// Process 'childBoneName'
if (idx + childBoneNameLength > bytes.size())
{
std::printf("\t\t[ERROR] Unexpected file format; 'childBoneName' cannot be processed\n");
return EXIT_FAILURE;
}
auto childBoneName = ozz::string(reinterpret_cast<char*>(&bytes[idx]),
reinterpret_cast<char*>(&bytes[idx + childBoneNameLength]));
idx += childBoneNameLength;
std::printf("\t\tchildBoneName = '%s'\n", childBoneName.c_str());
joint->children[childBoneIdx].name = childBoneName;
}
std::printf("\n");
}
// Process 'unkUInt0'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'unkUInt0' cannot be processed\n");
return EXIT_FAILURE;
}
auto unkUInt0 = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tunkUInt0 = %i\n", unkUInt0);
// Process 'unkUInt1'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'unkUInt1' cannot be processed\n");
return EXIT_FAILURE;
}
auto unkUInt1 = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tunkUInt1 = %i\n", unkUInt1);
// 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");
// Test for skeleton validity.
// The main invalidity reason is the number of joints, which must be lower
// than ozz::animation::Skeleton::kMaxJoints.
if (!raw_skeleton.Validate())
{
std::printf("\t[ERROR] Raw skeleton validation failed\n");
return EXIT_FAILURE;
}
std::string sOutFileName = "media/skeleton.ozz";
// Creates a SkeletonBuilder instance.
ozz::animation::offline::SkeletonBuilder sBuilder;
// Executes the builder on the previously prepared RawSkeleton, which returns
// a new runtime skeleton instance.
// This operation will fail and return an empty unique_ptr if the RawSkeleton
// isn't valid.
ozz::unique_ptr<ozz::animation::Skeleton> skeleton = sBuilder(raw_skeleton);
if (!skeleton)
{
std::printf("\t[ERROR] Skeleton building failed\n");
return EXIT_FAILURE;
}
// save
{
std::printf("\tNow saving the skeleton...\n");
ozz::io::File file(sOutFileName.c_str(), "wb");
if (!file.opened())
{
std::printf("\t[ERROR] Could not open the file '%s' for binary writing\n", sOutFileName.c_str());
return EXIT_FAILURE;
}
ozz::io::OArchive archive(&file);
archive << *skeleton;
std::printf("\tSaved!\n");
}
// verify
{
std::printf("\tNow verifying the skeleton...\n");
ozz::io::File file(sOutFileName.c_str(), "rb");
if (!file.opened())
{
std::printf("\t[ERROR] Could not open the file '%s' for binary reading\n", sOutFileName.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("\tVerified!\n");
}
std::printf("\n");
std::map<ozz::string, int> boneNameToJointIndex;
// Debug these values, but we need this information to build the bind-pose 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 : skeleton->joint_names())
{
boneNameToJointIndex[name] = ji;
std::printf("\t[%i] %s\r\n", ji, name);
++ji;
}
std::printf("\n");
// Creates a RawAnimation.
ozz::animation::offline::RawAnimation raw_animation;
raw_animation.duration = 1; // bind-pose doesn't move
raw_animation.tracks.resize(boneNameToJointIndex.size());
// Now we build the raw animation
auto hasError = false;
AnimationJointFunctor ajf(raw_animation, boneNameToJointIndex, hasError);
ozz::animation::offline::IterateJointsBF<AnimationJointFunctor&>(raw_skeleton, ajf);
if (hasError)
{
std::printf("\t[ERROR] AnimationJointFunctor failed!\r\n");
return EXIT_FAILURE;
}
// 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: The image on the right is taken from Google, and is only a reference to match the skeleton in the bind pose against. The image on the left is what you'll see with the viewer.
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)
DaxterSoul - For creating/maintaining the SilkroadDoc repo, essentially giving everyone the basics of what they need to work with this stuff without having to reverse the client themselves JellyBitz - Repo contributions and motivational posts showing off his progress doing all this stuff and more in Blender (I have to catch up!) Cruor, theonly112, and anyone else I've missed who has contributed to the initial reversing of all this modeling stuff ages ago
I think the ideal community is just being able to build up from the works of others, then contributing things yourself, and having the cycle repeat when others then build up from what you do in the future
As I continue to learn new things and my interest in game design and development grows, my motivation for finally learning this stuff and being able to work with it has started to pickup.
Sounds amazing, I was also interested on 3D stuffs since long long ago. Happy with my PC upgrade a few months ago, I'm now able to explore these areas.
For me, the challenge was learning 3D stuffs without a true knowledge before passing to the action, more specifically learning Blender « which I barely touched 10 years ago » . It's a huge advantage having all this over there! The learning curve is expensive, but the result is the best a 3D artist can have.
I've been struggling recently learning about "Clothes simulation" on Blender. It is too much information and more on the way which makes me doubt if I'm going to success, just for adapting the simple cloth simulation which handles Silkroad.
Quote:
Originally Posted by pushedx
... having their work copy/pasted once the files are used in a server due to them being compatible with every other server ...
Yeah, that's the problem most people I know creating 3D stuffs are worry about. I kind have a subdivided opinion into myself:
If you are constantly creating new stuffs, you should not be worry about it. (+Creativity)
A new avatar/pet/monster will not save your server from failure. (+Gameplay)
WARNING: This is not a tool that has any practical use for most people. It's nothing new really, only building on what's already been released. It's a project I'm sharing for anyone who likes reading about this stuff (developer experiences) or possibly getting some new ideas for working with different things. It's a long ways away from being useful for the advancement of anything, so you can skip this thread if that's what you're after.
About
3d modeling and animation stuff has always been an extremely weak point for me. Perhaps it has to do with all the math, but I've just not really done much with this stuff over the years. As I continue to learn new things and my interest in game design and development grows, my motivation for finally learning this stuff and being able to work with it has started to pickup.
This project is my first attempt to make sense of these topics. Since I'm a programmer, I started out by wanting to find a library to programmatically work with skeletal animations. I know typically people go straight into writing importers/exporters for their 3d modeling program of choice (blender, max, maya, etc...), but I just don't have enough experience working with that stuff to start there. So instead, I wanted to find another way to visualize the 3d model data in a way that I could easily debug and understand code wise.
I stumbled upon the , and it seemed to be exactly what I was looking for to get my started.
The best part is that they have howtos that do exactly what I wanted (these are linked in the source file), which was to build a skeleton and animation at runtime. Since there's also a simple program to visualize the data, most of the work required for the task is simply converting the data in Silkroad's formats to OZZ's formats.
While that sounds pretty simple, the task turned out to be a bit more tricky due to my inexperience working with this stuff. My biggest roadblock was understanding exactly what ozz wanted when it came to using their API, given that Silkroad's data doesn't use a format that works as-is in ozz. OZZ requires a 1:1 matching of skeleton joints to animation joint tracks, so my initial attempts to load a character and play an animation just failed because the number of 'tracks' in BAN files don't match the number of 'bones' in BSK files.
I decided it'd be much simpler to just figure out a way to generate the bind pose animation from the skeleton's bind pose (as I'd have my 1:1 bone/track mapping then). My first project used C# to quickly and effortlessly parse the BSK data, and then I code generated the C++ code that I'd run using the ozz sdk to generate the ozz files. This sort of worked, but my bind pose was wrong. After trying to work it out over a week, I realized I needed to re-generate the skeleton using the correct joint order ozz creates. I had this nasty circular dependency where I need to use the program to generate a skeleton's bind pose animation, then dump the joint indices from that skeleton, only to generate the animation again with re-ordered joints. It's as convoluted as it sounds.
I then decided to just rewrite the BSK parser in C++, so I can build the skeleton at runtime, and then build the correct bind pose animation without jumping through any extra hoops. That is what this project does, although the C++ is super ugly because I just wanted something simple that works to know I'm actually on the right track in working with this data.
I decided I wanted to share this dev project and my experiences because I myself struggle making sense of a lot of this stuff, so I'm sure others will as well. I have a lot of experience doing data format interop though, so the idea of finding a library like ozz and getting the bsk data working in it is something I'm used to. There's not much code or projects to work from though, so despite this being a terrible C++ project (I'm really unhappy with my code, but I just don't want to try and make it better when I won't be using this anymore) I hope perhaps it can be of use to someone else.
This project is only a small step in the direction of what needs to be done. Using my other project, I'm able to load BAN files and play them, but I have to jump through the hoops of getting the data compatible with ozz first. As a result, I'll have to extend this project to also load a BAN file, and then re-build the animation per-skeleton to be able to view it. I don't think you typically should have to do that though, so it'd probably be better just getting this stuff loading in a 3d modeling program instead.
I myself don't have any desire to rebuild a 3d modeling pipeline compatible for Silkroad. Even if someone made the tools and released them, there's the obvious problem that comes up with 3d artists having their work copy/pasted once the files are used in a server due to them being compatible with every other server since they're using Joymax's system. A lot of people have expressed demotivation from trying to do much in this area for this simple reason.
In that regards, as well as my own interests in what it takes to create a custom Silkroad client, it makes more sense to understand using this 3d modeling and animation stuff in a custom library like ozz. While it's a bit theoretical, if you had custom animations exported to ozz, and then integrated the ozz sdk into the client to build the bsk/ban objects in memory from rather than loading from a file (in a properly protected dll), you could certainly increase the amount of work it takes for other people to use your skeleton/animations.
If someone made an in-memory bsk/ban dumping tool though, it might all be for naught, so that's why it's probably a better idea to look into the custom client approach, despite it being a much larger task. Much like the problem with PK2 files, there's only so much you can do to protect assets when you're maintaining compatibility with the original game.
I'm only mentioning some of these topics to answer the question of "why not just make tools for Blender". A few people have already made such private tools, but those tools don't really solve the biggest problem with wanting to create a bunch of new content for Silkroad if everyone will be able to easily use it and the creators don't really get any period of exclusivity having it.
I myself don't think Silkroad has a content problem. I can understand how a lot of people would love to see new areas, monsters, changed skill animations, etc.. so it doesn't look the same, but if the underlying mechanics of the game are going to stay the same (as is the case with pure content updates), then it's all just the same to me. Going out and killing mangnyangs is the same experience as going out and killing whatever max cap level monster there is. I can get why people love having dance animations, but come on, it doesn't affect the game one bit. People are still going to want to bot up their SP and gear, and I think that's a bigger problem than how stale the content of the game is from being so old.
Anyways though, I'm more interested in understanding how all this stuff works and how to build a new content pipeline. No promises on if I keep going with this and create the tools to make new animations and stuff. As I mentioned before, it's a super weak topic for me, and I still have a lot to learn with 3d modeling and animation stuff. I would like to find new and interesting ways to work with this stuff though, so I'll be on the lookout for other libraries like ozz to play with.
Source Only
If you only want to look at the ugly C++ code that is used to convert from the BSK data to OZZ's data, here's the file.
// [NOTES]
// * Usage
// Run 'BSK2OZZ.exe' from the commandline and pass in a fully quoted path to a BSK file you want to convert
// Alternatively, drag and drop a BSK file onto 'BSK2OZZ.exe' to convert it
// Run 'sample_playback.exe' to load and view the generated skeleton/bind pose animation
// * OZZ 0.13.0 SDK is used:
// https://github.com/guillaumeblanc/ozz-animation/releases/tag/0.13.0
// * BSK processing based off of:
// https://github.com/DummkopfOfHachtenduden/SilkroadDoc/wiki/JMXVBSK
// * OZZ code based off of:
// https://github.com/guillaumeblanc/ozz-animation/blob/master/howtos/custom_skeleton_importer.cc
// https://github.com/guillaumeblanc/ozz-animation/blob/master/howtos/custom_animation_importer.cc
// * Compile/Run in 'Release' mode for the 'x64' platform. Optimizations have been disabled to be able
// to somewhat debug (as the sdk doesn't include debug versions, and building from source is complex)
// * The path './ozz-0.13.0-windows64-vs2019/include' was added to the "Additional Include Directories" setting
// * The path './ozz-0.13.0-windows64-vs2019/lib' was added to the "Additional Library Directories" setting
// * The following files have been added to the "Additional Dependencies" setting:
// 'ozz_animation_fbx_r.lib', 'ozz_animation_offline_r.lib', 'ozz_animation_r.lib',
// 'ozz_animation_tools_r.lib', 'ozz_base_r.lib', 'ozz_geometry_r.lib', 'ozz_options_r.lib',
// 'Shlwapi.lib'
// * The precompiled 'bin\samples\playbacksample_playback.exe' that comes with the ozz SDK can be used to
// view the generated skeleton and bind-pose animation. Replace the files in the 'media' folder of the
// 'sample_playback' example with the files generated from this program (or just use the bundled exe).
// * This code is an extremely ugly mix of C/C++ conventions because just using C++ is more straightforward
// (yet more painful) than writing a C# interop library for the OZZ SDK. This is just a dev project to
// better understand 3d modeling stuff, so I'm not going to waste time trying to make overly complicated
// C++ code that is more clean and up to modern standards. Feel free to simplify/rewrite it using modern
// C++ stuff if you want to build around the tool (as I'm not).
// * Not many models have been tested yet, so there might be bugs. The viewer does not show bone names either,
// so it's a bit hard to eye unknown models for issues, especially since no meshes are being loaded. The
// included skeleton is 'Data\prim\skel\mob\roc\roc.bsk'. Coordinate conversion from DirectX to OpenGL has
// not been done either, but since this is just a viewer, it doesn't matter much.
// * 3d modeling and animation stuff is not trivial. Creating a good workflow and the right tools/plugins
// is a very time time consuming and complex task. It is possible though, but a lot of intermediate work
// needs to be done to fully understand the intricacies required. This project is just a step in that direction.
#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>
// This functor is required to iterate joints in a skeleton, as it's senseless to copy/paste
// the iteration code from the header only to modify it for our needs. This specific functor
// will locate a specific joint in the skeleton.
class SkeletonJointFunctor
{
public:
const ozz::animation::offline::RawSkeleton::Joint* Joint;
const ozz::string& JointName;
const ozz::string& ParentJointName;
public:
SkeletonJointFunctor(const ozz::string& jointName, const ozz::string& parentJointName) :
Joint(nullptr), JointName(jointName), ParentJointName(parentJointName)
{
}
void operator()(const ozz::animation::offline::RawSkeleton::Joint& joint,
const ozz::animation::offline::RawSkeleton::Joint* _parent)
{
// First match the joint name (easy)
if (joint.name != JointName)
return;
// Second, make sure the parent name matches, in the case we have bones with the same name, but
// different parents (shouldn't happen normally, but multiple roots are supported so that might trigger it)
if (ParentJointName.empty())
{
// No parent joint name is set, so only check the name if there is a valid parent
if (_parent)
{
if (_parent->name.empty())
Joint = &joint;
}
else
{
// Otherwise, it's a match since no parent and no parent joint name specified
Joint = &joint;
}
}
else
{
// Parent joint name is set, so parent also has to be set to check the name
if (_parent)
{
if (_parent->name == ParentJointName)
Joint = &joint;
}
}
}
};
// This functor is required to iterate joints in a skeleton, as it's senseless to copy/paste
// the iteration code from the header only to modify it for our needs. This specific functor
// will build a raw animation from the skeleton's bind pose.
class AnimationJointFunctor
{
private:
ozz::animation::offline::RawAnimation& RawAnimation;
const std::map<ozz::string, int>& BoneNameToJointIndex;
bool& HasError;
public:
AnimationJointFunctor(
ozz::animation::offline::RawAnimation& rawAnimation,
const std::map<ozz::string, int>& boneNameToJointIndex,
bool& hasError) :
RawAnimation(rawAnimation), BoneNameToJointIndex(boneNameToJointIndex), HasError(hasError)
{
}
void operator()(const ozz::animation::offline::RawSkeleton::Joint& joint,
const ozz::animation::offline::RawSkeleton::Joint* _parent)
{
const auto itr = BoneNameToJointIndex.find(joint.name);
if (itr == BoneNameToJointIndex.end())
{
// This should never happen since we're building the skeleton and then iterating the skeleton to build the animation
std::printf("\t[ERROR] Could not find the index of a joint named '%s' in the skeleton.", joint.name.c_str());
HasError = true;
return;
}
if (itr->second >= static_cast<int>(RawAnimation.tracks.size()))
{
// This should never happen since we're building the skeleton and then iterating the skeleton to build the animation
std::printf("\t[ERROR] The joint index exceeds the animation's track size");
HasError = true;
return;
}
auto& track = RawAnimation.tracks[itr->second];
track.rotations.push_back({ 0, joint.transform.rotation });
track.translations.push_back({ 0, joint.transform.translation });
track.scales.push_back({ 0, joint.transform.scale });
}
};
int main_wrapper(int argc, char* argv[])
{
if (argc != 2)
{
std::printf("Usage: BSK2OZZ <path to bsk>\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/bind pose, 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, but first we needed to get this stuff working (as this is a minimal version of another project)
CreateDirectoryA("media", nullptr);
std::string fileName = argv[1];
std::printf("Now processing the file: \"%s\"\n", fileName.c_str());
// Open the file
FILE* fi = nullptr;
fopen_s(&fi, fileName.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);
// 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, 0x53, 0x4B, 0x20, 0x30, 0x31, 0x30, 0x31,
};
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;
ozz::animation::offline::RawSkeleton raw_skeleton;
// Process 'subPrimCount'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'subPrimCount' cannot be processed\n");
return EXIT_FAILURE;
}
auto subPrimCount = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tsubPrimCount = %i\n", subPrimCount);
std::printf("\n");
int rootCount = 0;
for (uint32_t subPrimIdx = 0; subPrimIdx < subPrimCount; ++subPrimIdx)
{
std::printf("[%i]\n", subPrimIdx);
// Process 'boneType'
if (idx + sizeof(uint8_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'boneType' cannot be processed\n");
return EXIT_FAILURE;
}
uint8_t boneType = bytes[idx]; // 0 = CPrimBone, 1 = CPrimDummy
++idx;
std::printf("\tboneType = %i\n", boneType);
// Not sure if this changes anything or not, so going to just trigger an error for this model until
// it can be confirmed what needs to be done with it.
if (boneType == 1)
{
std::printf("\t[ERROR] Unexpected file data; 'boneType' has an unsupported value of %i\n", boneType);
return EXIT_FAILURE;
}
// Process 'boneNameLength'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\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("\tboneNameLength = %i\n", boneNameLength);
// Process 'boneName'
if (idx + boneNameLength > bytes.size())
{
std::printf("\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("\tboneName = '%s'\n", boneName.c_str());
// Process 'parentBoneNameLength'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'parentBoneNameLength' cannot be processed\n");
return EXIT_FAILURE;
}
auto parentBoneNameLength = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tparentBoneNameLength = %i\n", parentBoneNameLength);
// Process 'parentBoneName'
if (idx + parentBoneNameLength > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'parentBoneName' cannot be processed\n");
return EXIT_FAILURE;
}
auto parentBoneName = ozz::string(reinterpret_cast<char*>(&bytes[idx]),
reinterpret_cast<char*>(&bytes[idx + parentBoneNameLength]));
idx += parentBoneNameLength;
std::printf("\tparentBoneName = '%s'\n", parentBoneName.c_str());
// This will be the current joint we're loading from the bsk
ozz::animation::offline::RawSkeleton::Joint* joint = nullptr;
// This is our root bone if there's no parent
if (parentBoneName.empty())
{
raw_skeleton.roots.emplace_back(ozz::animation::offline::RawSkeleton::Joint());
joint = &raw_skeleton.roots[rootCount];
++rootCount;
std::printf("\t\t=> IsRoot [%i total roots]\n", rootCount);
}
else
{
// The joint should already exist if it's not a root joint, as it has to be a child of a previous joint
// (which creates its children's placeholders)
SkeletonJointFunctor jf(boneName, parentBoneName);
ozz::animation::offline::IterateJointsBF<SkeletonJointFunctor&>(raw_skeleton, jf);
joint = const_cast<ozz::animation::offline::RawSkeleton::Joint*>(jf.Joint);
}
if (!joint)
{
std::printf("\t[ERROR] Unexpected file data; Joint '%s:%s' was not found\n",
boneName.c_str(), parentBoneName.c_str());
return EXIT_FAILURE;
}
joint->name = boneName;
// NOTE: Since ozz::math structs are not POD types, don't cast it directly and instead read
// into each field. We're in x64 land and to be sure we don't run into alignment issues or other
// undefined behavior bugs, we can't use the hacks that might otherwise work most of the time in x86.
// Process 'rotationToParent'
ozz::math::Quaternion rotationToParent;
if (idx + sizeof(float) * 4 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'rotationToParent' cannot be processed\n");
return EXIT_FAILURE;
}
rotationToParent.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToParent.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToParent.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToParent.w = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\trotationToParentX = %f\n", rotationToParent.x);
std::printf("\trotationToParentY = %f\n", rotationToParent.y);
std::printf("\trotationToParentZ = %f\n", rotationToParent.z);
std::printf("\trotationToParentW = %f\n", rotationToParent.w);
// Process 'translationToParent'
ozz::math::Float3 translationToParent;
if (idx + sizeof(float) * 3 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'translationToParent' cannot be processed\n");
return EXIT_FAILURE;
}
translationToParent.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToParent.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToParent.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\ttranslationToParentX = %f\n", translationToParent.x);
std::printf("\ttranslationToParentY = %f\n", translationToParent.y);
std::printf("\ttranslationToParentZ = %f\n", translationToParent.z);
// Now we can set the transform of the joint
joint->transform.translation = translationToParent;
joint->transform.rotation = rotationToParent;
joint->transform.scale = ozz::math::Float3(1.0f, 1.0f, 1.0f);
// Process 'rotationToOrigin'
ozz::math::Quaternion rotationToOrigin;
if (idx + sizeof(float) * 4 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'rotationToOrigin' cannot be processed\n");
return EXIT_FAILURE;
}
rotationToOrigin.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToOrigin.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToOrigin.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToOrigin.w = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\trotationToOriginX = %f\n", rotationToOrigin.x);
std::printf("\trotationToOriginY = %f\n", rotationToOrigin.y);
std::printf("\trotationToOriginZ = %f\n", rotationToOrigin.z);
std::printf("\trotationToOriginW = %f\n", rotationToOrigin.w);
// Process 'translationToOrigin'
ozz::math::Float3 translationToOrigin;
if (idx + sizeof(float) * 3 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'translationToOrigin' cannot be processed\n");
return EXIT_FAILURE;
}
translationToOrigin.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToOrigin.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToOrigin.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\ttranslationToOriginX = %f\n", translationToOrigin.x);
std::printf("\ttranslationToOriginY = %f\n", translationToOrigin.y);
std::printf("\ttranslationToOriginZ = %f\n", translationToOrigin.z);
// Process 'rotationToLocal'
ozz::math::Quaternion rotationToLocal;
if (idx + sizeof(float) * 4 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'rotationToLocal' cannot be processed\n");
return EXIT_FAILURE;
}
rotationToLocal.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToLocal.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToLocal.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
rotationToLocal.w = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\trotationToLocalX = %f\n", rotationToLocal.x);
std::printf("\trotationToLocalY = %f\n", rotationToLocal.y);
std::printf("\trotationToLocalZ = %f\n", rotationToLocal.z);
std::printf("\trotationToLocalW = %f\n", rotationToLocal.w);
// Process 'translationToLocal'
ozz::math::Float3 translationToLocal;
if (idx + sizeof(float) * 3 > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'translationToLocal' cannot be processed\n");
return EXIT_FAILURE;
}
translationToLocal.x = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToLocal.y = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
translationToLocal.z = *reinterpret_cast<float*>(&bytes[idx]);
idx += sizeof(float);
std::printf("\ttranslationToLocalX = %f\n", translationToLocal.x);
std::printf("\ttranslationToLocalY = %f\n", translationToLocal.y);
std::printf("\ttranslationToLocalZ = %f\n", translationToLocal.z);
// Process 'childBoneCount'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'childBoneCount' cannot be processed\n");
return EXIT_FAILURE;
}
auto childBoneCount = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tchildBoneCount = %i\n", childBoneCount);
joint->children.resize(childBoneCount);
std::vector<ozz::string> childBones;
for (uint32_t childBoneIdx = 0; childBoneIdx < childBoneCount; ++childBoneIdx)
{
// Process 'childBoneNameLength'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t\t[ERROR] Unexpected file format; 'childBoneNameLength' cannot be processed\n");
return EXIT_FAILURE;
}
auto childBoneNameLength = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\t\tchildBoneNameLength = %i\n", childBoneNameLength);
// Process 'childBoneName'
if (idx + childBoneNameLength > bytes.size())
{
std::printf("\t\t[ERROR] Unexpected file format; 'childBoneName' cannot be processed\n");
return EXIT_FAILURE;
}
auto childBoneName = ozz::string(reinterpret_cast<char*>(&bytes[idx]),
reinterpret_cast<char*>(&bytes[idx + childBoneNameLength]));
idx += childBoneNameLength;
std::printf("\t\tchildBoneName = '%s'\n", childBoneName.c_str());
joint->children[childBoneIdx].name = childBoneName;
}
std::printf("\n");
}
// Process 'unkUInt0'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'unkUInt0' cannot be processed\n");
return EXIT_FAILURE;
}
auto unkUInt0 = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tunkUInt0 = %i\n", unkUInt0);
// Process 'unkUInt1'
if (idx + sizeof(uint32_t) > bytes.size())
{
std::printf("\t[ERROR] Unexpected file format; 'unkUInt1' cannot be processed\n");
return EXIT_FAILURE;
}
auto unkUInt1 = *reinterpret_cast<uint32_t*>(&bytes[idx]);
idx += sizeof(uint32_t);
std::printf("\tunkUInt1 = %i\n", unkUInt1);
// 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");
// Test for skeleton validity.
// The main invalidity reason is the number of joints, which must be lower
// than ozz::animation::Skeleton::kMaxJoints.
if (!raw_skeleton.Validate())
{
std::printf("\t[ERROR] Raw skeleton validation failed\n");
return EXIT_FAILURE;
}
std::string sOutFileName = "media/skeleton.ozz";
// Creates a SkeletonBuilder instance.
ozz::animation::offline::SkeletonBuilder sBuilder;
// Executes the builder on the previously prepared RawSkeleton, which returns
// a new runtime skeleton instance.
// This operation will fail and return an empty unique_ptr if the RawSkeleton
// isn't valid.
ozz::unique_ptr<ozz::animation::Skeleton> skeleton = sBuilder(raw_skeleton);
if (!skeleton)
{
std::printf("\t[ERROR] Skeleton building failed\n");
return EXIT_FAILURE;
}
// save
{
std::printf("\tNow saving the skeleton...\n");
ozz::io::File file(sOutFileName.c_str(), "wb");
if (!file.opened())
{
std::printf("\t[ERROR] Could not open the file '%s' for binary writing\n", sOutFileName.c_str());
return EXIT_FAILURE;
}
ozz::io::OArchive archive(&file);
archive << *skeleton;
std::printf("\tSaved!\n");
}
// verify
{
std::printf("\tNow verifying the skeleton...\n");
ozz::io::File file(sOutFileName.c_str(), "rb");
if (!file.opened())
{
std::printf("\t[ERROR] Could not open the file '%s' for binary reading\n", sOutFileName.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("\tVerified!\n");
}
std::printf("\n");
std::map<ozz::string, int> boneNameToJointIndex;
// Debug these values, but we need this information to build the bind-pose 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 : skeleton->joint_names())
{
boneNameToJointIndex[name] = ji;
std::printf("\t[%i] %s\r\n", ji, name);
++ji;
}
std::printf("\n");
// Creates a RawAnimation.
ozz::animation::offline::RawAnimation raw_animation;
raw_animation.duration = 1; // bind-pose doesn't move
raw_animation.tracks.resize(boneNameToJointIndex.size());
// Now we build the raw animation
auto hasError = false;
AnimationJointFunctor ajf(raw_animation, boneNameToJointIndex, hasError);
ozz::animation::offline::IterateJointsBF<AnimationJointFunctor&>(raw_skeleton, ajf);
if (hasError)
{
std::printf("\t[ERROR] AnimationJointFunctor failed!\r\n");
return EXIT_FAILURE;
}
// 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: The image on the right is taken from Google, and is only a reference to match the skeleton in the bind pose against. The image on the left is what you'll see with the viewer.
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)
DaxterSoul - For creating/maintaining the SilkroadDoc repo, essentially giving everyone the basics of what they need to work with this stuff without having to reverse the client themselves JellyBitz - Repo contributions and motivational posts showing off his progress doing all this stuff and more in Blender (I have to catch up!) Cruor, theonly112, and anyone else I've missed who has contributed to the initial reversing of all this modeling stuff ages ago
I think the ideal community is just being able to build up from the works of others, then contributing things yourself, and having the cycle repeat when others then build up from what you do in the future
So.. in shorter words this one would allow to have all the animations / models exported out from actual game and let them be used with this animation engine or implemented in ur own game (theoretically)?
Please don't spoil the spirit of silkroad online, the game has been badly affected so far, from the software you made and the things you shared, of course there are still some people who do things, but if someone really wants to do something good, and really want, they can do it themselves, please think about it, the game should not be badly affected for, don't spoil the spirit of silkroad
Please don't spoil the spirit of silkroad online, the game has been badly affected so far, from the software you made and the things you shared, of course there are still some people who do things, but if someone really wants to do something good, and really want, they can do it themselves, please think about it, the game should not be badly affected for, don't spoil the spirit of silkroad
silkroad answer: I don't even have a spirit to spoil