some audio things
[carveJwlIkooP6JGAAIwe30JlM.git] / blender_export.py
index ce4e8264af2fb32dfcdb3efbf73c98fd6733fb7c..3e4704ea466fa1e6d8d97eb3a4f44b6bb0dee6f2 100644 (file)
@@ -2,7 +2,7 @@
 # =============================================================================
 # 
 # Copyright  .        . .       -----, ,----- ,---.   .---.
-# 2021-2022  |\      /| |           /  |      |    | |    /|
+# 2021-2023  |\      /| |           /  |      |    | |    /|
 #            | \    / | +--        /   +----- +---'  |   / |
 #            |  \  /  | |         /    |      |   \  |  /  |
 #            |   \/   | |        /     |      |    \ | /   |
@@ -17,7 +17,7 @@
 # otherwise
 #
 
-import bpy, math, gpu
+import bpy, math, gpu, os
 import cProfile
 from ctypes import *
 from mathutils import *
@@ -57,10 +57,26 @@ class mdl_submesh(Structure):
                ("material_id",c_uint32)]        # index into the material array
 #}
 
+class mdl_texture(Structure):
+#{
+   _pack_ = 1
+   _fields_ = [("pstr_name",c_uint32),
+               ("pack_offset",c_uint32),
+               ("pack_length",c_uint32)]
+#}
+
 class mdl_material(Structure):
 #{
    _pack_ = 1
-   _fields_ = [("pstr_name",c_uint32)]
+   _fields_ = [("pstr_name",c_uint32),
+               ("shader",c_uint32),
+               ("flags",c_uint32),
+               ("surface_prop",c_uint32),
+               ("colour",c_float*4),
+               ("colour1",c_float*4),
+               ("tex_diffuse",c_uint32),
+               ("tex_decal",c_uint32),
+               ("tex_normal",c_uint32)]
 #}
 
 class mdl_node(Structure):
@@ -95,6 +111,9 @@ class mdl_header(Structure):
                ("material_count",c_uint32),
                ("material_offset",c_uint32),
 
+               ("texture_count",c_uint32),
+               ("texture_offset",c_uint32),
+
                ("anim_count",c_uint32),
                ("anim_offset",c_uint32),
 
@@ -111,7 +130,10 @@ class mdl_header(Structure):
                ("vertex_offset",c_uint32),
 
                ("indice_count",c_uint32),
-               ("indice_offset",c_uint32),]
+               ("indice_offset",c_uint32),
+
+               ("pack_size",c_uint32),
+               ("pack_offset",c_uint32)]
 #}
 
 class mdl_animation(Structure):
@@ -209,11 +231,13 @@ class classtype_gate(Structure):
       indices = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(7,8)]
 
       for l in indices:
+      #{
          v0 = vs[l[0]]
          v1 = vs[l[1]]
          cv_view_verts += [(v0[0],v0[1],v0[2])]
          cv_view_verts += [(v1[0],v1[1],v1[2])]
          cv_view_colours += [(1,1,0,1),(1,1,0,1)]
+      #}
 
       sw = (0.4,0.4,0.4,0.2)
       if obj.cv_data.target != None:
@@ -231,6 +255,40 @@ class classtype_gate(Structure):
    #}
 #}
 
+class classtype_nonlocal_gate(classtype_gate):
+#{
+   def encode_obj(_,node,node_def):
+   #{
+      node.classtype = 300
+
+      obj = node_def['obj']
+      _.target = encoder_process_pstr( node_def['obj'].cv_data.strp )
+
+      if obj.type == 'MESH':
+      #{
+         _.dims[0] = obj.data.cv_data.v0[0]
+         _.dims[1] = obj.data.cv_data.v0[1]
+         _.dims[2] = obj.data.cv_data.v0[2]
+      #}
+      else:
+      #{
+         _.dims[0] = obj.cv_data.v0[0]
+         _.dims[1] = obj.cv_data.v0[1]
+         _.dims[2] = obj.cv_data.v0[2]
+      #}
+   #}
+
+   @staticmethod
+   def editor_interface( layout, obj ):
+   #{
+      layout.prop( obj.cv_data, "strp", text="Nonlocal ID" )
+
+      mesh = obj.data
+      layout.label( text=F"(i) Data is stored in {mesh.name}" )
+      layout.prop( mesh.cv_data, "v0", text="Gate dimensions" )
+   #}
+#}
+
 # Classtype 3
 #
 #  Purpose: player can reset here, its a safe place
@@ -270,6 +328,8 @@ class classtype_spawn(Structure):
          cv_view_verts += [(v1[0],v1[1],v1[2])]
          cv_view_colours += [(0,1,1,1),(0,1,1,1)]
       #}
+
+      cv_draw_sphere( obj.location, 20.0, [0.1,0,0.9,0.4] )
    #}
 
    @staticmethod
@@ -522,13 +582,14 @@ class classtype_skeleton(Structure):
 class classtype_bone(Structure):
 #{
    _pack_ = 1
-   _fields_ = [("deform",c_uint32),
+   _fields_ = [("flags",c_uint32),
                ("ik_target",c_uint32),
                ("ik_pole",c_uint32),
-               ("collider",c_uint32),
-               ("use_limits",c_uint32),
-               ("angle_limits",(c_float*3)*2),
-               ("hitbox",(c_float*3)*2)]
+               ("hitbox",(c_float*3)*2),
+               ("conevx",c_float*3),
+               ("conevy",c_float*3),
+               ("coneva",c_float*3),
+               ("conet",c_float)]
 
    def encode_obj(_, node,node_def):
    #{
@@ -537,19 +598,24 @@ class classtype_bone(Structure):
       armature_def = node_def['linked_armature']
       obj = node_def['bone']
       
-      _.deform = node_def['deform']
+      _.flags = node_def['deform']
       
       if 'ik_target' in node_def:
       #{
+         _.flags |= 0x2
          _.ik_target = armature_def['bones'].index( node_def['ik_target'] )
          _.ik_pole   = armature_def['bones'].index( node_def['ik_pole'] )
       #}
       
       # For ragdolls
       #
-      if obj.cv_data.collider:
+      if obj.cv_data.collider != 'collider_none':
       #{
-         _.collider = 1
+         if obj.cv_data.collider == 'collider_box':
+            _.flags |= 0x4
+         else:
+            _.flags |= 0x8
+
          _.hitbox[0][0] =  obj.cv_data.v0[0]
          _.hitbox[0][1] =  obj.cv_data.v0[2]
          _.hitbox[0][2] = -obj.cv_data.v1[1]
@@ -560,13 +626,17 @@ class classtype_bone(Structure):
 
       if obj.cv_data.con0:
       #{
