Approaches of Parsing Bone Representations

Read or post any tutorial related to file format analysis for modding purposes.
Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Approaches of Parsing Bone Representations

As far as I know, bones are a set of linked nodes with transformation info includes translation, rotation and scaling, and due to the various representations of rotation, there're mostly 3 forms of representing a bone: Euler angle, rotation matrix and quaternion combined with the rest transformation.

Recognizing these forms is not a big deal, but parsing them correctly is. Therefore I decided to create a thread for an in-depth discussion of the common approaches of parsing different forms of bone representations, hopefully to give those who have only a scanty knowledge to this topic, me included, a hint of parsing a specific bone format.

For those who're completely new to these stuffs, you can refer to the Background/Basic Knowledge for a rough understanding.

Since I'd like to focus on parsing bones correctly, I will use minimum dumps of bone data only as samples, and a quick approach to build the skeleton, which in my case would be SkelBuilder — a lightweight ASCII FBX creator I made for constructing skeleton only, after countless failures with Noesis. Guess I may also post the Noesis python code to see if someone could spot the issues. But any other approaches are fine, so long as it's easy for understanding the process.

Source of SkelBuilder(Bone data not included).
• SkelBuilder_VS2015.zip
There're several working demonstrations in the source so I'm not exploring the usage here.

Quick entrance:
• Right-Handed System
• Quaternion Rotation
• 4x3 Matrices
• 4x4 Matrices
• Left-Handed System
• Quaternion Rotation
• 4x3 Matrices
Conclusion:
So far, seems there's no extra things needed to do for right-handed formats. For left-handed formats, they can be converted to right-handed by swapping the x-z axis. Not quite sure about why exactly the rotation orientations are equivalent.
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Thu Feb 21, 2019 7:06 am, edited 5 times in total.
"No investigation means no right to speak." Say it with action: click the when you get helped.

Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Re: Approaches of Parsing Bone Representations

Think I'll start with a few working examples first.
car_pagani_huayra.zip

Code: Select all

``````# Dump Name: car_pagani_huayra.pig
# From Game: Asphalt 8: Airborne
# Platform: Android
# Bone Format: Quaternion Rotation, Translation
# Coordinate System: Right-Hand
# Endian: Little
``````
Bone data structure:

Code: Select all

``````long	Signature
word	boneCount
for i = 0 < boneCount
long	Signature
word	nameLen
char	boneName[nameLen]
char	Zero
short	parentID
float	Translation[3]
float	Rotation[4] // Quaternion Rotation
float	Scaling[3]
char	NULL[6]``````
Very simple format and since it's already in right-handed system, you don't even need to worry about the convertion between different coordinate systems.
Transformation is referenced to parent space so just need to convert quaternion to Euler angle. It's even working with Noesis.

Noesis python code:

Code: Select all

``````#load the model
def noepyLoadModel(data, mdlList):
bs = NoeBitStream(data)
boneCount = bs.readUShort()
bones = []
for i in range(0, boneCount):
bs.seek(4, NOESEEK_REL)
NameLen = bs.readUShort()
boneName = noeStrFromBytes(bs.readBytes(NameLen), "ASCII")
bs.seek(1, NOESEEK_REL)
bonePIndex = bs.readShort()
Tran = NoeVec3.fromBytes(bs.readBytes(12))
Rot = NoeQuat.fromBytes(bs.readBytes(16))
Scal = NoeVec3.fromBytes(bs.readBytes(12))
bs.seek(6, NOESEEK_REL)
boneMat = Rot.toMat43(transposed = 1)
boneMat[3] = Tran
bones.append( NoeBone(i, boneName, boneMat, None, bonePIndex) )
# Converting local matrix to world space
for i in range(0, boneCount):
j = bones[i].parentIndex
if j != -1:
bones[i].setMatrix(bones[i].getMatrix().__mul__(bones[j].getMatrix()))

mdl = NoeModel()
mdl.setBones(bones)
mdlList.append(mdl)
rapi.setPreviewOption("setAngOfs", "-90 0 0")
return 1``````
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Wed Feb 06, 2019 7:21 am, edited 1 time in total.
"No investigation means no right to speak." Say it with action: click the when you get helped.

Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Re: Approaches of Parsing Bone Representations

