Approaches of Parsing Bone Representations

Read or post any tutorial related to file format analysis for modding purposes.
Post Reply
User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Approaches of Parsing Bone Representations

Post by Bigchillghost » Fri Feb 01, 2019 7:46 am

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: 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.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Re: Approaches of Parsing Bone Representations

Post by Bigchillghost » Fri Feb 01, 2019 7:54 am

Think I'll start with a few working examples first. :D
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.

Image

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 Sat Feb 23, 2019 9:20 am, edited 2 times in total.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Re: Approaches of Parsing Bone Representations

Post by Bigchillghost » Fri Feb 01, 2019 8:01 am

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
Image

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 Sat Feb 23, 2019 9:21 am, edited 2 times in total.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Re: Approaches of Parsing Bone Representations

Post by Bigchillghost » Fri Feb 01, 2019 8:06 am

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.

Image

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
Image
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Sat Feb 23, 2019 9:22 am, edited 2 times in total.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Re: Approaches of Parsing Bone Representations

Post by Bigchillghost » Fri Feb 01, 2019 8:12 am

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.

Image

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
Image
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Sat Feb 23, 2019 9:23 am, edited 2 times in total.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Re: Approaches of Parsing Bone Representations

Post by Bigchillghost » Fri Feb 01, 2019 8:19 am

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.

Image

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
Image
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Sat Feb 23, 2019 9:24 am, edited 1 time in total.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

User avatar
shakotay2
MEGAVETERAN
MEGAVETERAN
Posts: 2635
Joined: Fri Apr 20, 2012 9:24 am
Location: Nexus, searching for Jim Kirk
Has thanked: 649 times
Been thanked: 1352 times

Re: Approaches of Parsing Bone Representations

Post by shakotay2 » Sun Feb 03, 2019 9:40 pm

(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."

User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Re: Approaches of Parsing Bone Representations

Post by Bigchillghost » Tue Feb 12, 2019 6:02 am

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.

Image

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
Image
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Sat Feb 23, 2019 9:25 am, edited 1 time in total.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Re: Approaches of Parsing Bone Representations

Post by Bigchillghost » Tue Feb 12, 2019 6:09 am

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.

Image

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
Image
You do not have the required permissions to view the files attached to this post.
Last edited by Bigchillghost on Sat Feb 23, 2019 9:26 am, edited 1 time in total.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

User avatar
Bigchillghost
ultra-veteran
ultra-veteran
Posts: 426
Joined: Tue Jul 05, 2016 9:37 am
Has thanked: 21 times
Been thanked: 269 times

Re: Approaches of Parsing Bone Representations

Post by Bigchillghost » Tue Feb 12, 2019 6:12 am

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.

Image

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
Image
You do not have the required permissions to view the files attached to this post.
May you find peace in this puzzle-solving game. Say it with action: click the Image when you get helped.:)

Post Reply