text3d
authorhgn <hgodden00@gmail.com>
Tue, 11 Apr 2023 02:35:51 +0000 (03:35 +0100)
committerhgn <hgodden00@gmail.com>
Tue, 11 Apr 2023 02:35:51 +0000 (03:35 +0100)
16 files changed:
blender_export.py
build.c
common.h
entity.h
font.h [new file with mode: 0644]
maps_src/mp_gridmap.mdl
maps_src/mp_mtzero.mdl
menu.h
models_src/rs_font.mdl [new file with mode: 0644]
player.c
shaders/model_font.fs [new file with mode: 0644]
shaders/model_font.h [new file with mode: 0644]
shaders/model_font.vs [new file with mode: 0644]
skaterift.c
world_gate.h
world_routes.h

index cbd2a1989087302c7844d535e44cd73b5aaf1a3a..f9b225d43507f1880058dd252e5bb6fc9e551783 100644 (file)
@@ -3,6 +3,7 @@ import cProfile
 from ctypes import *
 from mathutils import *
 from gpu_extras.batch import batch_for_shader
+from bpy_extras import mesh_utils
 
 bl_info = {
    "name":"Skaterift .mdl exporter",
@@ -24,7 +25,8 @@ sr_entity_alias = {
    'ent_water': 5,
    'ent_volume': 6,
    'ent_audio': 7,
-   'ent_marker': 8
+   'ent_marker': 8,
+   'ent_glyph': 9
 }
 
 class mdl_vert(Structure):              # 48 bytes. Quite large. Could compress
@@ -171,7 +173,8 @@ class ent_gate(Structure):
                ("transport",(c_float*3)*4),
                ("_anonymous_union",version_refcount_union),
                ("timing_time",c_double),
-               ("routes",c_uint16*4)]
+               ("routes",c_uint16*4),
+               ("route_count",c_uint8)]
 #}
 
 class ent_route_node(Structure):
@@ -289,6 +292,29 @@ class ent_marker(Structure):
                ("name",c_uint32)]
 #}
 
+class ent_glyph(Structure):
+#{
+   _fields_ = [("size",c_float*2),
+               ("indice_start",c_uint32),
+               ("indice_count",c_uint32)]
+#}
+
+class ent_font_variant(Structure):
+#{
+   _fields_ = [("name",c_uint32),
+               ("material_id",c_uint32)]
+#}
+
+class ent_font(Structure):
+#{
+   _fields_ = [("alias",c_uint32),
+               ("variant_start",c_uint32),
+               ("variant_count",c_uint32),
+               ("glyph_start",c_uint32),
+               ("glyph_count",c_uint32),
+               ("glyph_utf32_base",c_uint32)]
+#}
+
 def obj_ent_type( obj ):
 #{
    if obj.type == 'ARMATURE': return 'mdl_armature'
@@ -688,8 +714,7 @@ def sr_compile_mesh( obj ):
       # Write the vertex / indice data
       #
       for tri_index, tri in enumerate(data.loop_triangles):#{
-         if tri.material_index != material_id:
-            continue
+         if tri.material_index != material_id: continue
 
          for j in range(3):#{
             vert = data.vertices[tri.vertices[j]]
@@ -852,6 +877,136 @@ def sr_compile_mesh( obj ):
    sr_compile.mesh_data.extend(bytearray(node))
 #}
 
+def sr_compile_fonts( collection ):
+#{
+   print( F"[SR] Compiling fonts" )
+
+   glyph_count = 0
+   variant_count = 0
+
+   for obj in collection.all_objects:#{
+      if obj_ent_type(obj) != 'ent_font': continue
+
+      data = obj.SR_data.ent_font[0]
+
+      font=ent_font()
+      font.alias = sr_compile_string( data.alias )
+      font.variant_start = variant_count
+      font.variant_count = 0
+      font.glyph_start = glyph_count
+
+      glyph_base = data.glyphs[0].utf32
+      glyph_range = data.glyphs[-1].utf32 - glyph_base
+
+      font.glyph_utf32_base = glyph_base
+      font.glyph_count = glyph_range
+
+      for i in range(len(data.variants)):#{
+         data_var = data.variants[i]
+         if not data_var.mesh: continue 
+
+         mesh = data_var.mesh.data
+
+         variant = ent_font_variant()
+         variant.name = sr_compile_string( data_var.tipo )
+
+         # fonts (variants) only support one material each
+         mat = None
+         if len(mesh.materials) != 0:
+            mat = mesh.materials[0]
+         variant.material_id = sr_compile_material( mat )
+
+         font.variant_count += 1
+
+         islands = mesh_utils.mesh_linked_triangles(mesh)
+         centroids = [Vector((0,0)) for _ in range(len(islands))]
+
+         for j in range(len(islands)):#{
+            for tri in islands[j]:#{
+               centroids[j].x += tri.center[0]
+               centroids[j].y += tri.center[2]
+            #}
+
+            centroids[j] /= len(islands[j])
+         #}
+
+         for j in range(glyph_range):#{
+            data_glyph = data.glyphs[j]
+            glyph = ent_glyph()
+            glyph.indice_start = len(sr_compile.indice_data)//sizeof(c_uint32)
+            glyph.indice_count = 0
+            glyph.size[0] = data_glyph.bounds[2]
+            glyph.size[1] = data_glyph.bounds[3]
+
+            vertex_reference = {}
+
+            for k in range(len(islands)):#{
+               if centroids[k].x < data_glyph.bounds[0] or \
+                  centroids[k].x > data_glyph.bounds[0]+data_glyph.bounds[2] or\
+                  centroids[k].y < data_glyph.bounds[1] or \
+                  centroids[k].y > data_glyph.bounds[1]+data_glyph.bounds[3]:
+               #{
+                  continue
+               #}
+
+               for l in range(len(islands[k])):#{
+                  tri = islands[k][l]
+                  for m in range(3):#{
+                     vert = mesh.vertices[tri.vertices[m]]
+                     li = tri.loops[m]
+                     vi = mesh.loops[li].vertex_index
+                     
+                     # Gather vertex information
+                     #
+                     co      = [vert.co[_] for _ in range(3)]
+                     co[0]  -= data_glyph.bounds[0]
+                     co[2]  -= data_glyph.bounds[1]
+                     norm    = mesh.loops[li].normal
+                     uv      = (0,0)
+                     if mesh.uv_layers: uv = mesh.uv_layers.active.data[li].uv
+
+                     TOLERENCE = float(10**4)
+                     key = (int(co[0]*TOLERENCE+0.5),
+                            int(co[1]*TOLERENCE+0.5),
+                            int(co[2]*TOLERENCE+0.5),
+                            int(norm[0]*TOLERENCE+0.5),
+                            int(norm[1]*TOLERENCE+0.5),
+                            int(norm[2]*TOLERENCE+0.5),
+                            int(uv[0]*TOLERENCE+0.5),
+                            int(uv[1]*TOLERENCE+0.5))
+
+                     if key in vertex_reference:
+                        index = vertex_reference[key]
+                     else:#{
+                        vindex = len(sr_compile.vertex_data)//sizeof(mdl_vert)
+                        index = bytearray(c_uint32(vindex))
+                        vertex_reference[key] = index
+                        v = mdl_vert()
+                        v.co[0]   =  co[0]
+                        v.co[1]   =  co[2]
+                        v.co[2]   = -co[1]
+                        v.norm[0] =  norm[0]
+                        v.norm[1] =  norm[2]
+                        v.norm[2] = -norm[1]
+                        v.uv[0]   =  uv[0]
+                        v.uv[1]   =  uv[1]
+                        
+                        sr_compile.vertex_data.extend(bytearray(v))
+                     #}
+                     
+                     glyph.indice_count += 1
+                     sr_compile.indice_data.extend( index )
+                  #}
+               #}
+            #}
+            sr_ent_push( glyph )
+         #}
+         sr_ent_push( variant )
+      #}
+      sr_ent_push( font )
+   #}
+#}
+
 def sr_compile_armature( obj ):
 #{
    node = mdl_armature()