Similiar format like Asphalt 8.
Genty_Akylone_car.json.zip

Code: Select all

``````# Dump Name: Genty_Akylone_car.jmodel
# From Game: Asphalt 9: Legends
# Platform: Android
# Bone Format: Quaternion Rotation, Translation
# Coordinate System: Right-Hand
# Engine: Jet Engine
# Endian: Little``````
Bone data format:

Code: Select all

``````byte	Skip1[0x11]
long	boneDataOffset
word	boneCount
byte	Skip2[0x1C]
for i = 0 < boneCount
word	nameLen
char	boneName[nameLen]
short	parentID
float	Translation[3]
float	Rotation[4] // Quaternion Rotation
float	Scaling[3]
word	NULL``````

Noesis python code:

Code: Select all

``````#load the model
def noepyLoadModel(data, mdlList):
bs = NoeBitStream(data)
bs.seek(0x11, NOESEEK_ABS)
boneOffset = bs.readUInt()
boneCount = bs.readUShort()
bs.seek(boneOffset, NOESEEK_ABS)
bones = []
for i in range(0, boneCount):
NameLen = bs.readUShort()
boneName = noeStrFromBytes(bs.readBytes(NameLen), "ASCII")
bonePIndex = bs.readShort()
Tran = NoeVec3.fromBytes(bs.readBytes(12))
Rot = NoeQuat.fromBytes(bs.readBytes(16))
Scal = NoeVec3.fromBytes(bs.readBytes(12))
bs.seek(2, NOESEEK_REL)
boneMat = Rot.toMat43(transposed = 0)
boneMat[3] = Tran
bones.append( NoeBone(i, boneName, boneMat, None, bonePIndex) )
# Converting local matrix to world space
for i in range(0, boneCount):
j = bones[i].parentIndex
if j != -1:
bones[i].setMatrix(bones[i].getMatrix().__mul__(bones[j].getMatrix()))

mdl = NoeModel()
mdl.setBones(bones)
mdlList.append(mdl)
rapi.setPreviewOption("setAngOfs", "0 0 180")
return 1``````
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Wed Feb 06, 2019 7:22 am, edited 1 time in total.
"No investigation means no right to speak." Say it with action: click the when you get helped.

Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Re: Approaches of Parsing Bone Representations

VenomSkel.zip

Code: Select all

``````# Dump Name: VenomSkel.AFM
# From Game: Assalt Fire
# Platform: PC
# Bone Format: Quaternion Rotation, Translation
# Coordinate System: Left-Hand
# Engine: Unreal Engine
# Endian: Little``````
Bone data structure:

Code: Select all

``````long	boneCount
for i = 0 < boneCount
long	boneNameIdx // Indexing to original string buffer
long	NULL
long	NULL
float	Rotation[4] // Quaternion Rotation
float	Translation[3]
long	Unknown
long	parentID
long	Marker // FFFFFFFF
for i = 0 < boneCount
string	boneName	// Mapping to bone[i]``````
Unreal Engine use left-handed system so need to flip the XZ-axis to fit with right-handed system. Transformation is also in parent space.

Noesis code works now thanks to shakotay2's correction(lack convertion to right-handed though):

Code: Select all

``````#load the model
def noepyLoadModel(data, mdlList):
bs = NoeBitStream(data)
boneCount = bs.readUInt()
bs.seek(boneCount * 0x34, NOESEEK_REL)
boneNames = []
for i in range(0, boneCount):
boneNames.append(bs.readString())
bs.seek(0x4, NOESEEK_ABS)
bones = []
for i in range(0, boneCount):
bs.seek(12, NOESEEK_REL)
Rot = NoeQuat.fromBytes(bs.readBytes(16))
Tran = NoeVec3.fromBytes(bs.readBytes(12))
bs.seek(4, NOESEEK_REL)
bonePIndex = bs.readInt()
if bonePIndex == i:
bonePIndex = -1
bs.seek(4, NOESEEK_REL)
boneMat = Rot.toMat43(1)
boneMat[3] = Tran
bones.append(NoeBone(i, boneNames[i], boneMat, None, bonePIndex))
# Converting local matrix to world space
for i in range(0, boneCount):
j = bones[i].parentIndex
if j != -1:
bones[i].setMatrix(bones[i].getMatrix().__mul__(bones[j].getMatrix()))
mdl = NoeModel()
mdl.setBones(bones)
mdlList.append(mdl)
rapi.setPreviewOption("setAngOfs", "0 90 0")
return 1
``````
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Wed Feb 06, 2019 7:29 am, edited 1 time in total.
"No investigation means no right to speak." Say it with action: click the when you get helped.

Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Re: Approaches of Parsing Bone Representations