-         _.use_limits = 1 
-         _.angle_limits[0][0] =  obj.cv_data.mins[0]
-         _.angle_limits[0][1] =  obj.cv_data.mins[2]
-         _.angle_limits[0][2] = -obj.cv_data.maxs[1]
-         _.angle_limits[1][0] =  obj.cv_data.maxs[0]
-         _.angle_limits[1][1] =  obj.cv_data.maxs[2]
-         _.angle_limits[1][2] = -obj.cv_data.mins[1]
+         _.flags |= 0x100
+         _.conevx[0] =  obj.cv_data.conevx[0]
+         _.conevx[1] =  obj.cv_data.conevx[2]
+         _.conevx[2] = -obj.cv_data.conevx[1]
+         _.conevy[0] =  obj.cv_data.conevy[0]
+         _.conevy[1] =  obj.cv_data.conevy[2]
+         _.conevy[2] = -obj.cv_data.conevy[1]
+         _.coneva[0] =  obj.cv_data.coneva[0]
+         _.coneva[1] =  obj.cv_data.coneva[2]
+         _.coneva[2] = -obj.cv_data.coneva[1]
+         _.conet = obj.cv_data.conet
       #}
    #}
 #}
@@ -712,6 +782,11 @@ class classtype_audio(Structure):
       if obj.cv_data.bp1: flags |= 0x4
       if obj.cv_data.bp2: flags |= 0x8
 
+      if obj.cv_data.audio_format == 'stereo':
+         flags |= 0x200
+      if obj.cv_data.audio_format == 'remain compressed':
+         flags |= 0x400
+
       _.flags = flags
       _.volume = obj.cv_data.fltp
    #}
@@ -719,11 +794,14 @@ class classtype_audio(Structure):
    @staticmethod
    def editor_interface( layout, obj ):
    #{
-      layout.prop( obj.cv_data, "strp" )
+      layout.prop( obj.cv_data, "strp", text = "File (.ogg)" )
 
       layout.prop( obj.cv_data, "bp0", text = "Looping" )
       layout.prop( obj.cv_data, "bp1", text = "3D Audio" )
       layout.prop( obj.cv_data, "bp2", text = "Auto Start" )
+      layout.prop( obj.cv_data, "audio_format" )
+
+      layout.prop( obj.cv_data, "fltp", text = "Volume (0-1)" )
    #}
 
    @staticmethod
@@ -735,6 +813,125 @@ class classtype_audio(Structure):
    #}
 #}
 
+# Classtype 200
+# 
+#  Purpose: world light
+#
+class classtype_world_light( Structure ):
+#{
+   _pack_ = 1
+   _fields_ = [("type",c_uint32),
+               ("colour",c_float*4),
+               ("angle",c_float),
+               ("range",c_float)]
+
+   def encode_obj(_, node, node_def):
+   #{
+      node.classtype = 200
+
+      obj  = node_def['obj']
+      data = obj.data
+      _.colour[0] = data.color[0]
+      _.colour[1] = data.color[1]
+      _.colour[2] = data.color[2]
+      _.colour[3] = data.energy
+      _.range = data.cutoff_distance # this has to be manually set
+                                     # TODO: At some point, automate a min
+                                     #       threshold value
+
+      if obj.data.type == 'POINT':
+      #{
+         _.type = 0
+         _.angle = 0.0
+      #}
+      elif obj.data.type == 'SPOT':
+      #{
+         _.type = 1
+         _.angle = data.spot_size*0.5
+      #}
+
+      if data.cv_data.bp0:
+         _.type += 2
+   #}
+
+   @staticmethod
+   def editor_interface( layout, obj ):
+   #{
+      pass
+   #}
+#}
+
+# Classtype 201
+# 
+#  Purpose: lighting settings for world
+#
+class classtype_lighting_info(Structure):
+#{
+   _pack_ = 1
+   _fields_ = [("colours",(c_float*3)*3),
+               ("directions",(c_float*2)*3),
+               ("states",c_uint32*3),
+               ("shadow_spread",c_float),
+               ("shadow_length",c_float),
+               ("ambient",c_float*3)]
+
+   def encode_obj(_, node, node_def):
+   #{
+      node.classtype = 201
+
+      # TODO
+   #}
+
+   @staticmethod
+   def editor_interface( layout, obj ):
+   #{
+      pass
+   #}
+#}
+
+class classtype_spawn_link(Structure):
+#{
+   _pack_ = 1
+   _fields_ = [("connections",c_uint32*4)]
+
+   def encode_obj(_, node,node_def ):
+   #{
+      node.classtype = 0
+   #}
+
+   @staticmethod
+   def editor_interface( layout, obj ):
+   #{
+      pass
+   #}
+
+   @staticmethod
+   def draw_scene_helpers( obj ):
+   #{
+      global cv_view_verts, cv_view_colours
+
+      count = 0
+
+      for obj1 in bpy.context.collection.objects:
+      #{
+         if (obj1.cv_data.classtype != 'classtype_spawn_link') and \
+            (obj1.cv_data.classtype != 'classtype_spawn') :
+            continue
+
+         if (obj1.location - obj.location).length < 40.0:
+         #{
+            cv_draw_line( obj.location, obj1.location, [1,1,1,1] )
+            count +=1
+         #}
+
+         if count == 4:
+            break
+      #}
+      
+      cv_draw_sphere( obj.location, 20.0, [0.5,0,0.2,0.4] )
+   #}
+#}
+
 # ---------------------------------------------------------------------------- #
 #                                                                              #
 #                                Compiler section                              #
@@ -747,7 +944,7 @@ g_encoder = None
 
 # Reset encoder
 #
-def encoder_init():
+def encoder_init( collection ):
 #{
    global g_encoder
 
@@ -757,6 +954,10 @@ def encoder_init():
       #
       'header': mdl_header(),
 
+      # Options
+      #
+      'pack_textures': collection.cv_data.pack_textures,
+
       # Compiled data chunks (each can be read optionally by the client)
       #
       'data':
@@ -765,6 +966,7 @@ def encoder_init():
          'node': [],      # Metadata 'chunk'
          'submesh': [],
          'material': [],
+         'texture': [],
          'anim': [],
          'entdata': bytearray(), # variable width
          'strings': bytearray(), # .
@@ -773,6 +975,8 @@ def encoder_init():
          #3---------------------------------
          'vertex': [],    # Mesh data
          'indice': [],
+         #4---------------------------------
+         'pack': bytearray()  # Other generic packed data
       },
 
       # All objects of the model in their final heirachy
@@ -786,16 +990,27 @@ def encoder_init():
       'string_cache':{},
       'mesh_cache': {},
       'material_cache': {},
+      'texture_cache': {}
    }
 
    g_encoder['header'].identifier = 0xABCD0000
    g_encoder['header'].version = 1
 
-   # Add fake NoneID material
+   # Add fake NoneID material and texture
    #
-   none_material = c_uint32(1234)
-   none_material.name = ""
-   encoder_process_material( none_material )
+   none_material = mdl_material()
+   none_material.pstr_name = encoder_process_pstr( "" )
+   none_material.texture_id = 0
+
+   none_texture = mdl_texture()
+   none_texture.pstr_name = encoder_process_pstr( "" )
+   none_texture.pack_offset = 0
+   none_texture.pack_length = 0
+   
+   g_encoder['data']['material'] += [none_material]
+   g_encoder['data']['texture']  += [none_texture]
+
+   g_encoder['data']['pack'].extend( b'datapack\0\0\0\0\0\0\0\0' )
 
    # Add root node
    #