@@ -1252,6 +1407,8 @@ def sr_compile( collection ):
       #}
    #}
 
+   sr_compile_fonts(collection)
+
    def _children( col ):#{
       yield col
       for c in col.children:#{
@@ -1609,6 +1766,10 @@ class SR_INTERFACE(bpy.types.Panel):
          active_object = context.active_object
          if not active_object: return
 
+         _.layout.operator( 'skaterift.copy_entity_data', \
+               text=F'Copy entity data to {len(context.selected_objects)-1} '+\
+                    F'other objects' )
+
          box = _.layout.box()
          row = box.row()
          row.alignment = 'CENTER'
@@ -1789,15 +1950,86 @@ class SR_UL_ROUTE_NODE_LIST(bpy.types.UIList):
    #}
 #}
 
+def internal_listdel_execute(self,context,ent_name,collection_name):
+#{
+   active_object = context.active_object
+   data = getattr(active_object.SR_data,ent_name)[0]
+   lista = getattr(data,collection_name)
+   index = getattr(data,F'{collection_name}_index')
+
+   lista.remove(index)
+
+   setattr(data,F'{collection_name}_index', min(max(0,index-1), len(lista)-1))
+   return{'FINISHED'}
+#}
+
+def internal_listadd_execute(self,context,ent_name,collection_name):
+#{
+   active_object = context.active_object
+   getattr(getattr(active_object.SR_data,ent_name)[0],collection_name).add()
+   return{'FINISHED'}
+#}
+
+def copy_propgroup( de, to ):
+#{
+   for a in de.__annotations__:#{
+      if isinstance(getattr(de,a), bpy.types.bpy_prop_collection):#{
+         ca = getattr(de,a)
+         cb = getattr(to,a)
+
+         while len(cb) != len(ca):#{
+            if len(cb) < len(ca): cb.add()
+            else: cb.remove(0)
+         #}
+         for i in range(len(ca)):#{
+            copy_propgroup(ca[i],cb[i])
+         #}
+      #}
+      else:#{
+         setattr(to,a,getattr(de,a))
+      #}
+   #}
+#}
+
+class SR_OT_COPY_ENTITY_DATA(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.copy_entity_data"
+   bl_label = "Copy entity data"
+   
+   def execute(self, context):#{
+      data = context.active_object.SR_data
+      new_type = data.ent_type
+      print( F"Copy entity data from: {context.active_object.name}" )
+
+      for obj in context.selected_objects:#{
+         if obj != context.active_object:#{
+            print( F"   To: {obj.name}" )
+
+            obj.SR_data.ent_type = new_type
+
+            if active_object.type == 'MESH':#{
+               col = getattr( obj.data.SR_data, new_type, None )
+               if col != None and len(col)==0: col.add()
+               mdata = context.active_object.data.SR_data
+               copy_propgroup( getattr(mdata,new_type)[0], col[0] )
+            #}
+            
+            col = getattr( obj.SR_data, new_type, None )
+            if col != None and len(col)==0: col.add()
+            copy_propgroup( getattr(data,new_type)[0], col[0] )
+         #}
+      #}
+      return{'FINISHED'}
+   #}
+#}
+
 class SR_OT_ROUTE_LIST_NEW_ITEM(bpy.types.Operator):
 #{
    bl_idname = "skaterift.new_entry"
    bl_label = "Add gate"
    
    def execute(self, context):#{
-      active_object = context.active_object
-      active_object.SR_data.ent_route[0].gates.add()
-      return{'FINISHED'}
+      return internal_listadd_execute(self,context,'ent_route','gates')
    #}
 #}
 
@@ -1816,13 +2048,7 @@ class SR_OT_ROUTE_LIST_DEL_ITEM(bpy.types.Operator):
    #}
    
    def execute(self, context):#{
-      active_object = context.active_object
-      lista = active_object.SR_data.ent_route[0].gates
-      index = active_object.SR_data.ent_route[0].gates_index
-      lista.remove(index) 
-      active_object.SR_data.ent_route[0].gates_index = \
-            min(max(0, index-1), len(lista) - 1)
-      return{'FINISHED'}
+      return internal_listdel_execute(self,context,'ent_route','gates')
    #}
 #}
 
@@ -1832,9 +2058,7 @@ class SR_OT_AUDIO_LIST_NEW_ITEM(bpy.types.Operator):
    bl_label = "Add file"
    
    def execute(self, context):#{
-      active_object = context.active_object
-      active_object.SR_data.ent_audio[0].files.add()
-      return{'FINISHED'}
+      return internal_listadd_execute(self,context,'ent_audio','files')
    #}
 #}
 
@@ -1853,16 +2077,115 @@ class SR_OT_AUDIO_LIST_DEL_ITEM(bpy.types.Operator):
    #}
    
    def execute(self, context):#{
+      return internal_listdel_execute(self,context,'ent_audio','files')
+      return{'FINISHED'}
+   #}
+#}
+
+class SR_OT_GLYPH_LIST_NEW_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.gl_new_entry"
+   bl_label = "Add glyph"
+   
+   def execute(self, context):#{
+      active_object = context.active_object
+
+      font = active_object.SR_data.ent_font[0]
+      font.glyphs.add()
+
+      if len(font.glyphs) > 1:#{
+         prev = font.glyphs[-2]
+         cur = font.glyphs[-1]
+
+         cur.bounds = prev.bounds
+         cur.utf32 = prev.utf32+1
+      #}
+
+      return{'FINISHED'}
+   #}
+#}
+
+class SR_OT_GLYPH_LIST_DEL_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.gl_del_entry"
+   bl_label = "Remove Glyph"
+
+   @classmethod 
+   def poll(cls, context):#{
+      active_object = context.active_object
+      if obj_ent_type(active_object) == 'ent_font':#{
+         return active_object.SR_data.ent_font[0].glyphs
+      #}
+      else: return False
+   #}
+   
+   def execute(self, context):#{
+      return internal_listdel_execute(self,context,'ent_font','glyphs')
+   #}
+#}
+
+class SR_OT_GLYPH_LIST_MOVE_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.gl_move_item"
+   bl_label = "aa"
+   direction: bpy.props.EnumProperty(items=(('UP', 'Up', ""),
+                                             ('DOWN', 'Down', ""),))
+   
+   @classmethod 
+   def poll(cls, context):#{
+      active_object = context.active_object
+      if obj_ent_type(active_object) == 'ent_font':#{
+         return active_object.SR_data.ent_font[0].glyphs
+      #}
+      else: return False
+   #}
+
+   def execute(_, context):#{
       active_object = context.active_object
-      lista = active_object.SR_data.ent_audio[0].files
-      index = active_object.SR_data.ent_audio[0].file_index
-      lista.remove(index) 
-      active_object.SR_data.ent_audio[0].file_index = \
-            min(max(0, index-1), len(lista) - 1)
+      data = active_object.SR_data.ent_font[0]
+
+      index = data.glyphs_index
+      neighbor = index + (-1 if _.direction == 'UP' else 1)
+      data.glyphs.move( neighbor, index )
+
+      list_length = len(data.glyphs) - 1
+      new_index = index + (-1 if _.direction == 'UP' else 1)
+
+      data.glyphs_index = max(0, min(new_index, list_length))
+
       return{'FINISHED'}
    #}
 #}
 
+class SR_OT_FONT_VARIANT_LIST_NEW_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.fv_new_entry"
+   bl_label = "Add variant"
+   
+   def execute(self, context):#{
+      return internal_listadd_execute(self,context,'ent_font','variants')
+   #}
+#}
+
+class SR_OT_FONT_VARIANT_LIST_DEL_ITEM(bpy.types.Operator):
+#{
+   bl_idname = "skaterift.fv_del_entry"
+   bl_label = "Remove variant"
+
+   @classmethod 
+   def poll(cls, context):#{
+      active_object = context.active_object
+      if obj_ent_type(active_object) == 'ent_font':#{
+         return active_object.SR_data.ent_font[0].variants
+      #}
+      else: return False
+   #}
+   
+   def execute(self, context):#{
+      return internal_listdel_execute(self,context,'ent_font','variants')
+   #}
+#}
+
 class SR_OBJECT_ENT_AUDIO_FILE_ENTRY(bpy.types.PropertyGroup):
 #{
    path: bpy.props.StringProperty( name="Path" )
@@ -1883,6 +2206,37 @@ class SR_UL_AUDIO_LIST(bpy.types.UIList):
    #}
 #}
 
+class SR_UL_FONT_VARIANT_LIST(bpy.types.UIList):
+#{
+   bl_idname = 'SR_UL_FONT_VARIANT_LIST'
+
+   def draw_item(_,context,layout,data,item,icon,active_data,active_propname):
+   #{
+      layout.prop( item, 'mesh', emboss=False )
+      layout.prop( item, 'tipo' )
+   #}
+#}
+
+class SR_UL_FONT_GLYPH_LIST(bpy.types.UIList):
+#{
+   bl_idname = 'SR_UL_FONT_GLYPH_LIST'
+
+   def draw_item(_,context,layout,data,item,icon,active_data,active_propname):
+   #{
+      s0 = layout.split(factor=0.3)
+      c = s0.column()
+      s1 = c.split(factor=0.3)
+      c = s1.column()
+      row = c.row()
+      lbl = chr(item.utf32) if item.utf32 >= 32 and item.utf32 <= 126 else 'ERR'
+      row.label(text=lbl)
+      c = s1.column()
+      c.prop( item, 'utf32', text='', emboss=True )
+      c = s0.column()
+      row = c.row()
+      row.prop( item, 'bounds', text='', emboss=False )
+   #}
+#}
 
 class SR_OBJECT_ENT_ROUTE(bpy.types.PropertyGroup):
 #{
@@ -1941,7 +2295,7 @@ class SR_OBJECT_ENT_VOLUME(bpy.types.PropertyGroup):
 class SR_OBJECT_ENT_AUDIO(bpy.types.PropertyGroup):
 #{
    files: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_AUDIO_FILE_ENTRY)
