Little Skeleton importer module (alpha-beta)

Everything related to 3D programming
benubi
Enthusiast
Enthusiast
Posts: 251
Joined: Tue Mar 29, 2005 4:01 pm

Little Skeleton importer module (alpha-beta)

Post by benubi »

Update

1) I wrote a fully 100% PureBasic skeleton importer for Ogre meshs (see bellow, updated code, still "beta", and only most actual format supported) original problem was that I missed Bones and Animation names

2) There are (yet) undocumented procedures already in PB that do what I was/we were looking :D for (see bellow for pf shadoko's posts)

3) next goals: make a 100% purebasic skeleton serializer, make the mesh unserializer, once that is done a homebrew "CatchMesh" procedure, or perhaps some editor things but I'm bad at math XD

4) Get some wishes and feedback from you, is there something I should put my eyes on? The way it is written for now it has no dependencies, it could be run in a command line program on linux off desktop (RGB() makes linux console programs say they want a desktop).

5) Sorry for being lazy and haven't tried myself, yet. What could already be tested is if the data is correct, if you can create the skeleton in PB/Ogre on the mesh and it works flawlessly. If the imported bone positions are identical with the positions when no animations are running. And simple "editing" on the animations is certainly also possible, like time stretching. As for inserting key frames there needs to be a math genius for that to happen easily, it's not so obvious for me what it means but perhaps the importer isn't working right. I'll do that next. Many little procedures could be possible, like querying all child bones of a parent etc. When the serializer is there renaming, copying, changing animation or skeleton data should be easy. I'm curious about the mysterious Animation Link File thing.

I had to share this quickly with you. Perhaps I will start the mesh file reader this weekend already, we'll see ;)

Btw. the sinbad.skeleton output takes more than 12 seconds on my computer to complete, robo is fastest with 0.5 s and ninja takes 2 s debugging the imported data.

Code: Select all


; source: GetBoneNamesFromFile.pb
; date: 27/Feb/2026
; author: benubi
; OS: all
; License: free
;
; Description:
; ============
; o3d helper modules for Ogre3D
; -------------------------------------------------------------------------------------------------------------