@@ -858,6 +1073,175 @@ def encoder_process_pstr( s ):
    return cache[s]
 #}
 
+def get_texture_resource_name( img ):
+#{
+   return os.path.splitext( img.name )[0]
+#}
+
+# Pack a texture
+#
+def encoder_process_texture( img ):
+#{
+   global g_encoder
+
+   if img == None:
+      return 0
+
+   cache = g_encoder['texture_cache']
+   buffer = g_encoder['data']['texture']
+   pack = g_encoder['data']['pack']
+
+   name = get_texture_resource_name( img )
+
+   if name in cache:
+      return cache[name]
+   
+   cache[name] = len( buffer )
+   
+   tex = mdl_texture()
+   tex.pstr_name = encoder_process_pstr( name )
+
+   if g_encoder['pack_textures']:
+   #{
+      tex.pack_offset = len( pack )
+      pack.extend( qoi_encode( img ) )
+      tex.pack_length = len( pack ) - tex.pack_offset
+   #}
+   else:
+      tex.pack_offset = 0
+
+   buffer += [ tex ]
+   return cache[name]
+#}
+
+def material_tex_image(v):
+#{
+    return {
+       "Image Texture":
+       {
+          "image": F"{v}"
+       }
+    }
+#}
+
+cxr_graph_mapping = \
+{
+   # Default shader setup 
+   "Principled BSDF":
+   {
+      "Base Color":
+      {
+         "Image Texture":
+         {
+            "image": "tex_diffuse"
+         },
+         "Mix":
+         {
+            "A": material_tex_image("tex_diffuse"),
+            "B": material_tex_image("tex_decal")
+         },
+      },
+      "Normal":
+      {
+         "Normal Map":
+         {
+            "Color": material_tex_image("tex_normal")
+         }
+      }
+   }
+}
+
+# https://harrygodden.com/git/?p=convexer.git;a=blob;f=__init__.py;#l1164
+#
+def material_info(mat):
+#{
+   info = {}
+
+   # Using the cv_graph_mapping as a reference, go through the shader
+   # graph and gather all $props from it.
+   #
+   def _graph_read( node_def, node=None, depth=0 ):
+   #{
+      nonlocal mat
+      nonlocal info
+      
+      # Find rootnodes
+      #
+      if node == None:
+      #{
+         _graph_read.extracted = []
+
+         for node_idname in node_def:
+         #{
+            for n in mat.node_tree.nodes:
+            #{
+               if n.name == node_idname:
+               #{
+                  node_def = node_def[node_idname]
+                  node = n
+                  break
+               #}
+            #}
+         #}
+      #}
+
+      for link in node_def:
+      #{
+         link_def = node_def[link]
+
+         if isinstance( link_def, dict ):
+         #{
+            node_link = None
+            for x in node.inputs:
+            #{
+               if isinstance( x, bpy.types.NodeSocketColor ):
+               #{
+                  if link == x.name:
+                  #{
+                     node_link = x
+                     break
+                  #}
+               #}
+            #}
+
+            if node_link and node_link.is_linked:
+            #{
+               # look for definitions for the connected node type
+               #
+               from_node = node_link.links[0].from_node
+               
+               node_name = from_node.name.split('.')[0]
+               if node_name in link_def:
+               #{
+                  from_node_def = link_def[ node_name ]
+
+                  _graph_read( from_node_def, from_node, depth+1 )
+               #}
+               
+               # No definition! :(
+               #  TODO: Make a warning for this?
+            #}
+            else:
+            #{
+               if "default" in link_def:
+               #{
+                  prop = link_def['default']
+                  info[prop] = node_link.default_value
+               #}
+            #}
+         #}
+         else:
+         #{
+            prop = link_def
+            info[prop] = getattr( node, link )
+         #}
+      #}
+   #}
+
+   _graph_read( cxr_graph_mapping )
+   return info
+#}
+
 # Add a material to the material buffer. Returns 0 (None ID) if invalid
 #
 def encoder_process_material( mat ):
@@ -877,8 +1261,70 @@ def encoder_process_material( mat ):
 
    dest = mdl_material()
    dest.pstr_name = encoder_process_pstr( mat.name )
-   buffer += [dest]
+   
+   flags = 0x00
+   if mat.cv_data.collision: 
+      flags |= 0x2
+      if mat.cv_data.skate_surface: flags |= 0x1
+      if mat.cv_data.grind_surface: flags |= (0x8|0x1)
+
+   if mat.cv_data.grow_grass: flags |= 0x4
+   dest.flags = flags
+
+   if mat.cv_data.surface_prop == 'concrete': dest.surface_prop = 0
+   if mat.cv_data.surface_prop == 'wood': dest.surface_prop = 1
+   if mat.cv_data.surface_prop == 'grass': dest.surface_prop = 2
+   if mat.cv_data.surface_prop == 'tiles': dest.surface_prop = 3
+
+   if mat.cv_data.shader == 'standard': dest.shader = 0
+   if mat.cv_data.shader == 'standard_cutout': dest.shader = 1
+   if mat.cv_data.shader == 'terrain_blend': 
+   #{
+      dest.shader = 2
+
+      dest.colour[0] = pow( mat.cv_data.sand_colour[0], 1.0/2.2 )
+      dest.colour[1] = pow( mat.cv_data.sand_colour[1], 1.0/2.2 )
+      dest.colour[2] = pow( mat.cv_data.sand_colour[2], 1.0/2.2 )
+      dest.colour[3] = 1.0
+
+      dest.colour1[0] = mat.cv_data.blend_offset[0]
+      dest.colour1[1] = mat.cv_data.blend_offset[1]
+   #}
+
+   if mat.cv_data.shader == 'vertex_blend':
+   #{
+      dest.shader = 3
+
+      dest.colour1[0] = mat.cv_data.blend_offset[0]
+      dest.colour1[1] = mat.cv_data.blend_offset[1]
+   #}
+
+   if mat.cv_data.shader == 'water':
+   #{
+      dest.shader = 4
+
+      dest.colour[0]  = pow( mat.cv_data.shore_colour[0], 1.0/2.2 )
+      dest.colour[1]  = pow( mat.cv_data.shore_colour[1], 1.0/2.2 )
+      dest.colour[2]  = pow( mat.cv_data.shore_colour[2], 1.0/2.2 )
+      dest.colour[3]  = 1.0
+      dest.colour1[0] = pow( mat.cv_data.ocean_colour[0], 1.0/2.2 )
+      dest.colour1[1] = pow( mat.cv_data.ocean_colour[1], 1.0/2.2 )
+      dest.colour1[2] = pow( mat.cv_data.ocean_colour[2], 1.0/2.2 )
+      dest.colour1[3] = 1.0
+   #}
+   
+   inf = material_info( mat )
+   
+   if mat.cv_data.shader == 'standard' or \
+      mat.cv_data.shader == 'standard_cutout' or \
+      mat.cv_data.shader == 'terrain_blend' or \
+      mat.cv_data.shader == 'vertex_blend':
+   #{
+      if 'tex_diffuse' in inf: 
+         dest.tex_diffuse = encoder_process_texture(inf['tex_diffuse'])
+   #}
 