-   file_index: bpy.props.IntProperty()
+   files_index: bpy.props.IntProperty()
 
    flag_3d: bpy.props.BoolProperty( name="3D audio",default=True )
    flag_loop: bpy.props.BoolProperty( name="Loop",default=False )
@@ -2020,6 +2374,59 @@ class SR_OBJECT_ENT_MARKER(bpy.types.PropertyGroup):
    alias: bpy.props.StringProperty()
 #}
 
+class SR_OBJECT_ENT_GLYPH(bpy.types.PropertyGroup):
+#{
+   mini: bpy.props.FloatVectorProperty(size=2)
+   maxi: bpy.props.FloatVectorProperty(size=2)
+   utf32: bpy.props.IntProperty()
+#}
+
+class SR_OBJECT_ENT_GLYPH_ENTRY(bpy.types.PropertyGroup):
+#{
+   bounds: bpy.props.FloatVectorProperty(size=4,subtype='NONE')
+   utf32: bpy.props.IntProperty()
+#}
+
+class SR_OBJECT_ENT_FONT_VARIANT(bpy.types.PropertyGroup):
+#{
+   mesh: bpy.props.PointerProperty(type=bpy.types.Object)
+   tipo: bpy.props.StringProperty()
+#}
+
+class SR_OBJECT_ENT_FONT(bpy.types.PropertyGroup):
+#{
+   variants: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_FONT_VARIANT)
+   glyphs: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_GLYPH_ENTRY)
+   alias: bpy.props.StringProperty()
+
+   glyphs_index: bpy.props.IntProperty()
+   variants_index: bpy.props.IntProperty()
+
+   @staticmethod
+   def sr_inspector( layout, data ):
+   #{
+      layout.prop( data[0], 'alias' )
+
+      layout.label( text='Variants' )
+      layout.template_list('SR_UL_FONT_VARIANT_LIST', 'Variants', \
+                            data[0], 'variants', data[0], 'variants_index',\
+                            rows=5 )
+      row = layout.row()
+      row.operator( 'skaterift.fv_new_entry', text='Add' )
+      row.operator( 'skaterift.fv_del_entry', text='Remove' )
+
+      layout.label( text='ASCII Glyphs' )
+      layout.template_list('SR_UL_FONT_GLYPH_LIST', 'Glyphs', \
+                            data[0], 'glyphs', data[0], 'glyphs_index', rows=5)
+
+      row = layout.row()
+      row.operator( 'skaterift.gl_new_entry', text='Add' )
+      row.operator( 'skaterift.gl_del_entry', text='Remove' )
+      row.operator( 'skaterift.gl_move_item', text='^' ).direction='UP'
+      row.operator( 'skaterift.gl_move_item', text='v' ).direction='DOWN'
+   #}
+#}
+
 class SR_OBJECT_PROPERTIES(bpy.types.PropertyGroup):
 #{
    ent_gate: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_GATE)
@@ -2028,7 +2435,8 @@ class SR_OBJECT_PROPERTIES(bpy.types.PropertyGroup):
    ent_volume: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_VOLUME)
    ent_audio: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_AUDIO)
    ent_marker: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_MARKER)