CHAR_Bloxx.zip

Code: Select all

``````# Dump Name: CHAR_Bloxx.ve2
# From Game: Ben 10 Omniverse
# Platform: Xbox 360
# Bone Format: 4x3 Matrices
# Coordinate System: Right-Hand
# Engine: Vicious Engine 2
# Endian: Big``````
Bone data structure:

Code: Select all

``````long	boneCount
for i = 0 < boneCount
long	parentID
float	boneMat[4][3] // Translation[3], Rotation[3][3]
long	nameLen
char	boneName[nameLen]
long	Unknown
``````
The 3x3 rotation matries are in column-major order so need to transpose them before converting to Euler angles.

Noesis code works now thanks to shakotay2's correction:

Code: Select all

``````#load the model
def noepyLoadModel(data, mdlList):
bs = NoeBitStream(data, NOE_BIGENDIAN)
boneCount = bs.readUInt()
bones = []
for i in range(0, boneCount):
bonePIndex = bs.readInt()
Matrix = []
for j in range(0, 12):
Matrix.append(bs.readFloat())
boneMat = NoeMat43( [(Matrix[3],Matrix[4],Matrix[5]),
(Matrix[6],Matrix[7],Matrix[8]),
(Matrix[9],Matrix[10],Matrix[11]),
(Matrix[0],Matrix[1],Matrix[2])] ).transpose()
NameLen = bs.readInt()
boneName = noeStrFromBytes(bs.readBytes(NameLen), "ASCII")
bs.seek(4, NOESEEK_REL)
bones.append( NoeBone(i, boneName, boneMat, None, bonePIndex) )
# Converting local matrix to world space
for i in range(0, boneCount):
j = bones[i].parentIndex
if j != -1:
bones[i].setMatrix(bones[i].getMatrix().__mul__(bones[j].getMatrix()))

mdl = NoeModel()
mdl.setBones(bones)
mdlList.append(mdl)
rapi.setPreviewOption("setAngOfs", "0 90 0")
return 1
``````
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Wed Feb 06, 2019 7:39 am, edited 1 time in total.
"No investigation means no right to speak." Say it with action: click the when you get helped.

Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Re: Approaches of Parsing Bone Representations

Now for a yet not working case:
suitUpTony.zip

Code: Select all

``````# Dump Name: suitUpTony.SKELETON
# From Game: Iron Man 2
# Platform: PS3
# Bone Format: 4x4 Matrices
# Coordinate System: Left-Hand
# Endian: Big``````
Bone data structure:

Code: Select all

``````long64	unknown
long	indexOffset // Relative from 0x10
long	boneNameOffset // Relative from 0x10
long	boneCount
for i = 0 < boneCount
long	boneID
long	parentID
long	RelBoneNameOffset // Relative from boneNameOffset
float	boneMat[4][4]
long	boneIdx[boneCount]
for i = 0 < boneCount
string	boneName``````
I think the transformation is in world space since it makes more sense. But I've tried different combination of flipping axis or changing the sign of the coordinates, none of the results looks correct.

As usually Noesis code not working:

Code: Select all