+   buffer += [dest]
    return cache[mat.name]
 #}
 
@@ -910,10 +1356,12 @@ def encoder_build_scene_graph( collection ):
 
    for obj in collection.all_objects:
    #{
-      if obj.parent: continue
+      #if obj.parent: continue
 
       def _extend( p, n, d ):
       #{
+         nonlocal collection
+
          uid = _new_uid()
          tree = {}
          tree["uid"] = uid
@@ -922,7 +1370,7 @@ def encoder_build_scene_graph( collection ):
          tree["obj"] = n
          tree["parent"] = p
          n.cv_data.uid = uid
-         
+
          # Descend into amature
          #
          if n.type == 'ARMATURE':
@@ -930,6 +1378,7 @@ def encoder_build_scene_graph( collection ):
             tree["bones"] = [None] # None is the root transform
             tree["ik_count"] = 0
             tree["collider_count"] = 0
+            tree["compile_animation"] = collection.cv_data.animations
             
             # Here also collects some information about constraints, ik and 
             # counts colliders for the armature.
@@ -962,7 +1411,7 @@ def encoder_build_scene_graph( collection ):
                   #}
                #}
 
-               if n.cv_data.collider:
+               if n.cv_data.collider != 'collider_none':
                   tree['collider_count'] += 1
 
                btree['deform'] = n.use_deform
@@ -978,7 +1427,6 @@ def encoder_build_scene_graph( collection ):
          #
          for obj1 in n.children:
          #{
-            nonlocal collection
             for c1 in obj1.users_collection:
             #{
                if c1 == collection:
@@ -1033,18 +1481,18 @@ def encoder_vertex_push( vertex_reference, co,norm,uv,colour,groups,weights ):
           int(norm[2]*m+0.5),
           int(uv[0]*m+0.5),
           int(uv[1]*m+0.5),
-          colour[0]*m+0.5,    # these guys are already quantized
-          colour[1]*m+0.5,    # .
-          colour[2]*m+0.5,    # .
-          colour[3]*m+0.5,    # .
-          weights[0]*m+0.5,   # v
-          weights[1]*m+0.5,
-          weights[2]*m+0.5,
-          weights[3]*m+0.5,
-          groups[0]*m+0.5,
-          groups[1]*m+0.5,
-          groups[2]*m+0.5,
-          groups[3]*m+0.5)
+          colour[0],    # these guys are already quantized
+          colour[1],    # .
+          colour[2],    # .
+          colour[3],    # .
+          weights[0],   # v
+          weights[1],
+          weights[2],
+          weights[3],
+          groups[0],
+          groups[1],
+          groups[2],
+          groups[3])
 
    if key in vertex_reference:
       return vertex_reference[key]
@@ -1128,9 +1576,6 @@ def encoder_compile_mesh( node, node_def ):
    node.submesh_start = len( g_encoder['data']['submesh'] )
    node.submesh_count = 0
 
-   default_mat = c_uint32(12345)
-   default_mat.name = ""
-
    dgraph = bpy.context.evaluated_depsgraph_get()
    data = obj.evaluated_get(dgraph).data
    data.calc_loop_triangles()
@@ -1138,7 +1583,7 @@ def encoder_compile_mesh( node, node_def ):
    
    # Mesh is split into submeshes based on their material
    #
-   mat_list = data.materials if len(data.materials) > 0 else [default_mat]
+   mat_list = data.materials if len(data.materials) > 0 else [None]
    for material_id, mat in enumerate(mat_list):
    #{
       mref = {}
@@ -1235,6 +1680,20 @@ def encoder_compile_mesh( node, node_def ):
                      weights[ml] = max( weights[ml], 0 )
                   #}
                #}
+            #}
+            else:
+            #{
+               li1 = tri.loops[(j+1)%3]
+               vi1 = data.loops[li1].vertex_index
+               e0 = data.edges[ data.loops[li].edge_index ]
+
+               if e0.use_freestyle_mark and \
+                     ((e0.vertices[0] == vi and e0.vertices[1] == vi1) or \
+                      (e0.vertices[0] == vi1 and e0.vertices[1] == vi)):
+               #{
+                  weights[0] = 1
+               #}
+            #}
             
             # Add vertex and expand bound box
             #
@@ -1329,7 +1788,12 @@ def encoder_compile_armature( node, node_def ):
    # extra info
    node_def['anim_start'] = len(animdata)
    node_def['anim_count'] = 0
-   
+   if not node_def['compile_animation']:
+   #{
+      return
+   #}  
+
    # Compile anims
    #
    if obj.animation_data:
@@ -1467,10 +1931,14 @@ def encoder_process_definition( node_def ):
    #{
       obj      = node_def['obj']
       obj_type = obj.type
-      obj_co   = obj.location
+      obj_co   = obj.matrix_world @ Vector((0,0,0))
 
       if obj_type == 'ARMATURE':
          obj_classtype = 'classtype_skeleton'
+      elif obj_type == 'LIGHT':
+      #{
+         obj_classtype = 'classtype_world_light'
+      #}
       else:
       #{
          obj_classtype = obj.cv_data.classtype
@@ -1643,10 +2111,13 @@ def write_model(collection_name):
 #{
    global g_encoder
    print( F"Model graph | Create mode '{collection_name}'" )
+   folder = bpy.path.abspath(bpy.context.scene.cv_data.export_dir)
+   path = F"{folder}{collection_name}.mdl"
+   print( path )
    
    collection = bpy.data.collections[collection_name]
 
-   encoder_init()
+   encoder_init( collection )
    encoder_build_scene_graph( collection )
 
    # Compile 
@@ -1658,8 +2129,6 @@ def write_model(collection_name):
 
    # Write 
    #
-   # TODO HOLY
-   path = F"/home/harry/Documents/carve/models_src/{collection_name}.mdl"
    encoder_write_to_file( path )
 
    print( F"Completed {collection_name}.mdl" )
@@ -1712,6 +2181,44 @@ def cv_draw_sphere( pos, radius, colour ):
    cv_draw_lines()
 #}
 
+# Draw axis alligned sphere at position with radius
+#
+def cv_draw_halfsphere( pos, tx, ty, tz, radius, colour ):
+#{
+   global cv_view_verts, cv_view_colours
+   
+   ly = pos + tz*radius
+   lx = pos + ty*radius
+   lz = pos + tz*radius
+   
+   pi = 3.14159265358979323846264
+
+   for i in range(16):
+   #{
+      t = ((i+1.0) * 1.0/16.0) * pi
+      s = math.sin(t)
+      c = math.cos(t)
+
+      s1 = math.sin(t*2.0)
+      c1 = math.cos(t*2.0)
+
+      py = pos + s*tx*radius +                c *tz*radius
+      px = pos + s*tx*radius + c *ty*radius 
+      pz = pos +               s1*ty*radius + c1*tz*radius
+
+      cv_view_verts += [ px, lx ]
+      cv_view_verts += [ py, ly ]
+      cv_view_verts += [ pz, lz ]
+
+      cv_view_colours += [ colour, colour, colour, colour, colour, colour ]
+
+      ly = py
+      lx = px
+      lz = pz
+   #}
+   cv_draw_lines()
+#}
+
 # Draw transformed -1 -> 1 cube
 #
 def cv_draw_ucube( transform, colour ):