-
+   ent_glyph: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_GLYPH)
+   ent_font: bpy.props.CollectionProperty(type=SR_OBJECT_ENT_FONT)
    ent_type: bpy.props.EnumProperty(
       name="Type",
       items=[('none', 'None', '', 0),
@@ -2039,7 +2447,9 @@ class SR_OBJECT_PROPERTIES(bpy.types.PropertyGroup):
              ('ent_water', 'Water Surface', '', 5),
              ('ent_volume', 'Volume', '', 6 ),
              ('ent_audio', 'Audio Files', '', 7),
-             ('ent_marker', 'Marker', '', 8)],
+             ('ent_marker', 'Marker', '', 8),
+             ('ent_font', 'Font', '', 9),
+             ('ent_font_variant','Font variant','',10)],
       update=sr_on_type_change
    )
 #}
@@ -2962,6 +3372,29 @@ def cv_draw():
             if obj.SR_data.ent_audio[0].flag_3d:
                cv_draw_sphere( obj.location, obj.scale[0], (1,1,0) )
          #}
+         elif ent_type == 'ent_font':#{
+            data = obj.SR_data.ent_font[0]
+
+            for i in range(len(data.variants)):#{
+               sub = data.variants[i].mesh
+               if not sub: continue
+
+               for ch in data.glyphs:#{
+                  mini = (ch.bounds[0],ch.bounds[1])
+                  maxi = (ch.bounds[2]+mini[0],ch.bounds[3]+mini[1])
+                  p0 = sub.matrix_world @ Vector((mini[0],0.0,mini[1]))
+                  p1 = sub.matrix_world @ Vector((maxi[0],0.0,mini[1]))
+                  p2 = sub.matrix_world @ Vector((maxi[0],0.0,maxi[1]))
+                  p3 = sub.matrix_world @ Vector((mini[0],0.0,maxi[1]))
+
+                  if i == data.variants_index: cc = (0.5,0.5,0.5)
+                  else: cc = (0,0,0)
+
+                  cv_view_verts += [p0,p1,p1,p2,p2,p3,p3,p0]
+                  cv_view_colours += [cc,cc,cc,cc,cc,cc,cc,cc]
+               #}
+            #}
+         #}
       #}
    #}
 
