Could you do a writeup on how you did model animations? this is something i still have a problem with in my own game.
Could you do a writeup on how you did model animations? this is something i still have a problem with in my own game.
This is my game project - Top Down City:
http://www.pascalgamedevelopment.com...y-Topic-Reboot
My OpenAL audio wrapper with Intelligent Source Manager to use unlimited:
http://www.pascalgamedevelopment.com...source+manager
Unfortunately, I don't know an easy way.
My path was like this:
- Learn how skeletal animation works in general.
- Learn Blender, and learn how to import models and animations from Mixamo.
- Learn Python, and learn how to write Python scripts in Blender.
- Write a script that exports these model and animation to my own format.
- In the game itself, you need to write functions for loading this format, for converting to format understandable GPU ...
If you're a beginner, all of this can take over a year. I think my answer will not help you much.
I know the principles behind the skinning but the issue is loading up the data in a consistient and useful way in pascal and getting it to opengl & shaders (or a decent CPU method to deform mesh).
There is no useful example that works with modern file formats in pascal, there are ancient quake file format examples in glscene and some GLTF examples which are super hard to integrate because they rely on certain way their shaders are done and are hard to just integrate into a custom engine pipeline, and most of these don't even have support to share animations between different meshes, if you want multiple characters you usually need to export animations for each model separatedly.
If you could make a minimal example of this, all of pascal community could benefit from it.
This is my game project - Top Down City:
http://www.pascalgamedevelopment.com...y-Topic-Reboot
My OpenAL audio wrapper with Intelligent Source Manager to use unlimited:
http://www.pascalgamedevelopment.com...source+manager
The animation is too deeply integrated with the game engine. The code is written for internal useage only, it contains many strange and specific structures and names, it will not be understandable to the general public without detailed comments.
Right now It's too hard and not realistic to quickly make a simple animation example without the game engine itself. Sorry.
If anyone is interested, here is the Python code I use to export an animation model from Blender to my own binary format:
Code:import os import bpy import mathutils from struct import pack from mathutils import Matrix, Vector, Color from bpy_extras import io_utils, node_shader_utils print('*********************************************************') oo = bpy.context.scene.objects VV = [] TT = [] NN = [] WW = [] II = [] MM = [] GG = [] BB = [] KK = [] ML = [] MI = [] MB = [] MG = [] arm_name = 'Armature' icon_a = 8 icon_k = 0 if bpy.data.objects.find(arm_name) != -1: armature = bpy.data.objects[arm_name] BB = armature.data.bones[:] def bone_index( b ): for i in range(len(BB)): if b == BB[i]: return i return -1 def get_MG( i ): M = ML[i] @ MB[i] @ MI[i] if KK[i] < 0: return M else: return get_MG(KK[i]) @ M def root_path( _f_name = '' ): s = os.path.dirname(bpy.data.filepath) return s + '\\' + _f_name def name_compat(name): if name is None: return 'None' else: return name.replace(' ', '_') def mesh_triangulate(me): import bmesh bm = bmesh.new() bm.from_mesh(me) bmesh.ops.triangulate( bm , faces = bm.faces ) bm.to_mesh( me ) bm.free() def aa_add(aa,a): for i in range(len(aa)): if a == aa[i]: return i aa.append(a) return len(aa)-1 def get_mat_tex(m): mat_wrap = node_shader_utils.PrincipledBSDFWrapper(m) tex_wrap = getattr( mat_wrap , "base_color_texture" , None ) if tex_wrap: image = tex_wrap.image if image: d0 = os.path.dirname(bpy.data.filepath) d1 = os.path.normpath( d0 + image.filepath ) d2 = os.path.relpath( d1 , start = d0 ) return d2 return 'none' def get_w2(v): gg = v.groups[:] if len(gg) == 0: return ( 0 , 1.0 ) , ( 0 , 0.0 ) if len(gg) == 1: return ( gg[0].group , 1.0 ) , ( 0 , 0.0 ) if len(gg) > 2: gg.sort( key = lambda g: -g.weight ) w1 = gg[0].weight w2 = gg[1].weight ww = w1 + w2 return ( gg[0].group , w1/ww ) , ( gg[1].group , w2/ww ) def read_mesh(o): mesh_triangulate(o) o.calc_normals_split() vv_count = len(VV) LL = o.loops tt = o.uv_layers.active.data[:] def loop_to_ii(L): return LL[L].vertex_index + vv_count , aa_add( TT , tt[L].uv ) , aa_add( NN , LL[L].normal ) gg = [ [[],m,0] for m in o.materials ] for f in o.polygons: f_ii = tuple( loop_to_ii(L) for L in f.loop_indices ) gg[f.material_index][0].append( f_ii ) VV.extend( o.vertices[:] ) for g in gg: if len(g[0]) > 0: g[2] = aa_add( MM , g[1] ) GG.append(g) def mesh_to_aa(): for o in oo: if o.type == "MESH": read_mesh(o.to_mesh()) for i in range(len(MM)): tex_name = get_mat_tex(MM[i]) tex_index = aa_add( II , tex_name ) MM[i] = MM[i] , tex_index def write_arm( fw ): def write_m( M ): t = M.to_translation() e = M.to_euler() fw( pack('3f', M[0][3] , M[1][3] , M[2][3] ) ) fw( pack('3f', e[0] , e[1] , e[2] ) ) # icon pos fw( pack( '2B' , icon_a , icon_k ) ) # BB fw( pack( 'i' , len(BB) ) ) for b in BB: i = bone_index( b.parent ) KK.append(i) fw( pack('i', i ) ) # t_pose for i in range(len(BB)): b = BB[i] M = b.matrix_local.copy() ML.append( M.copy() ) MI.append( M.inverted().copy() ) MB.append( None ) write_m(M) # t_anim fw( pack( 'i' , len(bpy.data.actions)-1 ) ) for i in range( 1 , len(bpy.data.actions) ): a = bpy.data.actions[i] cc = a.fcurves a_len = len( cc[0].keyframe_points ) fw( pack('B',len(a.name)) ) fw( bytes( a.name , 'ansi' ) ) fw( pack( 'i' , a_len ) ) for j in range( a_len ): for k in range(len(BB)): b = BB[k] path_p = 'pose.bones["' + b.name + '"].location' path_q = 'pose.bones["' + b.name + '"].rotation_quaternion' pp = [] qq = [] for c in cc: if c.data_path == path_p: pp.append(c) if c.data_path == path_q: qq.append(c) v = [c.keyframe_points[j].co[1] for c in pp] p = mathutils.Matrix.Translation( ( v[0] , v[1] , v[2] ) ) v = [c.keyframe_points[j].co[1] for c in qq] q = mathutils.Quaternion( ( v[0] , v[1] , v[2] , v[3] ) ) MB[k] = p @ q.to_matrix().to_4x4() for k in range(len(BB)): M = get_MG(k) write_m(M) def save_arm( f_name ): f = open( f_name , 'bw' ) fw = f.write write_arm( fw ) f.close() def save_to_b2m( f_name , s_format ): mesh_to_aa() f = open(f_name,'bw') fw = f.write # format fw( pack( 'B' , s_format ) ) # images fw( pack( 'i' , len(II) ) ) for I in II: fw( pack('B',len(I)) ) fw( bytes( I , 'ansi' ) ) # mat fw( pack( 'i' , len(MM) ) ) for M in MM: fw( pack( 'i' , M[1] ) ) s = M[0].name fw( pack( 'B' , len(s) ) ) fw( bytes( s , 'ansi' ) ) # VV fw( pack( 'i' , len(VV) ) ) for v in VV: fw( pack('3f', v.co[0] , v.co[1] , v.co[2] ) ) # TT fw( pack( 'i' , len(TT) ) ) for t in TT: fw( pack('2f', t[0] , t[1] ) ) # NN fw( pack( 'i' , len(NN) ) ) for n in NN: fw( pack('3f', n[0] , n[1] , n[2] ) ) # GG fw( pack( 'i' , len(GG) ) ) for G in GG: fw( pack( 'i' , G[2] ) ) fw( pack( 'i' , len(G[0]) ) ) for g in G[0]: #fw( pack( '9i', g[0][0],g[0][1],g[0][2], g[1][0],g[1][1],g[1][2], g[2][0],g[2][1],g[2][2] ) ) fw( pack( '9H', g[0][0],g[0][1],g[0][2], g[1][0],g[1][1],g[1][2], g[2][0],g[2][1],g[2][2] ) ) # animation ... if s_format < 1: f.close() return # WW for v in VV: w = get_w2(v) fw( pack('<BfBf', w[0][0] , w[0][1] , w[1][0] , w[1][1] ) ) if s_format == 2: write_arm( fw ) f.close() save_to_b2m( root_path('m1.b2m') , 2 ) #save_arm( 'm1.arm' )
Last edited by rts111; 11-12-2021 at 03:44 PM.
Thanks, i will try to make sense of that, it looks like a simple exporter with clearly separated structures, t-pose is also exported so each model has its own t-pose (does this cause you issues if you want to share animations between multiple modems? )
This is my game project - Top Down City:
http://www.pascalgamedevelopment.com...y-Topic-Reboot
My OpenAL audio wrapper with Intelligent Source Manager to use unlimited:
http://www.pascalgamedevelopment.com...source+manager
Yes, I am using several models with the same armature, and only one animation collection for all models.
In fact, t-pose is not used in any way when imported into the game engine. This function in the script only serves to initialize the matrices. The values did not need to be written to the file.
In the engine, I get matrices directly from positions and rotations:
For animation, I send these matrices to a simple shader like this:Code:procedure mat4.AssignEulerTranslation( e,t :vec3 ); var cy,sy,cp,sp,cr,sr :float; q :vec4; begin LoadIdentity; cy := cos(e.z * 0.5); sy := sin(e.z * 0.5); cp := cos(e.y * 0.5); sp := sin(e.y * 0.5); cr := cos(e.x * 0.5); sr := sin(e.x * 0.5); q.w := cy * cp * cr + sy * sp * sr; q.x := cy * cp * sr - sy * sp * cr; q.y := sy * cp * sr + cy * sp * cr; q.z := sy * cp * cr - cy * sp * sr; with q do // m3 AssignQuaternion begin e00 := 1.0 - ( y * y + z * z ) * 2.0; e01 := ( x * y + w * z ) * 2.0; e02 := ( x * z - w * y ) * 2.0; e10 := ( x * y - w * z ) * 2.0; e11 := 1.0 - ( x * x + z * z ) * 2.0; e12 := ( y * z + w * x ) * 2.0; e20 := ( x * z + w * y ) * 2.0; e21 := ( y * z - w * x ) * 2.0; e22 := 1.0 - ( x * x + y * y ) * 2.0; end; e30 := t.x; e31 := t.y; e32 := t.z; end;
Code:uniform mat4 mm[32]; attribute vec3 vv; attribute vec3 nn; attribute vec2 tt; attribute ivec2 bb; attribute vec2 ww; varying vec2 tex_coord; varying vec3 normal; void main( void ) { ivec2 ibb = ivec2( bb ); vec4 v1 = vec4( vv , 1.0 ); vec4 v2 = ( mm[ibb[0]] * v1 ) * ww[0] + ( mm[ibb[1]] * v1 ) * ww[1]; vec4 n1 = vec4( nn , 0.0 ); vec4 n2 = ( mm[ibb[0]] * n1 ) * ww[0] + ( mm[ibb[1]] * n1 ) * ww[1]; normal = normalize( gl_NormalMatrix * n2.xyz ); tex_coord = tt; gl_Position = gl_ModelViewProjectionMatrix * v2; }
I tried the game. It is really awsome! Excellent work, from an design- but also from a programming stand point. The Age of Empires controls and sounds are great, too! - I wonder, as it is MMORTS, what happens if I log off and someone attacks. I will be an easy target in this case, right?
The source code is not available, right?
Best regards
Matthias
Bookmarks