@@ -1767,9 +2274,9 @@ def cv_draw_line2( p0, p1, c0, c1 ):
    cv_draw_lines()
 #}
 
-# Just the tx because we dont really need ty for this app
+# 
 #
-def cv_tangent_basis_tx( n, tx ):
+def cv_tangent_basis( n, tx, ty ):
 #{
    if abs( n[0] ) >= 0.57735027:
    #{
@@ -1785,6 +2292,11 @@ def cv_tangent_basis_tx( n, tx ):
    #}
 
    tx.normalize()
+   _ty = n.cross( tx )
+
+   ty[0] = _ty[0]
+   ty[1] = _ty[1]
+   ty[2] = _ty[2]
 #}
 
 # Draw coloured arrow
@@ -1798,7 +2310,8 @@ def cv_draw_arrow( p0, p1, c0 ):
    n.normalize()
 
    tx = Vector((1,0,0))
-   cv_tangent_basis_tx( n, tx )
+   ty = Vector((1,0,0))
+   cv_tangent_basis( n, tx, ty )
    
    cv_view_verts += [p0,p1, midpt+(tx-n)*0.15,midpt, midpt+(-tx-n)*0.15,midpt ]
    cv_view_colours += [c0,c0,c0,c0,c0,c0]
@@ -1931,19 +2444,61 @@ def draw_limit( obj, center, major, minor, amin, amax, colour ):
    cv_draw_lines()
 #}
 
+# Cone and twist limit
+#
+def draw_cone_twist( center, vx, vy, va ):
+#{
+   global cv_view_verts, cv_view_colours
+   axis = vy.cross( vx )
+   axis.normalize()
+
+   size = 0.12
+
+   cv_view_verts += [center, center+va*size]
+   cv_view_colours += [ (1,1,1,1), (1,1,1,1) ]
+
+   for x in range(32):
+   #{
+      t0 = (x/32) * math.tau
+      t1 = ((x+1)/32) * math.tau
+
+      c0 = math.cos(t0)
+      s0 = math.sin(t0)
+      c1 = math.cos(t1)
+      s1 = math.sin(t1)
+      
+      p0 = center + (axis + vx*c0 + vy*s0).normalized() * size
+      p1 = center + (axis + vx*c1 + vy*s1).normalized() * size
+
+      col0 = ( abs(c0), abs(s0), 0.0, 1.0 )
+      col1 = ( abs(c1), abs(s1), 0.0, 1.0 )
+
+      cv_view_verts += [center, p0, p0, p1]
+      cv_view_colours += [ (0,0,0,0), col0, col0, col1 ]
+   #}
+
+   cv_draw_lines()
+#}
+
 # Draws constraints and stuff for the skeleton. This isnt documented and wont be
 #
 def draw_skeleton_helpers( obj ):
 #{
    global cv_view_verts, cv_view_colours
 
+   if obj.data.pose_position != 'REST':
+   #{
+      return
+   #}
+
    for bone in obj.data.bones:
    #{
-      if bone.cv_data.collider and (obj.data.pose_position == 'REST'):
+      c = bone.head_local
+      a = Vector((bone.cv_data.v0[0], bone.cv_data.v0[1], bone.cv_data.v0[2]))
+      b = Vector((bone.cv_data.v1[0], bone.cv_data.v1[1], bone.cv_data.v1[2]))
+
+      if bone.cv_data.collider == 'collider_box':
       #{
-         c = bone.head_local
-         a = bone.cv_data.v0
-         b = bone.cv_data.v1
          
          vs = [None]*8
          vs[0]=obj.matrix_world@Vector((c[0]+a[0],c[1]+a[1],c[2]+a[2]))
@@ -1967,20 +2522,67 @@ def draw_skeleton_helpers( obj ):
             cv_view_verts += [(v1[0],v1[1],v1[2])]
             cv_view_colours += [(0.5,0.5,0.5,0.5),(0.5,0.5,0.5,0.5)]
          #}
+      #}
+      elif bone.cv_data.collider == 'collider_capsule':
+      #{
+         v0 = b-a
+         major_axis = 0
+         largest = -1.0
 
-         center = obj.matrix_world @ c
-         if bone.cv_data.con0:
+         for i in range(3):
          #{
-            draw_limit( obj, c, Vector((0,1,0)),Vector((0,0,1)), \
-                        bone.cv_data.mins[0], bone.cv_data.maxs[0], \
-                        (1,0,0,1))
-            draw_limit( obj, c, Vector((0,0,1)),Vector((1,0,0)), \
-                        bone.cv_data.mins[1], bone.cv_data.maxs[1], \
-                        (0,1,0,1))
-            draw_limit( obj, c, Vector((1,0,0)),Vector((0,1,0)), \
-                        bone.cv_data.mins[2], bone.cv_data.maxs[2], \
-                        (0,0,1,1))
+            if abs(v0[i]) > largest:
+            #{
+               largest = abs(v0[i])
+               major_axis = i
+            #}
          #}
+
+         v1 = Vector((0,0,0))
+         v1[major_axis] = 1.0
+
+         tx = Vector((0,0,0))
+         ty = Vector((0,0,0))
+
+         cv_tangent_basis( v1, tx, ty )
+         r = (abs(tx.dot( v0 )) + abs(ty.dot( v0 ))) * 0.25
+         l = v0[ major_axis ] - r*2
+
+         p0 = obj.matrix_world@Vector( c + (a+b)*0.5 + v1*l*-0.5 )
+         p1 = obj.matrix_world@Vector( c + (a+b)*0.5 + v1*l* 0.5 )
+
+         colour = [0.2,0.2,0.2,1.0]
+         colour[major_axis] = 0.5
+
+         cv_draw_halfsphere( p0, -v1, ty, tx, r, colour )
+         cv_draw_halfsphere( p1,  v1, ty, tx, r, colour )
+         cv_draw_line( p0+tx* r, p1+tx* r, colour )
+         cv_draw_line( p0+tx*-r, p1+tx*-r, colour )
+         cv_draw_line( p0+ty* r, p1+ty* r, colour )
+         cv_draw_line( p0+ty*-r, p1+ty*-r, colour )
+      #}
+      else:
+      #{
+         continue
+      #}
+
+      center = obj.matrix_world @ c
+      if bone.cv_data.con0:
+      #{
+         vx = Vector([bone.cv_data.conevx[_] for _ in range(3)])
+         vy = Vector([bone.cv_data.conevy[_] for _ in range(3)])
+         va = Vector([bone.cv_data.coneva[_] for _ in range(3)])
+         draw_cone_twist( center, vx, vy, va )
+
+         #draw_limit( obj, c, Vector((0,0,1)),Vector((0,-1,0)), \
+         #            bone.cv_data.mins[0], bone.cv_data.maxs[0], \
+         #            (1,0,0,1))
+         #draw_limit( obj, c, Vector((0,-1,0)),Vector((1,0,0)), \
+         #            bone.cv_data.mins[1], bone.cv_data.maxs[1], \
+         #            (0,1,0,1))
+         #draw_limit( obj, c, Vector((1,0,0)),Vector((0,0,1)), \
+         #            bone.cv_data.mins[2], bone.cv_data.maxs[2], \
+         #            (0,0,1,1))
       #}
    #}
 #}