``````#load the model
def noepyLoadModel(data, mdlList):
bs = NoeBitStream(data, NOE_BIGENDIAN)
bs.seek(8, NOESEEK_ABS)
indexOffset = bs.readUInt() + 0x10
boneNameOffset = bs.readUInt() + 0x10
boneCount = bs.readUInt()
boneIDs=[]
boneNames = []
bs.seek(indexOffset, NOESEEK_ABS)
for i in range(0, boneCount):
boneIDs.append(bs.readUInt())
for i in range(0, boneCount):
boneNames.append(bs.readString())
bs.seek(0x14, NOESEEK_ABS)
bones = []
for i in range(0, boneCount):
boneIndex = bs.readInt()
bonePIndex = bs.readInt()
RelBoneNameOffset = bs.readInt()
if boneIndex == bonePIndex:
bonePIndex = -1
Matrix = []
for j in range(0, 16):
Matrix.append(bs.readFloat())
boneMat = NoeMat43([(Matrix[0], Matrix[4], Matrix[8]),
(Matrix[1], Matrix[5], Matrix[9]),
(Matrix[2], Matrix[6], Matrix[10]),
(Matrix[13],Matrix[12],Matrix[14])])
bones.append( NoeBone(boneIndex, boneNames[boneIndex], boneMat, None, bonePIndex) )#boneIDs[i]

mdl = NoeModel()
mdl.setBones(bones)
mdlList.append(mdl)
#rapi.setPreviewOption("setAngOfs", "0 0 0")
return 1``````
You do not have the required permissions to view the files attached to this post.
"No investigation means no right to speak." Say it with action: click the when you get helped.

shakotay2
MEGAVETERAN
Posts: 2611
Joined: Fri Apr 20, 2012 9:24 am
Location: Nexus, searching for Jim Kirk
Has thanked: 642 times
Been thanked: 1338 times

Re: Approaches of Parsing Bone Representations

(pmed you) seems you need to replace "while" like such for .ve2/.AFM:
j = bones.parentIndex
if j != -1:
Bigchillghost, Reverse Engineering a Game Model: viewtopic.php?f=29&t=17889
extracting simple models: viewtopic.php?f=29&t=10894
Make_H2O-ForzaHor3-jm9.zip
"You quoted the whole thing, what a mess."

Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Re: Approaches of Parsing Bone Representations

leonopteryx.zip

Code: Select all

``````# Dump Name: leonopteryx.xbg
# From Game: James Cameron's AVATAR THE GAME
# Platform: PC
# Bone Format: Quaternion Rotation, Translation
# Coordinate System: Left-Hand
# Engine: Dunia Engine
# Endian: Little
``````
Bone data structure:

Code: Select all

``````long	boneCount
for i = 0 < boneCount
long 	CRC?
long 	boneIndex // sometimes -1
long 	Unknown
long 	parentIndex
float	Rotation[4] // Quaternion
float	Tanslation[3]
float	Scaling[3]
long 	Unknown[3]
long 	boneNameLen
char 	boneName[boneNameLen + 1] // null-terminated
``````
Same procedure as for Assalt Fire.

Noesis code(lack convertion to right-handed):

Code: Select all

``````#load the model
def noepyLoadModel(data, mdlList):
bs = NoeBitStream(data)
boneCount = bs.readUInt()
bones = []
for i in range(0, boneCount):
bs.seek(0xC, NOESEEK_REL)
bonePIndex = bs.readInt()
Rot = NoeQuat.fromBytes(bs.readBytes(16))
Tran = NoeVec3.fromBytes(bs.readBytes(12))
bs.seek(0x18, NOESEEK_REL)
NameLen = bs.readUInt() + 1
boneName = noeStrFromBytes(bs.readBytes(NameLen), "ASCII")
boneMat = Rot.toMat43(transposed = 1)
boneMat[3] = Tran
bones.append( NoeBone(i, boneName, boneMat, None, bonePIndex) )
# Converting local matrix to world space
for i in range(0, boneCount):
j = bones[i].parentIndex
if j != -1:
bones[i].setMatrix(bones[i].getMatrix().__mul__(bones[j].getMatrix()))

mdl = NoeModel()
mdl.setBones(bones)
mdlList.append(mdl)
rapi.setPreviewOption("setAngOfs", "0 -90 0")
return 1
``````
You do not have the required permissions to view the files attached to this post.
"No investigation means no right to speak." Say it with action: click the when you get helped.

Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Re: Approaches of Parsing Bone Representations

Rath.zip

Code: Select all

``````# Dump Name: Rath.rn
# From Game: Ben 10 Omniverse 2
# Platform: 3DS
# Bone Format: 4x3 Matrix
# Coordinate System: Right-Hand
# Endian: Little
``````
Bone data structure:

Code: Select all

``````long	Magic
long	boneDataOffset // Absolute
long	boneCount
long	Skip[4]