DeclareModule o3d
  
  ; Extract Bone & Pose names from .skeleton files
  
  ; Get from file in memory
  Declare GetBoneNamesFromMemory(*memory, size, Array Result.s(1))   ; Get bone names from memory
  
  Declare GetAnimationNamesFromMemory(*memory, size, Array Result.s(1))   ; Get pose names from memory
  
  Declare ImportSkeletonFromMemory(*memory, size, filename$ = #Empty$) ; returns a filled *o3d_Skeleton structure or null
  
  
  ; Get from file on medium
  Declare GetBoneNamesFromFile(File$, Array Result.s(1)) ; Get bone names from file$
  
  Declare GetAnimationNamesFromFile(File$, Array Result.s(1)) ; Get pose names from file$
  
  Declare ImportSkeletonFromFile(File$) ; returns a filled *o3d_Skeleton structure or null
  
  ; Count tags (main section only, yet, no sub-tags support, yet)
  Declare CountTagsInFile(File$, tag_id.w)
  
  ; misc.
  Declare o3d_Bload(File$)
  
  ; Skeleton construction
  Declare o3d_DeleteSkeleton(*This)
  Declare o3d_NewSkeleton()
  
;   Interface iSkeleton
;     Free()
;
;     GetAnimationLinkCount()
;     GetAnimationLinkNames(Array result$(1))
;     GetAnimationLinkScale.f(Name$)
;
;     GetBoneCount()
;     GetBoneByName(name$)
;     GetBoneByHandle(Handle)
;     GetBoneNames(Array result$(1))
;
;     GetAnimationCount()
;     GetAnimationNames(Array result$(1))
;     GetAnimationByName(name$)
;
;   EndInterface
;
  Structure o3d_any
    StructureUnion
      a.a
      b.b
      c.c
      d.d
      f.f
      i.i
      l.l
      *ptr.o3d_any
      q.q
      u.u
      w.w
      aa.a[0]
      bb.b[0]
      cc.c[0]
      dd.d[0]
      ff.f[0]
      ii.i[0]
      ll.l[0]
      ;  *ptr.o3d_any[0]
      *pptr[0]
      qq.q[0]
      uu.u[0]
      ww.w[0]
    EndStructureUnion
  EndStructure
  
  Structure o3d_TagHeader
    type_id.w
    size.l
  ; *name string size not count
  ; data.b[size] follows string/name
  EndStructure
  
  Structure o3d_Bone
    BoneId.u
    ParentBoneId.u
    pos.vector3
    ori.vector4
    scale.vector3 ; optional /default xyz = 1 1 1
    Name.s        ;
    List ChildNode.u()
  EndStructure
  
  Structure o3d_KeyFrame
    time.f
    ori.vector4
    translation.vector3
    scale.vector3 ; optional /default xyz 1 1 1
  EndStructure
  
  
  Structure o3d_AnimationTrack
    BoneId.u
    List KeyFrame.o3d_KeyFrame() ; key frames sorted by time
  EndStructure
  
  Structure o3d_Animation
    Name.s
    Length.f ; in seconds
    BaseAnimation.s ;
    BaseKeyFrame.f
    List AnimationTrack.o3d_AnimationTrack() ; "dyn" array must be sorted by BoneId
  EndStructure
  
  Structure o3d_AnimationLink
    SkeletonName.s
    Scale.f
  EndStructure
  
  Structure o3d_Skeleton
    *VT
    Name.s
    Filename.s
    Map Bone.o3d_Bone()
    Map Animation.o3d_Animation()
    Map AnimationLink.o3d_AnimationLink()
  EndStructure
  
  Structure o3d_Pose
    ; ???????
  EndStructure
  
  Structure o3d_SubMesh
    SubMeshName.s
    ; ????
  EndStructure
  
  Structure o3d_Mesh
    *VT
    InternalName.s
    Filename.s
    VertexCount.i
    VertexFormat.i
    Array Vertex.MeshVertex(1)
    SubMeshCount.i
    Array SubMesh.o3d_SubMesh(1)
    PoseCount.i
    Array Pose.o3d_Pose(1)
    ; ???
  EndStructure
  
  
EndDeclareModule


Module o3d
  
  EnableExplicit
  
  #ID_OGRE3D_STREAM   = $1000
  #SIGNATURE_SKELETON = "[Serializer_v1.10]"      ; most actual by ogre version 14.2
  #SIGNATURE_MESHFILE = "[MeshSerializer_v1.100]" ; 1.100 is most actual (1.80 is previous version)
  
  ;{ From: OgreSkeletonFileFormat.h  (includes directory)
  
  ;         SKELETON_HEADER            = 0x1000,
  #SKELETON_HEADER = $1000
;             // char* version           : Version number check
;             SKELETON_BLENDMODE         = 0x1010, // optional
  #SKELETON_BLENDMODE = $1010; // optional
;                 // unsigned short blendmode     : SkeletonAnimationBlendMode
;
;         SKELETON_BONE              = 0x2000,
  #SKELETON_BONE = $2000
;         // Repeating section defining each bone in the system.
;         // Bones are assigned indexes automatically based on their order of declaration
;         // starting with 0.
;
;             // char* name                       : name of the bone
;             // unsigned short handle            : handle of the bone, should be contiguous & start at 0
;             // Vector3 position                 : position of this bone relative to parent
;             // Quaternion orientation           : orientation of this bone relative to parent
;             // Vector3 scale                    : scale of this bone relative to parent
;
;         SKELETON_BONE_PARENT       = 0x3000,
  #SKELETON_BONE_PARENT = $3000
;         // Record of the parent of a single bone, used to build the node tree
;         // Repeating section, listed in Bone Index order, one per Bone
;
;             // unsigned short handle             : child bone
;             // unsigned short parentHandle   : parent bone
;
;         SKELETON_ANIMATION         = 0x4000,
  #SKELETON_ANIMATION = $4000
;         // A single animation for this skeleton
;
;             // char* name                       : Name of the animation
;             // float length                      : Length of the animation in seconds
;
;             SKELETON_ANIMATION_BASEINFO = 0x4010,
  #SKELETON_ANIMATION_BASEINFO = $4010
;             // [Optional] base keyframe information
;             // char* baseAnimationName (blank for self)
;             // float baseKeyFrameTime
;
;             SKELETON_ANIMATION_TRACK = 0x4100,
  #SKELETON_ANIMATION_TRACK = $4100
;             // A single animation track (relates to a single bone)
;             // Repeating section (within SKELETON_ANIMATION)
;
;                 // unsigned short boneIndex     : Index of bone to apply to
;
;                 SKELETON_ANIMATION_TRACK_KEYFRAME = 0x4110,
  #SKELETON_ANIMATION_TRACK_KEYFRAME = $4110
;                 // A single keyframe within the track
;                 // Repeating section
;
;                     // float time                    : The time position (seconds)
;                     // Quaternion rotate            : Rotation to apply at this keyframe
;                     // Vector3 translate            : Translation to apply at this keyframe
;                     // Vector3 scale                : Scale to apply at this keyframe
;         SKELETON_ANIMATION_LINK         = 0x5000
  #SKELETON_ANIMATION_LINK = $5000
;         // Link to another skeleton, to re-use its animations
;
;             // char* skeletonName                   : name of skeleton to get animations from
;             // float scale                          : scale to apply to trans/scale keys
;}
  
  
  Macro o3d_ClearMemfile() ; clean up
    *o3d_tag            = 0
    *o3d_tag_next       = 0
    o3d_tag_size        = 0
    *o3d_base           = 0
    *o3d_limit          = 0
    o3d_file_type       = 0
    *o3d_file_sig       = 0
    o3d_tag_id          = 0
    *o3d_tag_name_start = 0
    o3d_tag_name_length = 0
    *o3d_reader         = 0
  EndMacro
  
  Macro o3d_eof() ; *memory file eof
    Bool(*o3d_reader >= *o3d_limit Or *o3d_tag >= *o3d_limit)
  EndMacro
  
  Macro o3d_readdata(_MEM_, _BYTES_)
    CompilerIf Not Defined(*o3d_out, #PB_Variable)
      Protected *o3d_out.o3d_Any
      Protected *o3d_out_lim
    CompilerEndIf
    
    *o3d_out     = _MEM_
    *o3d_out_lim = _MEM_ + _BYTES_
    
    While *o3d_out + 8 <= *o3d_out_lim And *o3d_reader + 8 <= *o3d_limit
      *o3d_out\q = *o3d_reader\q
      *o3d_out + 8 : *o3d_reader + 8
    Wend
    
    If *o3d_out + 4 <= *o3d_out_lim And *o3d_reader + 4 <= *o3d_limit
      *o3d_out\l = *o3d_reader\l
      *o3d_out + 4 : *o3d_reader + 4
    EndIf
    
    If *o3d_out + 2 <= *o3d_out_lim And *o3d_reader + 2 <= *o3d_limit
      *o3d_out\w = *o3d_reader\w
      *o3d_out + 2 : *o3d_reader + 2
    EndIf
    
    If *o3d_out < *o3d_out_lim And *o3d_reader < *o3d_limit
      *o3d_out\b = *o3d_reader\b
      *o3d_out + 1 : *o3d_reader + 1
    EndIf
    
  EndMacro
  
  Macro o3d_readshort()
    (*o3d_reader\u) : *o3d_reader + 2
  EndMacro
  
  Macro o3d_readlong()
    (*o3d_reader\l) : *o3d_reader + 4
  EndMacro
  
  Macro o3d_readfloat()
    *o3d_reader\f : *o3d_reader + 4
  EndMacro
  
  Macro o3d_readstring(_RESULT_VAR_NAME_)
    *__ = *o3d_reader
    ___ = 0
    While *o3d_reader < *o3d_limit And *o3d_reader\a <> #LF
      *o3d_reader + 1
      ___ + 1
    Wend
    _RESULT_VAR_NAME_ = PeekS(*__, ___, #PB_Ascii)
    *o3d_reader + Bool(*o3d_reader < *o3d_limit)
  EndMacro
  
  Macro o3d_FileType() ; 1 = mesh file , 2 = skeleton file
    (o3d_file_type)
  EndMacro
  
  Macro o3d_InitMemFile(_MEMORY_, _SIZE_) ; Check memory for file signatures and init parser variables
    CompilerIf Not Defined(o3d_tag, #PB_Variable)
      Protected *o3d_tag.o3d_TagHeader
      Protected *o3d_tag_next
      Protected o3d_tag_size ; 32bit long
      Protected *o3d_tag_data
      Protected *o3d_base
      Protected *o3d_limit
      Protected o3d_file_type
      Protected *o3d_file_sig
      Protected o3d_tag_id
      Protected *o3d_tag_name_start
      Protected o3d_tag_name_length
      Protected *o3d_reader.o3d_any
      Protected *__.o3d_any, ___
    CompilerEndIf
    
    *o3d_reader   = _MEMORY_
    *o3d_base     = _MEMORY_
    *o3d_limit    = _MEMORY_ + _SIZE_
    *o3d_tag      = _MEMORY_
    *o3d_tag_next = 0
    o3d_tag_size  = 0
    o3d_file_type = *o3d_reader\w
    *o3d_reader   = _MEMORY_
    *o3d_file_sig = *o3d_reader + 2
    
    If o3d_file_type <> $1000
    ; bad file type
      o3d_ClearMemfile()
      DebuggerWarning(#PB_Compiler_Procedure + " - *memory is not an ogre3d file or stream")
    Else
      ;Debug "File signature: " + PeekS(*o3d_file_sig, -1, #PB_Ascii)
      Select PeekS(*o3d_file_sig, -1, #PB_Ascii)
        Case #SIGNATURE_MESHFILE + #LF$
          o3d_file_type = 1
        ;  Debug "this is an ogre3d mesh file"
        Case #SIGNATURE_SKELETON + #LF$
          o3d_file_type = 2
         ; Debug "this is an ogre3d skeleton file"
        Default
          DebuggerWarning(#PB_Compiler_Procedure + ": This is an unrecognized file signature!")
          o3d_file_type = 0
      EndSelect
    EndIf
  EndMacro
  
  Macro o3d_StringByteLength(_RESULT_, boolAdvance = #False) ; get string length from reader position ; optionally skip string, set reader cursor behind terminator
    *__ = *o3d_reader
    ___ = 1 ; pre-add null byte
    While *__\a <> #LF  ; while not null byte
      *__ + 1 ; next byte
      ___ + 1 ; increase length
    Wend
    _RESULT_ = ___
    *o3d_reader + (Bool(boolAdvance) * (1 + ___)) ; Move read cursor behind LF
  EndMacro
  
  Macro o3d_SkipString() ; read a string until LF + null (0A 00) terminator is encountered and put reader pointer behind it
    While *o3d_reader\a <> #LF And *o3d_reader < *o3d_limit
      *o3d_reader + 1
    Wend
    *o3d_reader + Bool(*o3d_reader < *o3d_limit) ; Skip LF
  EndMacro
  
  Macro o3d_PrepareNext() ; used by FirstTag() and NextTag(), *** also prepares the name string ***
    If o3d_isSqueezyTag(o3d_tag_id)
      *o3d_tag_name_start = *o3d_reader
      o3d_StringByteLength(o3d_tag_name_length, #True)
    Else
      *o3d_tag_name_start = 0
      o3d_tag_name_length = 0
    EndIf
    *o3d_tag_data = *o3d_reader
    *o3d_tag_next = *o3d_tag + o3d_tag_size + o3d_tag_name_length
    If o3d_IsNamed(o3d_tag_id)
      *o3d_tag_name_start = *o3d_tag + 6
      o3d_StringByteLength(o3d_tag_name_length, #True)
    EndIf
  EndMacro
  
  Macro o3d_FirstTag() ; start parsing, first tag after file signature
    *o3d_reader = 2 + *o3d_base
    o3d_SkipString()
    *o3d_tag     = *o3d_reader
    o3d_tag_id   = *o3d_tag\type_id
    o3d_tag_size = *o3d_tag\size
    *o3d_reader  = *o3d_tag + 6
    o3d_PrepareNext()
  EndMacro
  
  Macro o3d_Loc() ; unused yet
    (*o3d_reader - *o3d_base)
  EndMacro
  
  Macro o3d_Seek(offset, boolRelative = 0) ; unused yet
    CompilerIf boolRelative
      *o3d_reader + offset
    CompilerElse
      *o3d_reader = *base + offset
    CompilerEndIf
  EndMacro
  
  Macro o3d_NextTag() ; InitMemFile() FirstTag() must be called first
    *o3d_tag     = *o3d_tag_next
    o3d_tag_id   = *o3d_tag\type_id
    o3d_tag_size = *o3d_tag\size
    *o3d_reader  = *o3d_tag + 6
    o3d_PrepareNext()
  EndMacro
  
  Macro o3d_isSqueezyTag(_ID_) ;
    Bool((_ID_ = $2000 And o3d_file_type = 2))
  EndMacro
  
  Macro o3d_IsNamed(_ID_) ; incomplete tags that embed the name strings in their payload
    Bool((_ID_ = $4000 And o3d_file_type = 2) Or (_ID_ = $9000 And o3d_file_type = 1))
  EndMacro
  
  Macro o3d_TagId()
    (o3d_tag_id)
  EndMacro
  
  Macro o3d_TagSize()
    (o3d_tag_size)
  EndMacro
  
  Macro o3d_TagName(_RESULT_) ;
    If *o3d_tag_name_start
      _RESULT_ = PeekS(*o3d_tag_name_start, o3d_tag_name_length - 1, #PB_Ascii)
    Else
      _RESULT_ = #Empty$
    EndIf
  EndMacro
  
  Procedure o3d_CountTags(Tag_id.w, *Memory, Size, boolCheckContainers = 0) ; count the tags in an ogre file (incomplete needs containers support)
    Protected count, name$
    o3d_InitMemFile(*Memory, Size)
    If o3d_FileType()
      o3d_FirstTag()
      While Not o3d_eof()
       ; o3d_TagName(name$)
       ; Debug Hex(o3d_TagID()) + "  " + Str(o3d_TagSize()) + "  " + name$
        If o3d_TagID() = Tag_id
          count + 1
        EndIf
        o3d_NextTag()
      Wend
    EndIf
    ProcedureReturn count
  EndProcedure
  
  Macro o3d_CountBones(_memory_, _size_)
    o3d_CountTags($2000, _memory_, _size_)
  EndMacro
  
  
  Procedure o3d_Bload(File$) ; Load a file into new allocated memory and return the new memory pointer
    Protected fh = ReadFile( - 1, file$)
    Protected size
    Protected *result
    If fh
      size = Lof(fh)
      If size < 6
        *result = AllocateMemory(6) ; minimum tag size is 6 (int16=ID int32=tag length)
        If size
          ReadData(fh, *result, size)
        EndIf
      Else
        *result = AllocateMemory(size, #PB_Memory_NoClear)
        ReadData(fh, *result, size)
      EndIf
      CloseFile(fh)
    Else
      DebuggerWarning(#PB_Compiler_Procedure + ": Could not read file '" + file$ + "'")
    EndIf
    ProcedureReturn *result
  EndProcedure
  
  Procedure GetAnimationNamesFromMemory(*memory, size, Array result.s(1))
    
    Protected c, i
    o3d_InitMemFile(*memory, size)
    
    If o3d_FileType() = 2
      c = o3d_CountTags($4000, *memory, size)
      Dim result(c)
      o3d_FirstTag()
      
      While Not o3d_eof()
        If o3d_TagID() = $4000
          o3d_TagName(result(i))
          i + 1
        EndIf
        o3d_NextTag()
      Wend
    Else
      DebuggerWarning(#PB_Compiler_Procedure + ": The *memory data needs to be an Ogre3D .skeleton file.")
    EndIf
    ProcedureReturn c
  EndProcedure
  
  Procedure GetBoneNamesFromMemory(*memory, size, Array Result.s(1))
    Protected i, c
    Protected *W_ID.WORD, *S
    Protected *A.Ascii, len
    
    *W_ID = *memory ; skeleton file from memory
    
    If *W_ID\w & $FFFF <> #ID_OGRE3D_STREAM ; is serializer stream?
      DebuggerWarning(#PB_Compiler_Procedure + ": *Memory is not an Ogre3D mesh or skeleton file header.")
      ProcedureReturn 0
    EndIf
    
    If PeekS(*W_ID + 2, -1, #PB_Ascii) <> #SIGNATURE_SKELETON + #LF$
      DebuggerWarning(#PB_Compiler_Procedure + ": *Memory needs to be an Ogre3D .skeleton file, unsupported serializer version '" + PeekS(*W_ID + 2, -1, #PB_Ascii) + "'. ")
      ProcedureReturn 0
    EndIf
    
    ; Count bone names
    *W_ID = *memory + $15
    While *W_ID\w = $2000 ; is bone?
      *A = *W_ID + 6 ; set name start
      c + 1 ; increase bone count
      While *A\a <> #LF ; string ends with LF
        *A + 1
      Wend
      *W_ID = *A + 31 ; next tag, skip null byte + bone data
    Wend
    
    ; Dim result array
    Dim result(c)
    
    ; Set bone names in array
    *W_ID = *memory + $15
    While *W_ID\w = $2000
      *A  = *W_ID + 6 ; set name start
      len = 0  ; clear name length
      *S  = *A  ; save name start
      While *A\a <> #LF ; check for LF terminator
        len + 1 ; inc length
        *A + 1
      Wend
      result(i) = PeekS(*S, len, #PB_Ascii) ; set result
      
      ; *a+1 ; LF
      ; BoneNumber = *a\a : *A+1
      ; ParentBoneNumber = *a\a : *A+1
      ; flags (1) and/or floats (28 bytes)
      i + 1 ; next bone name
      *W_ID = *A + 31 ; next tag
    Wend
    
    ProcedureReturn c
  EndProcedure
  
  Procedure GetBoneNamesFromFile(File$, Array result.s(1))
    Protected *skel = o3d_Bload(file$)
    Protected c
    If *skel
      c = GetBoneNamesFromMemory(*skel, MemorySize(*skel), result())
      FreeMemory(*skel)
    EndIf
    ProcedureReturn c
  EndProcedure
  
  Procedure GetAnimationNamesFromFile(File$, Array result.s(1))
    Protected *skel = o3d_Bload(file$)
    Protected c
    If *skel
      c = GetAnimationNamesFromMemory(*skel, MemorySize(*skel), result())
      FreeMemory(*skel)
    EndIf
    ProcedureReturn c
  EndProcedure
  
  Procedure CountTagsInFile(File$, tag_id.w)
    Protected *mem = o3d_Bload(file$)
    Protected c
    If *mem
      c = o3d_CountTags(tag_id, *mem, MemorySize(*mem))
      FreeMemory(*mem)
    EndIf
    ProcedureReturn c
  EndProcedure
  
  
  Procedure o3d_DeleteSkeleton(*This.o3d_Skeleton)
    If *this
      ClearStructure(*This, o3d_Skeleton)
      FreeMemory(*This)
    EndIf
  EndProcedure
  
  Procedure o3d_NewSkeleton()
    Protected *Skeleton.o3d_Skeleton
    *Skeleton = AllocateMemory(SizeOf(o3d_Skeleton))
    InitializeStructure(*Skeleton, o3d_Skeleton)
    ProcedureReturn *Skeleton
  EndProcedure
  
  
  Procedure ImportSkeletonFromMemory(*Memory, size, filename$ = #Empty$)
    
   ; Debug #PB_Compiler_Procedure
    
    Protected *new.o3d_Skeleton
    o3d_InitMemFile(*Memory, Size)
    If o3d_FileType() <> 2
      DebuggerWarning("*Memory is not an Ogre3D .skeleton file")
      ProcedureReturn #Null
    EndIf
    *new          = o3d_NewSkeleton()
    *new\Filename = filename$
    
    Protected bone_count, blendmode, animation_count, animation_link_count, bone_parent_count, temp
    Protected temp_name$, child, parent, *sub_tag.o3d_TagHeader, *track_start, *track_limit, *keyframe.o3d_TagHeader, *keyframe_next
    Protected *Bone.o3d_Bone, *Anim.o3d_Animation, *Track.o3d_AnimationTrack, *K.o3d_KeyFrame, *Link.o3d_AnimationLink, error_count, track_count, keyframe_count, base_info_count
    
    Macro debugPos(_ident_, _str_, _ptr_ = *o3d_tag)
      Debug LSet("+", _ident_, "+") + " " + _str_ + "  @" + Hex(_ptr_ - *o3d_base) + "  (" + Str(_ptr_ - *o3d_base) + ")"
    EndMacro
    Macro debugPosTag(_ident_, _str_, _ptr_ = *o3d_tag, _T_ = *o3d_tag )
      Debug LSet("+", _ident_, "+") + " " + _str_ + "  @" + Hex(_ptr_ - *o3d_base) + "  (" + Str(_ptr_ - *o3d_base) + ")  tag_id: $" + Hex(_T_\type_id) + "  size: " + Str(_T_\size)
    EndMacro
    
    o3d_FirstTag()
    While Not o3d_eof()
      Select o3d_TagID()
          
        Case #SKELETON_BLENDMODE
          ; todo: read blending mode
       ;   debugPosTag(1, "blendmode")
          blendmode + 1
          
        Case #SKELETON_BONE
          ; read bone
          o3d_TagName(temp_name$)
          o3d_SkipString()
         ; debugPosTag(1, "bone " + temp_name$)
          *Bone              = *new\Bone(temp_name$)
          *Bone\Name         = temp_name$
          *Bone\BoneId       = o3d_readshort() ; it's a handle, and it's rarely perfectly sorted
          *Bone\ParentBoneId = -1
          *Bone\scale\x      = 1
          *Bone\scale\y      = 1
          *Bone\scale\z      = 1
          
          o3d_readdata(@*Bone\pos, o3d_TagSize() - 8) ; floats should appear in structure order :)
          
          
          bone_count + 1
          
        Case #SKELETON_BONE_PARENT
          ; read bone parent pair
         ; debugPosTag(1, "bone parent")
          bone_parent_count + 1
          child  = o3d_readshort()
          parent = o3d_readshort()
          ; set relationship
          
          ForEach *new\Bone()
            *Bone = *new\Bone()
            If *Bone\BoneId = child ; set parent id aka handle
              *Bone\ParentBoneId = parent
            ElseIf *Bone\BoneId = parent ; add child to list (there might be more than 1)
              AddElement( *Bone\ChildNode() )
              *Bone\ChildNode() = child
            EndIf
          Next
          
        Case #SKELETON_ANIMATION
          ; read animation
          *o3d_reader = *o3d_tag_data
          animation_count + 1
          o3d_readstring(temp_name$)
         ; debugPosTag(1, "animation " + temp_name$)
          ;o3d_SkipString()
          *Anim        = *new\Animation(temp_name$)
          *Anim\Name   = temp_name$
          *Anim\Length = o3d_readfloat()
         ; Debug "anim length: " + *Anim\Length
          
          While *o3d_reader < *o3d_tag_next
            *sub_tag = *o3d_reader
            *o3d_reader + 6
            Select *sub_tag\type_id
                
                
              Case #SKELETON_ANIMATION_BASEINFO
                ; read optional base info
                o3d_readstring(*Anim\BaseAnimation)
                *Anim\BaseKeyFrame = o3d_readfloat()
                debugPosTag(2, "base info", *sub_tag)
                base_info_count + 1
                
              Case #SKELETON_ANIMATION_TRACK
                ; read track
                track_count + 1
                *Track        = AddElement(*Anim\AnimationTrack())
                *Track\BoneId = o3d_readshort()
                
               ; debugPosTag(2, "animation track #" + Str(track_count) + " [" + Str(*Track\BoneId) + "] {" + *Anim\Name + "}", *sub_tag, *sub_tag)
                
                *track_start = *o3d_reader
                *track_limit = *sub_tag + *sub_tag\size
                
                *keyframe = *o3d_reader
                
                
                While *keyframe\type_id = #SKELETON_ANIMATION_TRACK_KEYFRAME
                  ; read all track key frames
                  keyframe_count + 1
                ;  debugPosTag(3, "add key frame #" + Str(keyframe_count) + " [" + Str(*Track\BoneId) + "] {" + *Anim\Name + "}", *keyframe, *keyframe)
                  *o3d_reader    = *keyframe + 6
                  *keyframe_next = *keyframe + *keyframe\size
                  
                  AddElement(*Track\KeyFrame())
                  *K = @*Track\KeyFrame()
                  
                  *K\scale\x = 1
                  *K\scale\y = 1
                  *K\scale\z = 1
                  
                  temp = *keyframe\size - 6
                  If temp > SizeOf(o3d_KeyFrame)
                    temp = SizeOf(o3d_KeyFrame)
                  EndIf
                  
                  o3d_readdata(*K, temp) ; should match PB structure layout
                  
                  *keyframe = *keyframe_next
                Wend
                
                
              Default
                error_count + 1
                Debug " ### Loc: " + Hex(o3d_Loc()) + "  (" + Str(o3d_Loc()) + ")"
                Debug " ### unknown type: " + Hex(*sub_tag\type_id & $FFFF)
                Break
            EndSelect
            *sub_tag = *sub_tag + *sub_tag\size
          Wend
          
          
        Case #SKELETON_ANIMATION_LINK
          
          ; untested yet, no fitting data yet
          animation_link_count + 1
          temp_name$ = ""
          o3d_readstring(temp_name$)
         ; debugPosTag(1, "animation link " + temp_name$)
          *Link              = *new\AnimationLink(temp_name$)
          *Link\SkeletonName = temp_name$
          *Link\Scale        = o3d_readfloat()
          
        Default
          
          Debug "## ERROR UNEXPECTED TAG ##"
          Debug "Loc: " + Hex(o3d_Loc()) + "  (" + Str(o3d_Loc()) + ")"
          Debug "Tag ID : $" + Hex(o3d_TagId())
          Debug "Size   : " + Str(o3d_TagSize())
          error_count + 1
          
      EndSelect
      o3d_NextTag()
      
    Wend
    
    
;     Debug "parse results: "
;     Debug " "
;     Debug "Error count = " + Str(error_count)
;     Debug " "
;     Debug "Blendmode count =" + Str(blendmode)
;     Debug "Bone count = " + Str(bone_count)
;     Debug "Bone-parent count = " + Str(bone_parent_count)
;     Debug "Animation count = " + Str(animation_count)
;     Debug "Animation link count = " + Str(animation_link_count)
;     Debug "Animation track count = " + Str(track_count)
;     Debug "Animation key frame count = " + Str(keyframe_count)
;     Debug "Animation base info count = " + Str(base_info_count)
;
;
    ProcedureReturn *new
  EndProcedure
  
  Procedure ImportSkeletonFromFile(File$)
    Protected *Skel = o3d_Bload(File$)
    Protected *result
    If *Skel
      *result = ImportSkeletonFromMemory(*Skel, MemorySize(*Skel), File$)
      FreeMemory(*skel)
    EndIf
    ProcedureReturn *result
  EndProcedure
  
  
EndModule


; --------------------------------------------------------------------------------------------------------------------------------------------------------
; --------------------------------------------------------------------------------------------------------------------------------------------------------
; --------------------------------------------------------------------------------------------------------------------------------------------------------
;
; TESTING
;
; ------- -------------------------------------------------------------------------------------------------------------------------------------------------
; --------------------------------------------------------------------------------------------------------------------------------------------------------
; --------------------------------------------------------------------------------------------------------------------------------------------------------
;


UseModule o3d


Procedure$ GetPos(*Float.float, amount = 3) ; Helper function to print float vectors
  Protected i, result$
  While i < amount
    result$ + " " + StrF(*Float\f)
    *Float + 4
    i + 1
  Wend
  ProcedureReturn LTrim(result$)
EndProcedure

; Extract bone & pose names (quick and dirty way)
; select skeleton file
Define file$ = OpenFileRequester("Select file", #PB_Compiler_Home + "examples" + #PS$ + "3D" + #PS$ + "data" + #PS$ + "Models" + #PS$ + "ninja.skeleton", "ogre3d files|*.mesh;*.skeleton|All|*.*", 0)
Define c, i, t0, t1, t2, t3, t4
Dim result$(0)

t0 = ElapsedMilliseconds()
; Poses
c  = GetAnimationNamesFromFile(file$, result$())
t1 = ElapsedMilliseconds() - t0
Debug "Pose names (" + Str(c) + ") in '" + GetFilePart(file$) + "'"
While i < c
  Debug RSet(Str(i), 6) + "   " + result$(i)
  i + 1
Wend

; Bones
t0 = ElapsedMilliseconds()
c  = GetBoneNamesFromFile(file$, result$())
t2 = ElapsedMilliseconds() - t0
Debug "Bone names (" + Str(c) + ") in '" + GetFilePart(file$) + "'"
i = 0
While i < c
  Debug RSet(Str(i), 6) + "   " + result$(i)
  i + 1
Wend

; --------------------------------
; Import the entire skeleton file
; --------------------------------

Debug "..... Skeleton import test ...."
t0                        = ElapsedMilliseconds()
Define *Skel.o3d_Skeleton = ImportSkeletonFromFile(file$)
t3                        = ElapsedMilliseconds() - t0
If *Skel
  Debug "*Skel!"
  
  t0 = ElapsedMilliseconds()
  Debug "Original Filename (not imported)= " + *Skel\Filename
  Debug "Name=" + *Skel\Name ; this is perhaps useless since it can't be imported or exported
  Debug "Bones=" + MapSize(*Skel\Bone())
  Debug "Animations=" + MapSize(*Skel\Animation()) ; The animations
  Debug "Links=" + MapSize(*Skel\AnimationLink()) ; links to inherited animations
  
  Debug LSet("[ Bones ]", 100, "_")
  
  Define ln$, *Bone.o3d_Bone, *Anim.o3d_Animation, *Track.o3d_AnimationTrack, *KeyFrame.o3d_KeyFrame
  
  ForEach *Skel\bone()
    *Bone = *Skel\Bone()
    Debug LSet("", 50, "-")
    Debug "Bone Name: " + *Bone\Name
    Debug "Bone Id/Handle: " + *Bone\BoneId
    Debug "Parent Bone Id: " + *Bone\ParentBoneId
    Debug "Orientation x y z w: " + GetPos(*Bone\ori)
    Debug "Position x y z: " + GetPos(*Bone\pos)
    Debug "Bone scale x y z: " + GetPos(*Bone\scale)
    ln$ = ""
    ForEach *Bone\ChildNode()
      ln$ + Str(*Bone\ChildNode()) + " "
    Next
    
    Debug "Child nodes (" + Str(ListSize(*Bone\ChildNode())) + "):  " + ln$
  Next
  
  
  Debug LSet("[ Animations ]", 100, "_")
  
  ForEach *Skel\Animation()
    *Anim = *Skel\Animation()
    Debug LSet("", 50, "-")
    Debug "Animation Name: " + *Anim\Name
    Debug "Animation Length: " + StrF(*Anim\Length)
    Debug "Base Animation: " + *Anim\BaseAnimation
    Debug "Base key frame: " + StrF(*Anim\BaseKeyFrame)
    Debug "Tracks: " + Str(ListSize(*Anim\AnimationTrack()))
    Debug " "
    Debug "Animation Track Overview:"
    Debug LSet("", 50, "-")
    ForEach *Anim\AnimationTrack()
      *Track = *Anim\AnimationTrack()
      Debug "Track BoneId: " + Str(*Track\BoneId) + "  key frames: " + Str(ListSize(*Track\KeyFrame()))
    Next
    Debug " "
    Debug "Animation Key Frames"
    Debug LSet("", 50, "-")
    ForEach *Anim\AnimationTrack()
      *Track = *Anim\AnimationTrack()
      Debug "{" + *Anim\Name + "} BoneId [" + Str(*Track\BoneId) + "] track"
      ForEach *Track\KeyFrame()
        *KeyFrame = *Track\KeyFrame()
        Debug "  " + Str(1 + ListIndex(*Track\KeyFrame())) + ". Keyframe time=" + StrF(*KeyFrame\time) + " ori=" + GetPos(*KeyFrame\ori, 4) + " translation=" + GetPos(*KeyFrame\translation) + " scale=" + GetPos(*KeyFrame\scale)
      Next
      Debug " "
    Next
  Next
  Debug " "
  Debug LSet("[ Animation Links ]", 100, "_")
  Debug " "
  ForEach *Skel\AnimationLink()
    Debug "SkeletonName=" + *Skel\AnimationLink()\SkeletonName + "  Scale=" + StrF(*Skel\AnimationLink()\Scale)
  Next
  If MapSize(*Skel\AnimationLink()) = 0
    Debug "  no animation links"
  EndIf
  Debug " "
  
  
  t4 = ElapsedMilliseconds() - t0
  
  Debug LSet("[ Chronometers & Statistics ]", 100, "_")
  
  Debug " "
  Debug "Skeleton content"
  Debug "Bone count: "+FormatNumber(MapSize(*Skel\Bone()),0)
  Debug "Animation count: "+FormatNumber(MapSize(*Skel\Animation()),0)
  
  Define trackcount, keyframecount
  ForEach *Skel\Animation()
    trackcount = trackcount + ListSize(*Skel\Animation()\AnimationTrack())
    ForEach *Skel\Animation()\AnimationTrack()
      keyframecount = keyframecount + ListSize(*Skel\Animation()\AnimationTrack()\KeyFrame())
    Next 
  Next 
  Debug "Track count: "+FormatNumber(trackcount,0)
  Debug "Keyframes total: "+FormatNumber(keyframecount,0)
  Debug "Animation links: "+FormatNumber(MapSize(*Skel\AnimationLink()),0)
  Debug " "
  Debug "Parsing chronometers"
  Debug "GetAnimationNamesFromFile() = " + FormatNumber(t1, 0) + " ms"
  Debug "GetBoneNamesFromFile() = " + FormatNumber(t2, 0) + " ms"
  Debug "ImportSkeletonFromFile() = " + FormatNumber(t3, 0) + " ms"
  Debug "Debug detailed info = " + FormatNumber(t4, 0) + " ms"
EndIf

Debug "~~~~~~~~ FINISHED ~~~~~~~~~~"
Last edited by benubi on Fri Feb 27, 2026 2:41 pm, edited 2 times in total.
User avatar
minimy
Addict
Addict
Posts: 890
Joined: Mon Jul 08, 2013 8:43 pm
Location: off world

Re: Work around: Get bone & pose names from file

Post by minimy »

Wow benubi, very nice work!!
This is really useful for the ogre users. Work with mesh direct is very hard, i did many trys, but the 'chunks' system is complex, to find every thing.
Very interesting all the information, i will try to analize the mesh file.

Thank you very much for share this 'gem' of code.
If translation=Error: reply="Sorry, Im Spanish": Endif
benubi
Enthusiast
Enthusiast
Posts: 251
Joined: Tue Mar 29, 2005 4:01 pm

Re: Work around: Get bone & pose names from file

Post by benubi »

Thanks you for the great feedback!

My aim now is to extract the animation key frames and the bone/vertex weights, but first I will finish the bone import. Here's how.

I just have made the discovery that the strings are not always terminated by 0, in fact it seems to only happen in the file signature part in the header. :oops: They are all terminated by LF.

The bone name is also terminated by LF but then it's followed by 1 byte that is the bone number and apparently not part of the payload size, too. My guess is that the payload consists of 2 bytes for flags/bools (bone inheritance flags) + 7 floats for default position + orientation, if I'm not 1 byte off course; one of the "flag" bytes could be the parent bone.

In the hex editor the byte following the bone #id often seems to point to the previous bone, which also follows the general logic of the skeleton construction (haven't compared it but it should fit). The following byte after those two bone id bytes must either be the inheritance flag or a first float.

For the other tags I'll probably have to look into the ogre sources for the various class serializers. I wonder how the AAA titles make it to use 1 skeleton for multiple models, I suspect the models share the same number of vertices and also preserve the bone/vertex weights. But what if the vertex weights are in the mesh file instead - this way the skeleton animations could work on any model?
miso
Enthusiast
Enthusiast
Posts: 708
Joined: Sat Oct 21, 2023 4:06 pm
Location: Hungary

Re: Work around: Get bone & pose names from file

Post by miso »

Great work. It will be useful if you scucceed with the plans. In AAA the skeleton is individual to a mesh I think, but the bones and names are the same, and therefore the same animation can be applied to them. Its good, as you dont copy the animations with your new entities, if you manually update the ones that are visible. (so I mean not using the animation commands, but manually set the positions rotations of the bones from an array of yours)
User avatar
Caronte3D
Addict
Addict
Posts: 1386
Joined: Fri Jan 22, 2016 5:33 pm
Location: Some Universe

Re: Work around: Get bone & pose names from file

Post by Caronte3D »

benubi wrote: Wed Feb 25, 2026 3:36 pm My aim now is to extract the animation key frames and the bone/vertex weights...
Wow! :shock:
This would be awesome :wink:
benubi
Enthusiast
Enthusiast
Posts: 251
Joined: Tue Mar 29, 2005 4:01 pm

Re: Work around: Get bone & pose names from file

Post by benubi »

Yes!

I downloaded the most recent ogre source codes. It looks mostly like I had it in mind. I will post an updated version in the coming days, because there's nothing functionally new to add yet - and you can ignore the comments and speculations about the format that I made :lol: . Skeleton files will be the easiest, Mesh is a little more messy as it has many more types of structures/chunks and also there are the different vertex formats (perhaps you remember the old days when the vertex data could be differently sized when setting/getting mesh data). The bone id's are in unsigned word format (up to 64k are possible then, not 256, not 128 as the duck AI hallucinated during my research, hehe, it was also the plastic duck that gave me idea of the byte sized Id).

The bone weights are stored with the boneid's inside the mesh files as I expected, I will get there after finishing the Skeleton part. Then the aim will be to import 100% of the mesh data into Purebasic native structures or find a simple way to make everything easily extractable without too many necessary commands. Other stuff I have to correct and clarify to myself, there will be simple name changes already ;)

Animation = Skeleton Animation (in Skeleton file)
Pose = Vertex Animation (in Mesh file)
All strings are in fact limited by LF only, without any null char exceptions, the "null" char after the signature was/is in fact the low byte of the first tag id. My great results are due a lot to luck, perhaps praying worked too, but most importantly trying - and not to forget the wx hex editor discovery.
User avatar
pf shadoko
Enthusiast
Enthusiast
Posts: 437
Joined: Thu Jul 09, 2015 9:07 am

Re: Work around: Get bone & pose names from file

Post by pf shadoko »

Ouch! The new functions
- MeshAnimationList
- MeshBoneList
which return the list of animations and bones in a comma-separated string are not in the documentation (nor in the examples).
User avatar
pf shadoko
Enthusiast
Enthusiast
Posts: 437
Joined: Thu Jul 09, 2015 9:07 am

Re: Work around: Get bone & pose names from file

Post by pf shadoko »

Ouch! The new functions
- MeshAnimationList
- MeshBoneList
which return the list of animations and bones in a comma-separated string are not in the documentation (nor in the examples).

Code: Select all

; ------------------------------------------------------------
;
;   PureBasic - MeshAnimationList
;
;    (c) Fantaisie Software
;
; ------------------------------------------------------------
;

InitEngine3D()
InitSprite()
InitKeyboard()
InitMouse()

ExamineDesktops():dx=DesktopWidth(0)*0.8:dy=DesktopHeight(0)*0.8
OpenWindow(0, 0,0, DesktopUnscaledX(dx),DesktopUnscaledY(dy), " MeshAnimationList - [Esc] quit",#PB_Window_ScreenCentered)
OpenWindowedScreen(WindowID(0), 0, 0, dx, dy, 0, 0, 0)
InitScreenGadgets()

Add3DArchive(#PB_Compiler_Home + "examples/3d/Data/Textures", #PB_3DArchive_FileSystem)
Add3DArchive(#PB_Compiler_Home + "examples/3d/Data/Models", #PB_3DArchive_FileSystem)
Parse3DScripts()

LoadMesh(0, "ninja.mesh")
CreateMaterial(0, LoadTexture(0, "nskinrd.jpg"))
CreateEntity(0, MeshID(0), MaterialID(0))

CreateCamera(0, 0, 0, 100, 100)
MoveCamera(0, 0, 40, 250, #PB_Absolute)
CameraLookAt(0,0,90,0)

CreateLight(0,$ffffff,1000,1000,0)

animlist.s= MeshAnimationList(0)
animcount=CountString(animlist,",")
Dim anim.s(animcount)

ComboBoxScreenGadget(0,20,20,200,32)
For i=0 To animcount
  anim(i)=StringField(animlist,i+1,",")
  AddScreenGadgetItem(0,-1, anim(i))
Next
SetScreenGadgetState(0,0)
StartEntityAnimation(0,GetScreenGadgetText(0))

Repeat
  While WindowEvent():Wend  
  ExamineKeyboard()
  ExamineMouse()
  
  If ScreenWindowEvent()=#PB_Event_Gadget
    StartEntityAnimation(0,anim(GetScreenGadgetState(0)))
  EndIf
  
  RotateEntity(0,0,1,0,#PB_Relative)
  
  RenderWorld()
  RenderScreenGadgets()
  FlipBuffers()
Until KeyboardPushed(#PB_Key_Escape) Or MouseButton(3)

Code: Select all

; ------------------------------------------------------------
;
;   PureBasic - EnableManualEntityBoneControl
;
;    (c) Fantaisie Software
;
; ------------------------------------------------------------
;

#CameraSpeed = 1

Define.f KeyX, KeyY, MouseX, MouseY, RollZ, sens = -1


Macro Text3D(No, Texte, Color, Alignment)
  CreateText3D(No, Texte)
  Text3DColor(No, Color)
  Text3DAlignment(No, Alignment)
EndMacro

InitEngine3D()
InitSprite()
InitKeyboard()
InitMouse()

ExamineDesktops():dx=DesktopWidth(0)*0.8:dy=DesktopHeight(0)*0.8
OpenWindow(0, 0,0, DesktopUnscaledX(dx),DesktopUnscaledY(dy), " EnableManualEntityBoneControl -  [F5]   [PageUp]   [PageDown]  [Esc] quit",#PB_Window_ScreenCentered)
OpenWindowedScreen(WindowID(0), 0, 0, dx, dy, 0, 0, 0)

Add3DArchive(#PB_Compiler_Home + "examples/3d/Data/Textures", #PB_3DArchive_FileSystem)
Add3DArchive(#PB_Compiler_Home + "examples/3d/Data/fonts", #PB_3DArchive_FileSystem)
Add3DArchive(#PB_Compiler_Home + "examples/3d/Data/Models", #PB_3DArchive_FileSystem)
Add3DArchive(#PB_Compiler_Home + "examples/3d/Data/Packs/skybox.zip", #PB_3DArchive_Zip)
Add3DArchive(#PB_Compiler_Home + "examples/3d/Data/Scripts", #PB_3DArchive_FileSystem)
Parse3DScripts()

LoadMesh(0, "robot.mesh")

CreateMaterial(0, LoadTexture(0, "r2skin.jpg"))
MaterialShadingMode(0, #PB_Material_Wireframe)

CreateEntity(0, MeshID(0), MaterialID(0))

bonelist.s= MeshBoneList(0)
bonecount=CountString(bonelist,",")+1
Dim Bone.s(bonecount)

For i=1 To bonecount
  Bone(i) = StringField(bonelist,i,",")
  EnableManualEntityBoneControl(0, Bone(i), #True, #True)
  Text3D(i, Str(i), RGBA(255, 0, 0, 255), #PB_Text3D_HorizontallyCentered | #PB_Text3D_VerticallyCentered)
  AttachEntityObject(0, Bone(i), Text3DID(i))
  ScaleText3D(i, 4, 4, 0)
Next

RotateEntity(0, 0, -70, 0)

SkyBox("stevecube.jpg")

CreateCamera(0, 0, 0, 100, 100)
MoveCamera(0, 0, 40, 150, #PB_Absolute)

Repeat
  While WindowEvent():Wend
  
  If ExamineMouse()
    MouseX = -MouseDeltaX() * #CameraSpeed * 0.05
    MouseY = -MouseDeltaY() * #CameraSpeed * 0.05
  EndIf
  
  If ExamineKeyboard()
    
    If KeyboardReleased(#PB_Key_F5) ;??
      EnableManualEntityBoneControl(0, Bone(0), #False, #True)
    EndIf
    
    roty = KeyboardPushed(#PB_Key_Right) - KeyboardPushed(#PB_Key_Left)
    RotateEntityBone(0, bone(10), 0, roty, 0, #PB_Relative)
    
    rotx = KeyboardPushed(#PB_Key_Up) - KeyboardPushed(#PB_Key_Down)
    RotateEntityBone(0, bone(14), 0, 0, rotx, #PB_Relative)
    RotateEntityBone(0, bone(17), 0, 0, rotx, #PB_Relative)
    
    depz = KeyboardPushed(#PB_Key_PageUp) - KeyboardPushed(#PB_Key_PageDown)
    MoveEntityBone(0, bone(7), depz, 0, 0, #PB_Relative)
  EndIf
  
  RenderWorld()
  FlipBuffers()
Until KeyboardPushed(#PB_Key_Escape) Or Quit = 1

benubi
Enthusiast
Enthusiast
Posts: 251
Joined: Tue Mar 29, 2005 4:01 pm

Re: Little Skeleton importer module (alpha-beta)

Post by benubi »

Thanks pf shadoko, I was successful, no I mean victorious, and wrote a full skeleton importer module. I posted an updated version of the code and changed the title of the thread (may happen again).
Post Reply