@@ -2056,6 +2658,31 @@ class CV_MESH_SETTINGS(bpy.types.PropertyGroup):
    v3: bpy.props.FloatVectorProperty(name="v3",size=3)
 #}
 
+class CV_LIGHT_SETTINGS(bpy.types.PropertyGroup):
+#{
+   bp0: bpy.props.BoolProperty( name="bp0" );
+#}
+
+class CV_LIGHT_PANEL(bpy.types.Panel):
+#{
+   bl_label="[Skate Rift]"
+   bl_idname="SCENE_PT_cv_light"
+   bl_space_type='PROPERTIES'
+   bl_region_type='WINDOW'
+   bl_context='data'
+
+   def draw(_,context):
+   #{
+      active_object = context.active_object
+      if active_object == None: return
+
+      if active_object.type != 'LIGHT': return
+
+      data = active_object.data.cv_data
+      _.layout.prop( data, "bp0", text="Only on during night" )
+   #}
+#}
+
 class CV_OBJ_SETTINGS(bpy.types.PropertyGroup):
 #{
    uid: bpy.props.IntProperty( name="" )
@@ -2093,23 +2720,45 @@ class CV_OBJ_SETTINGS(bpy.types.PropertyGroup):
       ('classtype_trigger',"classtype_trigger","",100),
       ('classtype_logic_achievement',"classtype_logic_achievement","",101),
       ('classtype_logic_relay',"classtype_logic_relay","",102),
+      ('classtype_spawn_link',"classtype_spawn_link","",150),
+      ('classtype_nonlocal_gate', "classtype_nonlocal_gate", "", 300)
+      ])
+
+   audio_format: bpy.props.EnumProperty(
+      name="Loaded format",
+      items = [
+         ('mono', "mono", "", 0),
+         ('stereo', "stereo", "", 1),
+         ('remain compressed', "remain compressed", "", 2)
       ])
 #}
 
 class CV_BONE_SETTINGS(bpy.types.PropertyGroup):
 #{
-   collider: bpy.props.BoolProperty(name="Collider",default=False)
+   collider: bpy.props.EnumProperty(
+      name="Collider Type", 
+      items = [
+      ('collider_none', "collider_none", "", 0),
+      ('collider_box', "collider_box", "", 1),
+      ('collider_capsule', "collider_capsule", "", 2),
+      ])
+
    v0: bpy.props.FloatVectorProperty(name="v0",size=3)
    v1: bpy.props.FloatVectorProperty(name="v1",size=3)
 
    con0: bpy.props.BoolProperty(name="Constriant 0",default=False)
    mins: bpy.props.FloatVectorProperty(name="mins",size=3)
    maxs: bpy.props.FloatVectorProperty(name="maxs",size=3)
+
+   conevx: bpy.props.FloatVectorProperty(name="conevx",size=3)
+   conevy: bpy.props.FloatVectorProperty(name="conevy",size=3)
+   coneva: bpy.props.FloatVectorProperty(name="coneva",size=3)
+   conet:  bpy.props.FloatProperty(name="conet")
 #}
 
 class CV_BONE_PANEL(bpy.types.Panel):
 #{
-   bl_label="Bone Config"
+   bl_label="[Skate Rift]"
    bl_idname="SCENE_PT_cv_bone"
    bl_space_type='PROPERTIES'
    bl_region_type='WINDOW'
@@ -2129,14 +2778,149 @@ class CV_BONE_PANEL(bpy.types.Panel):
 
       _.layout.label( text="Angle Limits" )
       _.layout.prop( bone.cv_data, "con0" )
-      _.layout.prop( bone.cv_data, "mins" )
-      _.layout.prop( bone.cv_data, "maxs" )
+
+      _.layout.prop( bone.cv_data, "conevx" )
+      _.layout.prop( bone.cv_data, "conevy" )
+      _.layout.prop( bone.cv_data, "coneva" )
+      _.layout.prop( bone.cv_data, "conet" )
    #}
 #}
 
 class CV_SCENE_SETTINGS(bpy.types.PropertyGroup):
 #{
    use_hidden: bpy.props.BoolProperty( name="use hidden", default=False )
+   export_dir: bpy.props.StringProperty( name="Export Dir", subtype='DIR_PATH' )
+#}
+
+class CV_COLLECTION_SETTINGS(bpy.types.PropertyGroup):
+#{
+   pack_textures: bpy.props.BoolProperty( name="Pack Textures", default=False )
+   animations:    bpy.props.BoolProperty( name="Export animation", default=True)
+#}
+
+class CV_MATERIAL_SETTINGS(bpy.types.PropertyGroup):
+#{
+   shader: bpy.props.EnumProperty( 
+      name="Format", 
+      items = [
+      ('standard',"standard","",0),
+      ('standard_cutout', "standard_cutout", "", 1),
+      ('terrain_blend', "terrain_blend", "", 2),
+      ('vertex_blend', "vertex_blend", "", 3),
+      ('water',"water","",4),
+      ])
+
+   surface_prop: bpy.props.EnumProperty(
+      name="Surface Property",
+      items = [
+      ('concrete','concrete','',0),
+      ('wood','wood','',1),
+      ('grass','grass','',2),
+      ('tiles','tiles','',3)
+      ])
+   
+   collision: bpy.props.BoolProperty( \
+         name="Collisions Enabled",\
+         default=True,\
+         description = "Can the player collide with this material"\
+   )
+   skate_surface: bpy.props.BoolProperty( \
+         name="Skate Surface", \
+         default=True,\
+         description = "Should the game try to target this surface?" \
+   )
+   grind_surface: bpy.props.BoolProperty( \
+         name="Grind Surface", \
+         default=False,\
+         description = "Grind face?" \
+   )
+   grow_grass: bpy.props.BoolProperty( \
+         name="Grow Grass", \
+         default=False,\
+         description = "Spawn grass sprites on this surface?" \
+   )
+   blend_offset: bpy.props.FloatVectorProperty( \
+         name="Blend Offset", \
+         size=2, \
+         default=Vector((0.5,0.0)),\
+         description="When surface is more than 45 degrees, add this vector " +\
+                     "to the UVs" \
+   )
+   sand_colour: bpy.props.FloatVectorProperty( \
+         name="Sand Colour",\
+         subtype='COLOR',\
+         min=0.0,max=1.0,\
+         default=Vector((0.79,0.63,0.48)),\
+         description="Blend to this colour near the 0 coordinate on UP axis"\
+   )
+   shore_colour: bpy.props.FloatVectorProperty( \
+         name="Shore Colour",\
+         subtype='COLOR',\
+         min=0.0,max=1.0,\
+         default=Vector((0.03,0.32,0.61)),\
+         description="Water colour at the shoreline"\
+   )
+   ocean_colour: bpy.props.FloatVectorProperty( \
+         name="Ocean Colour",\
+         subtype='COLOR',\
+         min=0.0,max=1.0,\
+         default=Vector((0.0,0.006,0.03)),\
+         description="Water colour in the deep bits"\
+   )
+#}
+
+class CV_MATERIAL_PANEL(bpy.types.Panel):
+#{
+   bl_label="Skate Rift material"
+   bl_idname="MATERIAL_PT_cv_material"
+   bl_space_type='PROPERTIES'
+   bl_region_type='WINDOW'
+   bl_context="material"
+   
+   def draw(_,context):
+   #{
+      active_object = bpy.context.active_object
+      if active_object == None: return
+      active_mat = active_object.active_material
+      if active_mat == None: return
+
+      info = material_info( active_mat )
+
+      if 'tex_diffuse' in info:
+      #{
+         _.layout.label( icon='INFO', \
+            text=F"{info['tex_diffuse'].name} will be compiled" )
+      #}
+
+      _.layout.prop( active_mat.cv_data, "shader" )
+      _.layout.prop( active_mat.cv_data, "surface_prop" )
+      _.layout.prop( active_mat.cv_data, "collision" )
+
+      if active_mat.cv_data.collision:
+         _.layout.prop( active_mat.cv_data, "skate_surface" )
+         _.layout.prop( active_mat.cv_data, "grind_surface" )
+         _.layout.prop( active_mat.cv_data, "grow_grass" )
+
+      if active_mat.cv_data.shader == "terrain_blend":
+      #{
+         box = _.layout.box()
+         box.prop( active_mat.cv_data, "blend_offset" )
+         box.prop( active_mat.cv_data, "sand_colour" )
+      #}
+      elif active_mat.cv_data.shader == "vertex_blend":
+      #{
+         box = _.layout.box()
+         box.label( icon='INFO', text="Uses vertex colours, the R channel" )
+         box.prop( active_mat.cv_data, "blend_offset" )
+      #}
+      elif active_mat.cv_data.shader == "water":
+      #{
+         box = _.layout.box()
+         box.label( icon='INFO', text="Depth scale of 16 meters" )
+         box.prop( active_mat.cv_data, "shore_colour" )
+         box.prop( active_mat.cv_data, "ocean_colour" )
+      #}
+   #}
 #}
 
 class CV_OBJ_PANEL(bpy.types.Panel):