@@ -2983,11 +3416,19 @@ classes = [ SR_INTERFACE, SR_MATERIAL_PANEL,\
             SR_OBJECT_ENT_GATE, SR_MESH_ENT_GATE, SR_OBJECT_ENT_SPAWN, \
             SR_OBJECT_ENT_ROUTE_ENTRY, SR_UL_ROUTE_NODE_LIST, \
             SR_OBJECT_ENT_ROUTE, SR_OT_ROUTE_LIST_NEW_ITEM,\
+            SR_OT_GLYPH_LIST_NEW_ITEM, SR_OT_GLYPH_LIST_DEL_ITEM,\
+            SR_OT_GLYPH_LIST_MOVE_ITEM,\
             SR_OT_AUDIO_LIST_NEW_ITEM,SR_OT_AUDIO_LIST_DEL_ITEM,\
-            SR_OBJECT_ENT_VOLUME, 
+            SR_OT_FONT_VARIANT_LIST_NEW_ITEM,SR_OT_FONT_VARIANT_LIST_DEL_ITEM,\
+            SR_OT_COPY_ENTITY_DATA, \
+            SR_OBJECT_ENT_VOLUME, \
             SR_UL_AUDIO_LIST, SR_OBJECT_ENT_AUDIO_FILE_ENTRY,\
             SR_OT_ROUTE_LIST_DEL_ITEM,\
-            SR_OBJECT_ENT_AUDIO,SR_OBJECT_ENT_MARKER,\
+            SR_OBJECT_ENT_AUDIO,SR_OBJECT_ENT_MARKER,SR_OBJECT_ENT_GLYPH,\
+            SR_OBJECT_ENT_FONT_VARIANT,
+            SR_OBJECT_ENT_GLYPH_ENTRY,\
+            SR_UL_FONT_VARIANT_LIST,SR_UL_FONT_GLYPH_LIST,\
+            SR_OBJECT_ENT_FONT,\
             \
             SR_OBJECT_PROPERTIES, SR_LIGHT_PROPERTIES, SR_BONE_PROPERTIES, 
             SR_MESH_PROPERTIES, SR_MATERIAL_PROPERTIES \
diff --git a/build.c b/build.c
index f791927809d36e7a26e2fae30e79686f3739db20..73ba1b7a2c2952be5c6302b9227f87c207afd2f3 100644 (file)
--- a/build.c
+++ b/build.c
@@ -133,6 +133,7 @@ void build_shaders(void)
    _S( "model_menu",           "model.vs",         "model_menu.fs" );
    _S( "model_character_view", "model_skinned.vs", "model_character_view.fs" );
    _S( "model_gate",           "model_gate.vs",    "model_gate_lq.fs" );
+   _S( "model_font",           "model_font.vs",    "model_font.fs" );
 
    /* 2D */
    _S( "blit",      "blit.vs",      "blit.fs" );
index 9eda6f67ae0e59dfcf76c8fd39b0f26d46d7d8ae..0aaab5dc6e5a387e103dbcd785a58531f1363dfa 100644 (file)
--- a/common.h
+++ b/common.h
@@ -17,6 +17,9 @@
 #define RESET_MAX_TIME 45.0
 VG_STATIC v3f TEMP_BOARD_0, TEMP_BOARD_1;
 
+#include "font.h"
+VG_STATIC font3d test_font;
+
 enum menu_controller_type
 {
    k_menu_controller_type_keyboard,
index e437d3d77810f059bfc8dd22f5a488ed464df4a9..143ad0da8428d3e49d9525ef2a30bcf8db613ae7 100644 (file)
--- a/entity.h
+++ b/entity.h
@@ -18,6 +18,9 @@ typedef struct ent_volume ent_volume;
 typedef struct ent_audio ent_audio;
 typedef struct ent_index ent_index;
 typedef struct ent_marker ent_marker;
+typedef struct ent_font ent_font;
+typedef struct ent_font_variant ent_font_variant;
+typedef struct ent_glyph ent_glyph;
 
 enum entity_alias{
    k_ent_gate = 1,
@@ -87,12 +90,13 @@ struct ent_gate{
       u32 timing_version;
 
       struct{
-         u8 ref_count, ref_total;
+         u8 ref_count;
       };
    };
 
    double timing_time;
    u16 routes[4];       /* routes that pass through this gate */
+   u8 route_count;
 };
 
 struct ent_route_node{
@@ -124,11 +128,13 @@ struct ent_route{
    v4f colour;
 
    /* runtime */
-   u32 active_checkpoint;
+   u16 active_checkpoint, 
+       valid_checkpoints;
+
    float factive;
    m4x3f board_transform;
    mdl_submesh sm;
-   double latest_pass;
+   double timing_base;
 };
 
 struct ent_water{
@@ -189,6 +195,20 @@ struct ent_marker{
    u32 pstr_alias;
 };
 
+VG_STATIC ent_marker *ent_find_marker( mdl_context *mdl,
+                                       mdl_array_ptr *arr, const char *alias )
+{
+   for( u32 i=0; i<mdl_arrcount(arr); i++ ){
+      ent_marker *marker = mdl_arritm( arr, i );
+
+      if( !strcmp( mdl_pstr( mdl, marker->pstr_alias ), alias ) ){
+         return marker;
+      }
+   }
+
+   return NULL;
+}
+
 enum channel_behaviour{
    k_channel_behaviour_unlimited = 0,
    k_channel_behaviour_discard_if_full = 1,
@@ -201,18 +221,24 @@ enum probability_curve{
    k_probability_curve_wildlife_night = 2
 };
 
-VG_STATIC ent_marker *ent_find_marker( mdl_context *mdl,
-                                       mdl_array_ptr *arr, const char *alias )
-{
-   for( u32 i=0; i<mdl_arrcount(arr); i++ ){
-      ent_marker *marker = mdl_arritm( arr, i );
+struct ent_font{
+   u32 alias,
+       variant_start,
+       variant_count,
+       glyph_start,
+       glyph_count,
+       glyph_utf32_base;
+};
 
-      if( !strcmp( mdl_pstr( mdl, marker->pstr_alias ), alias ) ){
-         return marker;
-      }
-   }
+struct ent_font_variant{
+   u32 name,
+       material_id;
+};
 
-   return NULL;
-}
+struct ent_glyph{
+   v2f size;
+   u32 indice_start,
+       indice_count;
+};
 
 #endif /* ENTITY_H */
diff --git a/font.h b/font.h
new file mode 100644 (file)
index 0000000..4c7d8f4
--- /dev/null
+++ b/font.h
@@ -0,0 +1,146 @@
+#ifndef FONT_H
+#define FONT_H
+
+#include "model.h"
+#include "entity.h"
+#include "camera.h"
+#include "shaders/model_font.h"
+
+typedef struct font3d font3d;
+struct font3d{
+   mdl_context mdl;
+   GLuint texture;
+   glmesh mesh;
+
+   ent_font info;
+   mdl_array_ptr font_variants,
+                 glyphs;
+};
+
+VG_STATIC void font3d_load( font3d *font, const char *mdl_path, void *alloc )
+{
+   mdl_open( &font->mdl, mdl_path, alloc );
+   mdl_load_metadata_block( &font->mdl, alloc );
+
+   vg_linear_clear( vg_mem.scratch );
+   mdl_array_ptr fonts;
+   mdl_load_array( &font->mdl, &fonts, "ent_font", vg_mem.scratch );
+   font->info = *((ent_font *)mdl_arritm(&fonts,0));
+
+   mdl_load_array( &font->mdl, &font->font_variants, "ent_font_variant", alloc);
+   mdl_load_array( &font->mdl, &font->glyphs, "ent_glyph", alloc );
+
+   vg_linear_clear( vg_mem.scratch );
+   mdl_load_mesh_block( &font->mdl, vg_mem.scratch );
+   mdl_load_pack_block( &font->mdl, vg_mem.scratch );
+   mdl_close( &font->mdl );
+
+   vg_acquire_thread_sync();
+   {
+      /* upload mesh */
+      mesh_upload( &font->mesh, 
+                     font->mdl.verts.data, font->mdl.verts.count,
+                     font->mdl.indices.data, font->mdl.indices.count );
+
+      /* upload first texture */
+      font->texture = vg_tex2d_new();
+      mdl_texture *tex0 = mdl_arritm( &font->mdl.textures, 0 );
+
+      vg_tex2d_set_error();
+      vg_tex2d_qoi( mdl_arritm( &font->mdl.pack, tex0->file.pack_offset ),
+                    tex0->file.pack_size,
+                    mdl_pstr( &font->mdl, tex0->file.pstr_path ));
+      vg_tex2d_nearest();
+      vg_tex2d_repeat();
+   }
+   vg_release_thread_sync();
+}
+
+VG_STATIC void font3d_init(void)
+{
+   shader_model_font_register();
+}
+
+VG_STATIC u32 font3d_find_variant( font3d *font, const char *name )
+{
+   for( u32 i=0; i<mdl_arrcount( &font->font_variants ); i ++ ){
+      ent_font_variant *variant = mdl_arritm( &font->font_variants, i );
+
+      if( !strcmp( mdl_pstr( &font->mdl, variant->name ), name ) ){
+         return i;
+      }
+   }
+
+   return 0;
+}
+
+VG_STATIC void font3d_bind( font3d *font, camera *cam )
+{
+   shader_model_font_use();
+   shader_model_font_uColour( (v4f){1.0f,1.0f,1.0f,1.0f} );
+   shader_model_font_uTexMain( 1 );
+       glActiveTexture( GL_TEXTURE1 );
+       glBindTexture( GL_TEXTURE_2D, font->texture );
+
+   shader_model_font_uPv( cam->mtx.pv );
+   mesh_bind( &font->mesh );
+}
+
+VG_STATIC 
+void font3d_simple_draw( font3d *font, u32 variant_id, const char *text, 
+                         camera *cam, m4x3f transform )
+{
+   v3f offset;
+   v3_zero( offset );
+
+   m4x4f prev_mtx;
+
+   m4x3_expand( transform, prev_mtx );
+   m4x4_mul( cam->mtx_prev.pv, prev_mtx, prev_mtx );
+
+   shader_model_font_uPvmPrev( prev_mtx );
+   shader_model_font_uMdl( transform );
+
+   for( int i=0;; i++ ){
+      u32 c = text[i];
+      if(!c) break;
+
+      if( c < font->info.glyph_utf32_base ) continue;
+      if( c >= font->info.glyph_utf32_base+font->info.glyph_count) continue;
+
+      u32 index = c - font->info.glyph_utf32_base;
+          index += font->info.glyph_start;
+          index += font->info.glyph_count * variant_id;
+      ent_glyph *glyph = mdl_arritm( &font->glyphs, index );
+
+      if( glyph->indice_count ){
+         shader_model_font_uOffset( offset );
+         mesh_drawn( glyph->indice_start, glyph->indice_count );
+      }
+      offset[0] += glyph->size[0];
+   }
+}
+
+VG_STATIC 
+float font3d_string_width( font3d *font, u32 variant_id, const char *text )
+{
+   float width = 0.0f;
+   for( int i=0;; i++ ){
+      u32 c = text[i];
+      if(!c) break;
+
+      if( c < font->info.glyph_utf32_base ) continue;
+      if( c >= font->info.glyph_utf32_base+font->info.glyph_count) continue;
+
+      u32 index = c - font->info.glyph_utf32_base;
+          index += font->info.glyph_start;
+          index += font->info.glyph_count * variant_id;
+      ent_glyph *glyph = mdl_arritm( &font->glyphs, index );
+
+      width += glyph->size[0];
+   }
+
+   return width;
+}
+
+#endif /* FONT_H */
index a1c0ef40200ff32cea2981e2149de3df5ae65a08..484e1650d812c30dfb120a6c3ee4ec9d0ed19475 100644 (file)
Binary files a/maps_src/mp_gridmap.mdl and b/maps_src/mp_gridmap.mdl differ
index 1b58c565fc093de3d7a2f98b439b9f39910f0f54..29b4aff015d6603d6c9778832e41672e23411ad5 100644 (file)
Binary files a/maps_src/mp_mtzero.mdl and b/maps_src/mp_mtzero.mdl differ
diff --git a/menu.h b/menu.h
index 4318e628141912a31a10dbc11b06567eb9f2a20f..7a008468802d1775848fdf358c48ec8002db9a41 100644 (file)
--- a/menu.h
+++ b/menu.h
@@ -826,7 +826,7 @@ VG_STATIC void menu_render_fg( camera *cam )
       mdl_transform_m4x3( &btn->mesh->transform, mtx );
       m4x3_mul( menu_mdl_mtx, mtx, mtx );
       m4x3_identity( mtx_size );
-      m4x3_scale( mtx_size, expSustainedImpulse( btn->fsize, 0.5f, 8.7f) );
+      m3x3_scalef( mtx_size, expSustainedImpulse( btn->fsize, 0.5f, 8.7f) );
       m4x3_mul( mtx, mtx_size, mtx );
       shader_model_menu_uMdl( mtx );
 
diff --git a/models_src/rs_font.mdl b/models_src/rs_font.mdl
new file mode 100644 (file)
index 0000000..1c84117
Binary files /dev/null and b/models_src/rs_font.mdl differ
index 8d6429f097d0ff0d937dd0315ea61ab034638fbb..ea29b4cecd5ea68b675634dd1ae847896210c3a1 100644 (file)
--- a/player.c
+++ b/player.c
@@ -377,7 +377,7 @@ VG_STATIC void player__pre_render( player_instance *player )
          player->rewind_total_length = 0.0f;
          player->rewind_accum = 0.0f;
          world_global.sky_target_rate = 1.0;
-         world_global.last_use = world_global.time;
+         world_global.time = world_global.last_use;
       }
       else{
          world_global.sky_target_rate = -100.0;
diff --git a/shaders/model_font.fs b/shaders/model_font.fs
new file mode 100644 (file)
index 0000000..673238c
--- /dev/null
@@ -0,0 +1,18 @@
+out vec4 FragColor;
+
+uniform sampler2D uTexMain;
+uniform vec4 uColour;
+
+in vec4 aColour;
+in vec2 aUv;
+in vec3 aNorm;
+in vec3 aCo;
+
+#include "motion_vectors_fs.glsl"
+
+void main()
+{
+   compute_motion_vectors();
+   vec4 diffuse = texture( uTexMain, aUv );
+   FragColor = vec4( diffuse.rgb, 1.0 ) * uColour;
+}
diff --git a/shaders/model_font.h b/shaders/model_font.h
new file mode 100644 (file)
index 0000000..7de7971
--- /dev/null
@@ -0,0 +1,147 @@
+#ifndef SHADER_model_font_H
+#define SHADER_model_font_H
+static void shader_model_font_link(void);
+static void shader_model_font_register(void);
+static struct vg_shader _shader_model_font = {
+   .name = "model_font",
+   .link = shader_model_font_link,
+   .vs = 
+{
+.orig_file = "shaders/model_font.vs",
+.static_src = 
+"layout (location=0) in vec3 a_co;\n"
+"layout (location=1) in vec3 a_norm;\n"
+"layout (location=2) in vec2 a_uv;\n"
+"layout (location=3) in vec4 a_colour;\n"
+"layout (location=4) in vec4 a_weights;\n"
+"layout (location=5) in ivec4 a_groups;\n"
+"\n"
+"#line       1        1 \n"
+"const float k_motion_lerp_amount = 0.01;\n"
+"\n"
+"#line      2        0 \n"
+"\n"
+"out vec3 aMotionVec0;\n"
+"out vec3 aMotionVec1;\n"
+"\n"
+"void vs_motion_out( vec4 vproj0, vec4 vproj1 )\n"
+"{\n"
+"   // This magically solves some artifacting errors!\n"
+"   //\n"
+"   vproj1 = vproj0*(1.0-k_motion_lerp_amount) + vproj1*k_motion_lerp_amount;\n"
+"\n"
+"   aMotionVec0 = vec3( vproj0.xy, vproj0.w );\n"
+"   aMotionVec1 = vec3( vproj1.xy, vproj1.w );\n"
+"}\n"
+"\n"
+"#line      9        0 \n"
+"\n"
+"uniform mat4x3 uMdl;\n"
+"uniform mat4 uPv;\n"
+"uniform mat4 uPvmPrev;\n"
+"uniform vec3 uOffset;\n"
+"\n"
+"out vec4 aColour;\n"
+"out vec2 aUv;\n"
+"out vec3 aNorm;\n"
+"out vec3 aCo;\n"
+"out vec3 aWorldCo;\n"
+"\n"
+"void main()\n"
+"{\n"
+"   vec3 co = a_co+uOffset;\n"
+"   vec3 world_pos0 = uMdl     * vec4( co, 1.0 );\n"
+"   vec4 vproj0     = uPv      * vec4( world_pos0, 1.0 );\n"
+"   vec4 vproj1     = uPvmPrev * vec4( co, 1.0 );\n"
+"\n"
+"   vs_motion_out( vproj0, vproj1 );\n"
+"\n"
+"   gl_Position = vproj0;\n"
+"   aWorldCo = world_pos0;\n"
+"   aColour = a_colour;\n"
+"   aUv = a_uv;\n"
+"   aNorm = mat3(uMdl) * a_norm;\n"
+"   aCo = a_co;\n"
+"}\n"
+""},
+   .fs = 
+{
+.orig_file = "shaders/model_font.fs",
+.static_src = 
+"out vec4 FragColor;\n"
+"\n"
+"uniform sampler2D uTexMain;\n"
+"uniform vec4 uColour;\n"
+"\n"
+"in vec4 aColour;\n"
+"in vec2 aUv;\n"
+"in vec3 aNorm;\n"
+"in vec3 aCo;\n"
+"\n"
+"#line       1        1 \n"
+"const float k_motion_lerp_amount = 0.01;\n"
+"\n"
+"#line      2        0 \n"
+"\n"
+"layout (location = 1) out vec2 oMotionVec;\n"
+"\n"
+"in vec3 aMotionVec0;\n"
+"in vec3 aMotionVec1;\n"
+"\n"
+"void compute_motion_vectors()\n"
+"{\n"
+"   // Write motion vectors\n"
+"   vec2 vmotion0 = aMotionVec0.xy / aMotionVec0.z;\n"
+"   vec2 vmotion1 = aMotionVec1.xy / aMotionVec1.z;\n"
+"\n"
+"   oMotionVec = (vmotion1-vmotion0) * (1.0/k_motion_lerp_amount);\n"
+"}\n"
+"\n"
+"#line     12        0 \n"
+"\n"
+"void main()\n"
+"{\n"
+"   compute_motion_vectors();\n"
+"   vec4 diffuse = texture( uTexMain, aUv );\n"
+"   FragColor = vec4( diffuse.rgb, 1.0 ) * uColour;\n"
+"}\n"
+""},
+};
+
+static GLuint _uniform_model_font_uMdl;
+static GLuint _uniform_model_font_uPv;
+static GLuint _uniform_model_font_uPvmPrev;
+static GLuint _uniform_model_font_uOffset;
+static GLuint _uniform_model_font_uTexMain;
+static GLuint _uniform_model_font_uColour;
+static void shader_model_font_uMdl(m4x3f m){
+   glUniformMatrix4x3fv(_uniform_model_font_uMdl,1,GL_FALSE,(float*)m);
+}
+static void shader_model_font_uPv(m4x4f m){
+   glUniformMatrix4fv(_uniform_model_font_uPv,1,GL_FALSE,(float*)m);
+}
+static void shader_model_font_uPvmPrev(m4x4f m){
+   glUniformMatrix4fv(_uniform_model_font_uPvmPrev,1,GL_FALSE,(float*)m);
+}
+static void shader_model_font_uOffset(v3f v){
+   glUniform3fv(_uniform_model_font_uOffset,1,v);
+}
+static void shader_model_font_uTexMain(int i){
+   glUniform1i(_uniform_model_font_uTexMain,i);
+}
+static void shader_model_font_uColour(v4f v){
+   glUniform4fv(_uniform_model_font_uColour,1,v);
+}
+static void shader_model_font_register(void){
+   vg_shader_register( &_shader_model_font );
+}
+static void shader_model_font_use(void){ glUseProgram(_shader_model_font.id); }
+static void shader_model_font_link(void){
+   _uniform_model_font_uMdl = glGetUniformLocation( _shader_model_font.id, "uMdl" );
+   _uniform_model_font_uPv = glGetUniformLocation( _shader_model_font.id, "uPv" );
+   _uniform_model_font_uPvmPrev = glGetUniformLocation( _shader_model_font.id, "uPvmPrev" );
+   _uniform_model_font_uOffset = glGetUniformLocation( _shader_model_font.id, "uOffset" );
+   _uniform_model_font_uTexMain = glGetUniformLocation( _shader_model_font.id, "uTexMain" );
+   _uniform_model_font_uColour = glGetUniformLocation( _shader_model_font.id, "uColour" );
+}
+#endif /* SHADER_model_font_H */
diff --git a/shaders/model_font.vs b/shaders/model_font.vs
new file mode 100644 (file)
index 0000000..764f008
--- /dev/null
@@ -0,0 +1,36 @@
+layout (location=0) in vec3 a_co;
+layout (location=1) in vec3 a_norm;
+layout (location=2) in vec2 a_uv;
+layout (location=3) in vec4 a_colour;
+layout (location=4) in vec4 a_weights;
+layout (location=5) in ivec4 a_groups;
+
+#include "motion_vectors_vs.glsl"
+
+uniform mat4x3 uMdl;
+uniform mat4 uPv;
+uniform mat4 uPvmPrev;
+uniform vec3 uOffset;
+
+out vec4 aColour;
+out vec2 aUv;
+out vec3 aNorm;
+out vec3 aCo;
+out vec3 aWorldCo;
+
+void main()
+{
+   vec3 co = a_co+uOffset;
+   vec3 world_pos0 = uMdl     * vec4( co, 1.0 );
+   vec4 vproj0     = uPv      * vec4( world_pos0, 1.0 );
+   vec4 vproj1     = uPvmPrev * vec4( co, 1.0 );
+
+   vs_motion_out( vproj0, vproj1 );
+
+   gl_Position = vproj0;
+   aWorldCo = world_pos0;
+   aColour = a_colour;
+   aUv = a_uv;
+   aNorm = mat3(uMdl) * a_norm;
+   aCo = a_co;
+}
index f0fb21802cf194d54e7762a7dcbdae536138afff..303327706d5fde87471a435443fe6874b665fc07 100644 (file)
@@ -18,6 +18,7 @@
 #include "render.h"
 #include "audio.h"
 #include "world.h"
+#include "font.h"
 
 
 
@@ -217,6 +218,10 @@ VG_STATIC void vg_load(void)
    vg_loader_step( render_init, NULL );
    vg_loader_step( menu_init, NULL );
    vg_loader_step( world_init, NULL );
+   vg_loader_step( font3d_init, NULL );
+
+   font3d_load( &test_font, "models/rs_font.mdl", vg_mem.rtmemory );
+
    //vg_loader_step( player_init, NULL );
    //vg_loader_step( vehicle_init, NULL );
    //
@@ -241,8 +246,8 @@ VG_STATIC void vg_load(void)
    /* 'systems' are completely loaded now */
 
    /* load home world */
-   //world_load( &world_global.worlds[0], "maps/mp_gridmap.mdl" );
-   world_load( &world_global.worlds[0], "maps/mp_mtzero.mdl" );
+   world_load( &world_global.worlds[0], "maps/mp_gridmap.mdl" );
+   //world_load( &world_global.worlds[0], "maps/mp_mtzero.mdl" );
 
 #if 0
    world_load( &world_global.worlds[1], "maps/mp_gridmap.mdl" );
index ad87ed1d106cdf8c484071c3fc1603805ddc8a55..a073b26af051d6edcd08b7bc1e866a165ea4c882 100644 (file)
@@ -27,8 +27,8 @@ VG_STATIC void gate_transform_update( ent_gate *gate )
    v3_copy( gate->co[1], recv_to_world[3] );
    m4x3_mul( recv_to_world, to_local, gate->transport );
 
-   m4x3_scalev( gate->to_world, (v3f){ gate->dimensions[0], 
-                                       gate->dimensions[1], 1.0f } );
+   m3x3_scale( gate->to_world, (v3f){ gate->dimensions[0], 
+                                      gate->dimensions[1], 1.0f } );
 }
 
 VG_STATIC void world_gates_init(void)
index 61aa86cde5faa0ba3a3aeb3c2575edd5b5da8ecc..5a81d0c77acdd33831275d9b3d4235c41202da8b 100644 (file)
@@ -8,6 +8,7 @@
 #include <time.h>
 #include "world.h"
 #include "world_gate.h"
+#include "font.h"
 
 #if 0
 #include "shaders/vblend.h"
@@ -59,7 +60,7 @@ VG_STATIC void world_routes_clear( world_instance *world )
 {
    for( u32 i=0; i<mdl_arrcount( &world->ent_route ); i++ ){
       ent_route *route = mdl_arritm( &world->ent_route, i );
-      route->active_checkpoint = 0xffffffff;
+      route->active_checkpoint = 0xffff;
    }
 
    for( u32 i=0; i<mdl_arrcount( &world->ent_gate ); i++ ){
@@ -77,9 +78,10 @@ VG_STATIC void world_routes_time_lap( world_instance *world, ent_route *route )
    vg_info( "------- time lap %s -------\n", 
             mdl_pstr(&world->meta,route->pstr_name) );
 
-   double total_time = 0.0;
+   double start_time = 0.0;
    u32 last_version=0;
-   int validated = 1;
+
+   u32 valid_count=0;
 
    for( u32 i=0; i<route->checkpoints_count; i++ ){
       u32 cpid  = route->checkpoints_start+(i+route->active_checkpoint);
@@ -90,22 +92,34 @@ VG_STATIC void world_routes_time_lap( world_instance *world, ent_route *route )
                 rg = mdl_arritm( &world->ent_gate, rg->target );
 
       if( i == 0 )
-         total_time = rg->timing_time;
+         start_time = rg->timing_time;
       else{
-         if( last_version+1 != rg->timing_version )
-            validated = 0;
+         if( last_version+1 == rg->timing_version )
+            valid_count ++;
+         else
+            valid_count = 0;
       }
 
       last_version = rg->timing_version;
       vg_info( "%u %f\n", rg->timing_version, rg->timing_time );
    }
 
+   if( world_global.current_run_version == last_version+1 )
+      valid_count ++;
+   else
+      valid_count = 0;
+
    vg_info( "%u %f\n", world_global.current_run_version, world_global.time );
 
-   if( validated && (world_global.current_run_version == last_version+1)){
-      total_time = world_global.time - total_time;
-      world_routes_local_set_record( world, route, total_time );
+   if( valid_count==route->checkpoints_count ){
+      double lap_time = world_global.time - start_time;
+      world_routes_local_set_record( world, route, lap_time );
    }
+
+   route->valid_checkpoints = valid_count+1;
+   route->timing_base = start_time;
+
+   vg_info( "valid: %u\n", valid_count );
    vg_info( "----------------------------\n" );
 }
 
@@ -123,7 +137,7 @@ VG_STATIC void world_routes_activate_entry_gate( world_instance *world,
       ent_route *route = mdl_arritm( &world->ent_route, i );
 
       u32 active_prev = route->active_checkpoint;
-      route->active_checkpoint = 0xffffffff;
+      route->active_checkpoint = 0xffff;
 
       for( u32 j=0; j<4; j++ ){
          if( dest->routes[j] == i ){
@@ -165,7 +179,7 @@ VG_STATIC void world_routes_debug( world_instance *world )
                         0xff5442f5 };
 
       u32 cc = 0xffcccccc;
-      if( route->active_checkpoint != 0xffffffff ){
+      if( route->active_checkpoint != 0xffff ){
          cc = colours[i%vg_list_size(colours)];
       }
 
@@ -337,7 +351,7 @@ VG_STATIC void world_routes_create_mesh( world_instance *world, u32 route_id )
 
       v3_add( (v3f){0.0f,0.1f,0.0f}, start_gate->co[0], p[0] );
       p[0][3]  = start_gate->ref_count;
-      p[0][3] -= (float)start_gate->ref_total * 0.5f;
+      p[0][3] -= (float)start_gate->route_count * 0.5f;
       start_gate->ref_count ++;
 
       if( !c0->path_count )
@@ -401,7 +415,7 @@ VG_STATIC void world_routes_create_mesh( world_instance *world, u32 route_id )
             if( i == route->checkpoints_count-1)
                p[2][3] -= 1.0f;
 
-            p[2][3] -= (float)collector->ref_total * 0.5f;
+            p[2][3] -= (float)collector->route_count * 0.5f;
             //collector->ref_count ++;
          }
 
@@ -441,7 +455,7 @@ VG_STATIC void world_routes_generate( world_instance *world )
    for( u32 i=0; i<mdl_arrcount(&world->ent_gate); i++ ){
       ent_gate *gate = mdl_arritm( &world->ent_gate, i );
       gate->ref_count = 0;
-      gate->ref_total = 0;
+      gate->route_count = 0;
    }
 
    for( u32 i=0; i<mdl_arrcount(&world->ent_route_node); i++ ){
@@ -462,7 +476,7 @@ VG_STATIC void world_routes_generate( world_instance *world )
 
          ent_gate *start_gate = mdl_arritm( &world->ent_gate, c0->gate_index );
          start_gate = mdl_arritm( &world->ent_gate, start_gate->target );
-         start_gate->ref_total ++;
+         start_gate->route_count ++;
 
          if( !c0->path_count )
             continue;
@@ -579,7 +593,7 @@ VG_STATIC void world_routes_update( world_instance *world )
    for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
       ent_route *route = mdl_arritm( &world->ent_route, i );
       
-      int target = route->active_checkpoint == 0xffffffff? 0: 1;
+      int target = route->active_checkpoint == 0xffff? 0: 1;
       route->factive = vg_lerpf( route->factive, target, 0.6f*vg.time_delta );
    }
 }
@@ -629,6 +643,83 @@ VG_STATIC void render_world_routes( world_instance *world, camera *cam,
       mdl_draw_submesh( &route->sm );
    }
 
+   /* timers
+    * ---------------------------------------------------- */
+   if( layer_depth == 0 ){
+      font3d_bind( &test_font, cam );
+
+      for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
+         ent_route *route = mdl_arritm( &world->ent_route, i );
+
+         if( route->active_checkpoint != 0xffff ){
+            v4f colour;
+            float brightness = 0.3f + world->ub_lighting.g_day_phase;
+            v3_muls( route->colour, brightness, colour );
+            colour[3] = 1.0f-route->factive;
+
+            shader_model_font_uColour( colour );
+
+            u32 next = route->active_checkpoint+1+layer_depth;
+                next = next % route->checkpoints_count;
+                next += route->checkpoints_start;
+
+            ent_checkpoint *cp = mdl_arritm( &world->ent_checkpoint, next );
+            ent_gate *gate = mdl_arritm( &world->ent_gate, cp->gate_index );
+
+            
+            u32 j=0;
+            for( ; j<4; j++ ){
+               if( gate->routes[j] == i ){
+                  break;
+               }
+            }
+            float h0 = 0.8f,
+                  h1 = 1.2f,
+                  depth = 0.4f,
+                  size = 0.4f;
+
+            char text[256];
+
+            if( route->valid_checkpoints >= route->checkpoints_count ){
+               double lap_time = world_global.time - route->timing_base;
+               snprintf( text, 255, "%.1f", lap_time );
+            }
+            else{
+               snprintf( text, 255, "%hu/%hu", route->valid_checkpoints,
+                                               route->checkpoints_count );
+            }
+
+            float align_r = font3d_string_width( &test_font, 0, text ) * size;
+
+            v3f positions[] = {
+               { -0.92f, h0, depth },
+               {  0.92f - align_r, h0, depth },
+               { -0.92f, h1, depth },
+               {  0.92f - align_r, h1, depth },
+            };
+
+            ent_gate *dest = mdl_arritm( &world->ent_gate, gate->target );
+
+            if( dest->route_count == 1 ){
+               positions[0][0] = -align_r*0.5f;
+               positions[0][1] = h1;
+            }
+
+            m4x3f model;
+            m3x3_copy( gate->to_world, model );
+            float ratio = v3_length(model[0]) / v3_length(model[1]);
+
+            m3x3_scale( model, (v3f){ size, size*ratio, 0.1f } );
+            m4x3_mulv( gate->to_world, positions[j], model[3] );
+
+            font3d_simple_draw( &test_font, 0, text, cam, model );
+         }
+      }
+   }
+
+   /* gate markers 
+    * ---------------------------------------------------- */
+
    shader_model_gate_use();
    shader_model_gate_uPv( cam->mtx.pv );
    shader_model_gate_uCam( cam->pos );
@@ -645,7 +736,7 @@ VG_STATIC void render_world_routes( world_instance *world, camera *cam,
    for( u32 i=0; i<mdl_arrcount(&world->ent_route); i++ ){
       ent_route *route = mdl_arritm( &world->ent_route, i );
 
-      if( route->active_checkpoint != 0xffffffff ){
+      if( route->active_checkpoint != 0xffff ){
          v4f colour;
          float brightness = 0.3f + world->ub_lighting.g_day_phase;
          v3_muls( route->colour, brightness, colour );