for i = 0 < boneCount
long 	Unknown
short	UnknownID1
short	UnknownID2
long 	UnknownOffset // useless
long 	boneDataOffset // relative from this field

for i = 0 < boneCount
long 	Ignore[2]
long 	boneIndex
long 	parentIndex?
long 	Ignore[4]
float	Scaling[3]
float	Rotation[3] // ?
float	Translation[3]

float	Mat43[12] // row-major
float	identityMatrix[12] // row-major
float	Mat43Reverse[12] // row-major

float	Null[3]
``````
Similar to Ben 10 Omniverse on Xbox 360 except the matries are packed. Guess the tricky part would be identifying the parent IDs and the required matries.

Noesis code:

Code: Select all

``````#load the model
def noepyLoadModel(data, mdlList):
bs = NoeBitStream(data)
bs.seek(8, NOESEEK_REL)
boneCount = bs.readUInt()
bs.seek(boneCount * 0x10 + 0x10, NOESEEK_REL)
bones = []
#noesis.logPopup()
for i in range(0, boneCount):
bs.seek(8, NOESEEK_REL)
boneIndex = bs.readInt()
bonePIndex = bs.readInt()
bs.seek(0x34, NOESEEK_REL)
Matrix = []
for j in range(0, 12):
Matrix.append(bs.readFloat())
boneMat = NoeMat43( [(Matrix[0],Matrix[1],Matrix[2]),
(Matrix[4],Matrix[5],Matrix[6]),
(Matrix[8],Matrix[9],Matrix[10]),
(Matrix[3],Matrix[7],Matrix[11])] )#.transpose()
bs.seek(0x6C, NOESEEK_REL)
bones.append( NoeBone(boneIndex, 'bone%02d'%i, boneMat, None, bonePIndex) )
# Converting local matrix to world space
for i in range(0, boneCount):
j = bones[i].parentIndex
if j != -1:
bones[i].setMatrix(bones[i].getMatrix().__mul__(bones[j].getMatrix()))

mdl = NoeModel()
mdl.setBones(bones)
mdlList.append(mdl)
return 1
``````
You do not have the required permissions to view the files attached to this post.
"No investigation means no right to speak." Say it with action: click the when you get helped.

Bigchillghost
ultra-veteran
Posts: 410
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 18 times
Been thanked: 248 times

Re: Approaches of Parsing Bone Representations

mermaid_normal.zip

Code: Select all

``````# Dump Name: mermaid_normal.model
# From Game: GuJian3
# Platform: PC
# Bone Format: Translation, Quaternion
# Coordinate System: Right-Hand
# Engine: Vision Engine
# Endian: Little
``````
Bone data structure:

Code: Select all

``````word	boneCount
for i = 0 < boneCount
long 	nameLen
char 	Name[nameLen]
short	parentID

float	UnknownTranslation[3]
float	UnknownRotation[4] // Quaternion

float	Translation[3]
float	Rotation[4] // Quaternion
``````
Similiar to previous examples.

Noesis code:

Code: Select all

``````#load the model
def noepyLoadModel(data, mdlList):
bs = NoeBitStream(data)
boneCount = bs.readUShort()
bones = []
for i in range(0, boneCount):
NameLen = bs.readInt()
boneName = noeStrFromBytes(bs.readBytes(NameLen), "ASCII")
bonePIndex = bs.readShort()
bs.seek(28, NOESEEK_REL)
Tran = NoeVec3.fromBytes(bs.readBytes(12))
Rot = NoeQuat.fromBytes(bs.readBytes(16))
boneMat = Rot.toMat43(transposed = 0)
boneMat[3] = Tran
bones.append( NoeBone(i, boneName, boneMat, None, bonePIndex) )
# Converting local matrix to world space
for i in range(0, boneCount):
j = bones[i].parentIndex
if j != -1:
bones[i].setMatrix(bones[i].getMatrix().__mul__(bones[j].getMatrix()))

mdl = NoeModel()
mdl.setBones(bones)
mdlList.append(mdl)
rapi.setPreviewOption("setAngOfs", "0 -90 0")
return 1
``````
You do not have the required permissions to view the files attached to this post.
"No investigation means no right to speak." Say it with action: click the when you get helped.