@@ -2175,49 +2959,101 @@ class CV_OBJ_PANEL(bpy.types.Panel):
    #}
 #}
 
-class CV_INTERFACE(bpy.types.Panel):
+class CV_COMPILE(bpy.types.Operator):
 #{
-   bl_idname = "VIEW3D_PT_carve"
-   bl_label = "Carve"
-   bl_space_type = 'VIEW_3D'
-   bl_region_type = 'UI'
-   bl_category = "Carve"
+   bl_idname="carve.compile_all"
+   bl_label="Compile All"
 
-   def draw(_, context):
+   def execute(_,context):
    #{
-      layout = _.layout
-      layout.prop( context.scene.cv_data, "use_hidden")
-      layout.operator( "carve.compile_all" )
+      view_layer = bpy.context.view_layer
+      for col in view_layer.layer_collection.children["export"].children:
+         if not col.hide_viewport or bpy.context.scene.cv_data.use_hidden:
+            write_model( col.name )
+
+      return {'FINISHED'}
    #}
 #}
 
-def test_compile():
+class CV_COMPILE_THIS(bpy.types.Operator):
 #{
-   view_layer = bpy.context.view_layer
-   for col in view_layer.layer_collection.children["export"].children:
-      if not col.hide_viewport or bpy.context.scene.cv_data.use_hidden:
-         write_model( col.name )
+   bl_idname="carve.compile_this"
+   bl_label="Compile This collection"
+
+   def execute(_,context):
+   #{
+      col = bpy.context.collection
+      write_model( col.name )
+
+      return {'FINISHED'}
+   #}
 #}
 
-class CV_COMPILE(bpy.types.Operator):
+class CV_INTERFACE(bpy.types.Panel):
 #{
-   bl_idname="carve.compile_all"
-   bl_label="Compile All"
+   bl_idname = "VIEW3D_PT_carve"
+   bl_label = "Skate Rift"
+   bl_space_type = 'VIEW_3D'
+   bl_region_type = 'UI'
+   bl_category = "Skate Rift"
 
-   def execute(_,context):
+   def draw(_, context):
    #{
-      test_compile()
-      #cProfile.runctx("test_compile()",globals(),locals(),sort=1)
-      #for col in bpy.data.collections["export"].children:
-      #   write_model( col.name )
+      layout = _.layout
+      layout.prop( context.scene.cv_data, "export_dir" )
+      
+      col = bpy.context.collection
+      
+      found_in_export = False
+      export_count = 0
+      view_layer = bpy.context.view_layer
+      for c1 in view_layer.layer_collection.children["export"].children:
+      #{
+         if not c1.hide_viewport or bpy.context.scene.cv_data.use_hidden:
+            export_count += 1
 
-      return {'FINISHED'}
+         if c1.name == col.name:
+         #{
+            found_in_export = True
+         #}
+      #}
+
+      box = layout.box()
+      if found_in_export:
+      #{
+         box.label( text=col.name + ".mdl" )
+         box.prop( col.cv_data, "pack_textures" )
+         box.prop( col.cv_data, "animations" )
+         box.operator( "carve.compile_this" )
+      #}
+      else:
+      #{
+         row = box.row()
+         row.enabled=False
+         row.label( text=col.name )
+         box.label( text="This collection is not in the export group" )
+      #}
+
+      box = layout.box()
+      row = box.row()
+
+      split = row.split( factor = 0.3, align=True )
+      split.prop( context.scene.cv_data, "use_hidden", text="hidden" )
+      
+      row1 = split.row()
+      if export_count == 0:
+         row1.enabled=False
+      row1.operator( "carve.compile_all", \
+                        text=F"Compile all ({export_count} collections)" )
    #}
 #}
 
+
 classes = [CV_OBJ_SETTINGS,CV_OBJ_PANEL,CV_COMPILE,CV_INTERFACE,\
            CV_MESH_SETTINGS, CV_SCENE_SETTINGS, CV_BONE_SETTINGS,\
-           CV_BONE_PANEL]
+           CV_BONE_PANEL, CV_COLLECTION_SETTINGS, CV_COMPILE_THIS,\
+           CV_MATERIAL_SETTINGS, CV_MATERIAL_PANEL, CV_LIGHT_SETTINGS,\
+           CV_LIGHT_PANEL]
 
 def register():
 #{
@@ -2230,6 +3066,11 @@ def register():
    bpy.types.Mesh.cv_data = bpy.props.PointerProperty(type=CV_MESH_SETTINGS)
    bpy.types.Scene.cv_data = bpy.props.PointerProperty(type=CV_SCENE_SETTINGS)
    bpy.types.Bone.cv_data = bpy.props.PointerProperty(type=CV_BONE_SETTINGS)
+   bpy.types.Collection.cv_data = \
+         bpy.props.PointerProperty(type=CV_COLLECTION_SETTINGS)
+   bpy.types.Material.cv_data = \
+         bpy.props.PointerProperty(type=CV_MATERIAL_SETTINGS)
+   bpy.types.Light.cv_data = bpy.props.PointerProperty(type=CV_LIGHT_SETTINGS)
 
    cv_view_draw_handler = bpy.types.SpaceView3D.draw_handler_add(\
       cv_draw,(),'WINDOW','POST_VIEW')
@@ -2244,3 +3085,187 @@ def unregister():
 
    bpy.types.SpaceView3D.draw_handler_remove(cv_view_draw_handler,'WINDOW')
 #}
+
+# ---------------------------------------------------------------------------- #
+#                                                                              #
+#                                 QOI encoder                                  #
+#                                                                              #
+# ---------------------------------------------------------------------------- #
+#                                                                              #
+# Transliteration of:                                                          #
+#    https://github.com/phoboslab/qoi/blob/master/qoi.h                        #
+#                                                                              #
+# Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org               #
+# SPDX-License-Identifier: MIT                                                 #
+# QOI - The "Quite OK Image" format for fast, lossless image compression       #
+#                                                                              #
+# ---------------------------------------------------------------------------- #
+
+class qoi_rgba_t(Structure):
+#{
+   _pack_ = 1
+   _fields_ = [("r",c_uint8),
+               ("g",c_uint8),
+               ("b",c_uint8),
+               ("a",c_uint8)]
+#}
+
+QOI_OP_INDEX  = 0x00 # 00xxxxxx
+QOI_OP_DIFF   = 0x40 # 01xxxxxx
+QOI_OP_LUMA   = 0x80 # 10xxxxxx
+QOI_OP_RUN    = 0xc0 # 11xxxxxx
+QOI_OP_RGB    = 0xfe # 11111110
+QOI_OP_RGBA   = 0xff # 11111111
+
+QOI_MASK_2    = 0xc0 # 11000000
+
+def qoi_colour_hash( c ):
+#{
+   return c.r*3 + c.g*5 + c.b*7 + c.a*11
+#}
+
+def qoi_eq( a, b ):
+#{
+   return (a.r==b.r) and (a.g==b.g) and (a.b==b.b) and (a.a==b.a)
+#}
+
+def qoi_32bit( v ):
+#{
+   return bytearray([ (0xff000000 & v) >> 24, \
+                      (0x00ff0000 & v) >> 16, \
+                      (0x0000ff00 & v) >> 8, \
+                      (0x000000ff & v) ])
+#}
+
+def qoi_encode( img ):
+#{
+   data = bytearray()
+   
+   print(F"            . Encoding {img.name}.qoi[{img.size[0]},{img.size[1]}]")
+
+   index = [ qoi_rgba_t() for _ in range(64) ]
+
+   # Header
+   #
+   data.extend( bytearray(c_uint32(0x66696f71)) )
+   data.extend( qoi_32bit( img.size[0] ) )
+   data.extend( qoi_32bit( img.size[1] ) )
+   data.extend( bytearray(c_uint8(4)) )
+   data.extend( bytearray(c_uint8(0)) )
+
+   run = 0
+   px_prev = qoi_rgba_t()
+   px_prev.r = c_uint8(0)
+   px_prev.g = c_uint8(0)
+   px_prev.b = c_uint8(0)
+   px_prev.a = c_uint8(255)
+
+   px = qoi_rgba_t()
+   px.r = c_uint8(0)
+   px.g = c_uint8(0)
+   px.b = c_uint8(0)
+   px.a = c_uint8(255)
+
+   px_len = img.size[0] * img.size[1]
+
+   paxels = [ int(min(max(_,0),1)*255) for _ in img.pixels ]
+
+   for px_pos in range( px_len ):
+   #{
+      idx = px_pos * img.channels
+      nc = img.channels-1
+
+      px.r = paxels[idx+min(0,nc)]
+      px.g = paxels[idx+min(1,nc)]
+      px.b = paxels[idx+min(2,nc)]
+      px.a = paxels[idx+min(3,nc)]
+
+      if qoi_eq( px, px_prev ):
+      #{
+         run += 1
+
+         if (run == 62) or (px_pos == px_len-1):
+         #{
+            data.extend( bytearray( c_uint8(QOI_OP_RUN | (run-1))) )
+            run = 0
+         #}
+      #}
+      else:
+      #{
+         if run > 0:
+         #{
+            data.extend( bytearray( c_uint8(QOI_OP_RUN | (run-1))) )
+            run = 0
+         #}
+
+         index_pos = qoi_colour_hash(px) % 64
+
+         if qoi_eq( index[index_pos], px ):
+         #{
+            data.extend( bytearray( c_uint8(QOI_OP_INDEX | index_pos)) )
+         #}
+         else:
+         #{
+            index[ index_pos ].r = px.r
+            index[ index_pos ].g = px.g
+            index[ index_pos ].b = px.b
+            index[ index_pos ].a = px.a
+
+            if px.a == px_prev.a:
+            #{
+               vr = int(px.r) - int(px_prev.r)
+               vg = int(px.g) - int(px_prev.g)
+               vb = int(px.b) - int(px_prev.b)
+
+               vg_r = vr - vg
+               vg_b = vb - vg
+
+               if (vr > -3) and (vr < 2) and\
+                  (vg > -3) and (vg < 2) and\
+                  (vb > -3) and (vb < 2):
+               #{
+                  op = QOI_OP_DIFF | (vr+2) << 4 | (vg+2) << 2 | (vb+2)
+                  data.extend( bytearray( c_uint8(op) ))
+               #}
+               elif (vg_r > -9) and (vg_r < 8) and\
+                    (vg  > -33) and (vg < 32 ) and\
+                    (vg_b > -9) and (vg_b < 8):
+               #{
+                  op = QOI_OP_LUMA | (vg+32)
+                  delta = (vg_r+8) << 4 | (vg_b + 8)
+                  data.extend( bytearray( c_uint8(op) ) )
+                  data.extend( bytearray( c_uint8(delta) ))
+               #}
+               else:
+               #{
+                  data.extend( bytearray( c_uint8(QOI_OP_RGB) ) )
+                  data.extend( bytearray( c_uint8(px.r) ))
+                  data.extend( bytearray( c_uint8(px.g) ))
+                  data.extend( bytearray( c_uint8(px.b) ))
+               #}
+            #}
+            else:
+            #{
+               data.extend( bytearray( c_uint8(QOI_OP_RGBA) ) )
+               data.extend( bytearray( c_uint8(px.r) ))
+               data.extend( bytearray( c_uint8(px.g) ))
+               data.extend( bytearray( c_uint8(px.b) ))
+               data.extend( bytearray( c_uint8(px.a) ))
+            #}
+         #}
+      #}
+
+      px_prev.r = px.r
+      px_prev.g = px.g
+      px_prev.b = px.b
+      px_prev.a = px.a
+   #}
+   
+   # Padding
+   for i in range(7):
+      data.extend( bytearray( c_uint8(0) ))
+   data.extend( bytearray( c_uint8(1) ))
+   bytearray_align_to( data, 16, 0 )
+
+   return data
+#}