diff --git a/src/bundle.nim b/src/bundle.nim
index f973f26..833cabc 100644
--- a/src/bundle.nim
+++ b/src/bundle.nim
@@ -47,8 +47,10 @@ import ./incomplete
 import ./input
 import ./loaders/blend
 import ./loaders/loader_base
+import ./modifiers/armature_deform
 import ./modifiers/shape_keys
 import ./myou_engine
+import ./objects/armature
 import ./objects/camera
 import ./objects/cubemap_probe
 import ./objects/gameobject
@@ -64,6 +66,8 @@ import ./util
 import std/tables
 import vmath except Quat, quat
 
+export armature
+export armature_deform
 export attributes
 export blend
 export camera
diff --git a/src/incomplete.nim b/src/incomplete.nim
index 3bfe7e0..0574826 100644
--- a/src/incomplete.nim
+++ b/src/incomplete.nim
@@ -10,12 +10,4 @@ proc newWorld*(scene: Scene): World = discard
 proc destroy*(this: Body) = discard
 proc destroy*(this: World) = discard
 
-proc recalculate_bone_matrices*(this: Armature) = discard
-
 proc render*(this: PlanarProbe, v: Viewport) = discard
-
-proc newArmature*(engine: MyouEngine, name: string="", scene: Scene=nil): Armature =
-    result = new Armature
-    result.engine = engine
-    result.name = name
-
diff --git a/src/modifiers/armature_deform.nim b/src/modifiers/armature_deform.nim
new file mode 100644
index 0000000..f12686b
--- /dev/null
+++ b/src/modifiers/armature_deform.nim
@@ -0,0 +1,81 @@
+# The contents of this file are subject to the Common Public Attribution License
+# Version 1.0 (the “License”); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+# https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla
+# Public License Version 1.1 but Sections 14 and 15 have been added to cover use
+# of software over a computer network and provide for limited attribution for
+# the Original Developer. In addition, Exhibit A has been modified to be
+# consistent with Exhibit B.
+#
+# Software distributed under the License is distributed on an “AS IS” basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+# the specific language governing rights and limitations under the License.
+#
+# The Original Code is Myou Engine.
+#
+# the Original Developer is the Initial Developer.
+#
+# The Initial Developer of the Original Code is the Myou Engine developers.
+# All portions of the code written by the Myou Engine developers are Copyright
+# (c) 2024. All Rights Reserved.
+#
+# Alternatively, the contents of this file may be used under the terms of the
+# GNU Affero General Public License version 3 (the [AGPL-3] License), in which
+# case the provisions of [AGPL-3] License are applicable instead of those above.
+#
+# If you wish to allow use of your version of this file only under the terms of
+# the [AGPL-3] License and not to allow others to use your version of this file
+# under the CPAL, indicate your decision by deleting the provisions above and
+# replace them with the notice and other provisions required by the [AGPL-3]
+# License. If you do not delete the provisions above, a recipient may use your
+# version of this file under either the CPAL or the [AGPL-3] License.
+
+import ../types
+import ../graphics/ubo
+import std/strformat
+import vmath except Quat, quat
+import ../quat
+
+proc newArmatureModifier*(engine: MyouEngine, num_bones: int): VertexModifier =
+    doAssert num_bones <= 256, "More than 256 bones not yet supported"
+
+    result.get_code = proc(v: seq[Varying]): VertexModifierCodeLines =
+        result.uniform_lines = @[
+            "layout(std140) uniform ArmatureBones {",
+            &"    mat4 bones[{num_bones}];",
+            "};",
+        ]
+        result.body_lines = @[
+            "vec4 blendco = vec4(0.0);",
+            "vec3 blendnor = vec3(0.0);",
+            "mat4 m; float w;",
+            "ivec4 inds = ivec4(b_indices);",
+            "for(int i=0;i<4;i++){",
+            "    m = bones[inds[i]];",
+            &"    w = weights[i] * {1/255};",
+            "    blendco += m * co * w;",
+            "    blendnor += mat3(m) * normal * w;",
+            "}",
+            "co = blendco; normal = blendnor;",
+        ]
+
+    
+    let ubo = engine.renderer.newUBO("ArmatureBones", Mat4, num_bones)
+    result.ubo = ubo
+    
+    result.update = proc(mesh: Mesh) =
+        doAssert mesh.armature != nil, "Mesh has no armature assigned"
+        doAssert mesh.armature.deform_bones.len <= num_bones,
+            "Armature has more deform bones than specified in vertex modifier"
+        var data = ubo.storage(Mat4)
+
+        # TODO: apply armature in world space or camera space 
+        # instead of converting back and forth several times
+        # TODO: apply mesh transform and detect when it's the case
+        # TODO: 3x4 matrices, dual quaternions
+        let rel = mesh.armature.world_matrix.inverse * mesh.world_matrix
+        let rel_inv = rel.inverse
+        for i,bone in mesh.armature.deform_bones:
+            data[i] = rel_inv * bone.ol_matrix * rel
+        ubo.update()
+
diff --git a/src/objects/armature.nim b/src/objects/armature.nim
new file mode 100644
index 0000000..8e2774f
--- /dev/null
+++ b/src/objects/armature.nim
@@ -0,0 +1,189 @@
+# The contents of this file are subject to the Common Public Attribution License
+# Version 1.0 (the “License”); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+# https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla
+# Public License Version 1.1 but Sections 14 and 15 have been added to cover use
+# of software over a computer network and provide for limited attribution for
+# the Original Developer. In addition, Exhibit A has been modified to be
+# consistent with Exhibit B.
+#
+# Software distributed under the License is distributed on an “AS IS” basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+# the specific language governing rights and limitations under the License.
+#
+# The Original Code is Myou Engine.
+#
+# the Original Developer is the Initial Developer.
+#
+# The Initial Developer of the Original Code is the Myou Engine developers.
+# All portions of the code written by the Myou Engine developers are Copyright
+# (c) 2024. All Rights Reserved.
+#
+# Alternatively, the contents of this file may be used under the terms of the
+# GNU Affero General Public License version 3 (the [AGPL-3] License), in which
+# case the provisions of [AGPL-3] License are applicable instead of those above.
+#
+# If you wish to allow use of your version of this file only under the terms of
+# the [AGPL-3] License and not to allow others to use your version of this file
+# under the CPAL, indicate your decision by deleting the provisions above and
+# replace them with the notice and other provisions required by the [AGPL-3]
+# License. If you do not delete the provisions above, a recipient may use your
+# version of this file under either the CPAL or the [AGPL-3] License.
+
+import ../types
+import std/options
+import std/tables
+import vmath except Quat, quat
+import ../quat
+import ../util
+
+when defined(nimdoc):
+    type TYPES* = Armature | Bone | Constraint
+
+# Forward declarations
+proc newArmature*(engine: MyouEngine, name: string="armature", scene: Scene = nil): Armature
+proc add_bone*(self: Armature, name: string, position: Vec3, rotation: Quat, deform_id: int, parent_name: string, blength = 0.0): Bone {.discardable.}
+proc add_constraint*(self: Armature; constraint: Constraint)
+proc update_rest_matrices*(self: Armature)
+proc recalculate_bone_matrices*(self: Armature, use_constraints: bool = true)
+# End forward declarations
+
+method is_armature*(self: GameObject): bool {.base.} =
+    ## Return whether a GameObject is an armature
+    return false
+method get_armature*(self: GameObject): Armature {.base.} =
+    ## Get the Armature object of a GameObject, or nil if it's not an armature
+    return nil
+method is_armature*(self: Armature): bool =
+    ## Return whether a GameObject is an armature
+    return true
+method get_armature*(self: Armature): Armature =
+    ## Get the Armature object of a GameObject, or nil if it's not an armature
+    return self
+
+import ./gameobject
+
+proc newBone(): Bone =
+    let self = new Bone
+    self.base_rotation = quat()
+    self.rotation = quat()
+    self.scale = vec3(1, 1, 1)
+    self.final_scale = vec3(1, 1, 1)
+    self.matrix = mat4()
+    self.ol_matrix = mat4()
+    self.inv_rest_matrix = mat4()
+    self.deform_id = -1
+    self.blength = 1
+    return self
+
+proc clone_to(self: Bone, new_armature: Armature): Bone =
+    var n = newBone()
+    n[] = self[]
+    if self.parent != nil:
+        n.parent = new_armature.bone_list[self.parent.index]
+    n.object_children.setLen 0
+    return n
+
+proc head*(self: Bone): Vec3 =
+    # NOTE: assuming matrix is already calculated
+    self.matrix[3].xyz
+
+proc tail*(self: Bone): Vec3 =
+    # NOTE: assuming matrix is already calculated
+    self.matrix * vec3(0, self.blength, 0)
+
+proc center*(self: Bone): Vec3 =
+    # NOTE: assuming matrix is already calculated
+    self.matrix * vec3(0, self.blength/2, 0)
+
+proc dist_to_sphere*(self: Bone, point: Vec3): float32 =
+    # NOTE: assuming matrix is already calculated
+    dist(self.matrix * vec3(0, self.blength/2, 0), point) - self.blength/2
+
+proc newArmature*(engine: MyouEngine, name: string="armature", scene: Scene = nil): Armature =
+    ## Create a new Armature object. If you supply `scene` it will be added to that
+    ## scene.
+    var self = new Armature
+    discard procCall(self.GameObject.initGameObject(engine, name, scene))
+    return self
+
+
+# for now we will add bones, then update inv_rest_matrix of all
+var dedup = 0
+
+proc add_bone*(self: Armature,
+                name: string,
+                position: Vec3,
+                rotation: Quat,
+                deform_id: int,
+                parent_name: string,
+                blength = 0.0,
+            ): Bone {.discardable.} =
+    var bone = newBone()
+    var n = name
+    while n in self.bones:
+        dedup.inc
+        n = name & "$" & $dedup
+    bone.base_position = position
+    bone.base_rotation = rotation
+    if deform_id != -1:
+        bone.deform_id = deform_id
+        if self.deform_bones.len <= deform_id:
+            self.deform_bones.setLen 1 shl ceil(log2(float(deform_id+1))).int
+        self.deform_bones[deform_id] = bone
+    if parent_name != "":
+        bone.parent = self.bones[parent_name]
+    #bone.parent_matrix = bone.parent.matrix
+    bone.blength = blength
+    bone.index = self.bone_list.len
+    self.bone_list.add(bone)
+    self.bones[n] = bone
+    return bone
+
+proc add_constraint*(self: Armature; constraint: Constraint) =
+    discard
+    # self.bones[constraint.owner].constraints.add constraint
+
+proc update_rest_matrices*(self: Armature) =
+    # TODO: set rest pose first
+    self.recalculate_bone_matrices(use_constraints = false)
+    for bone in self.bone_list:
+        bone.inv_rest_matrix = inverse(bone.matrix)
+
+proc recalculate_bone_matrices*(self: Armature, use_constraints: bool = true) =
+    let inv = self.get_world_matrix.inverse
+    # update final position/rotation/scale
+    for bone in self.bone_list:
+        if bone.parent_object != nil:
+            continue
+        var pos = bone.base_position + bone.base_rotation * bone.position
+        var rot = bone.base_rotation * bone.rotation.normalize
+        var scl = bone.scale
+        let parent = bone.parent
+        if parent != nil:
+            scl = parent.final_scale * scl
+            rot = parent.final_rotation * rot
+            pos = pos * parent.final_scale
+            pos = parent.final_rotation * pos
+            pos = pos + parent.final_position
+        bone.final_position = pos
+        bone.final_rotation = rot
+        bone.final_scale = scl
+        if use_constraints:
+            for con in bone.constraints:
+                con.apply(con)
+    # update matrix
+    for bone in self.bone_list:
+        let ob = bone.parent_object
+        if ob == nil:
+            bone.matrix = scale(bone.final_scale) *
+                translate(bone.final_position) *
+                bone.final_rotation.to_mat4
+        else:
+            bone.matrix = inv * ob.world_matrix * ob.bone_matrix_inverse
+            # TODO: scale?
+            # TODO: this is probably what it's messinig with the rotation
+            # if the scale is not taken in account, rotation is messed up and we have to normalize
+            bone.final_position = bone.matrix[3].xyz
+            bone.final_rotation = bone.matrix.to_quat.normalize
+        bone.ol_matrix = bone.matrix * bone.inv_rest_matrix
diff --git a/src/objects/gameobject.nim b/src/objects/gameobject.nim
index d10fae9..3eaacda 100644
--- a/src/objects/gameobject.nim
+++ b/src/objects/gameobject.nim
@@ -103,12 +103,7 @@ proc set_world_size*(self: GameObject, size: SomeFloat)
 proc set_name*(self: GameObject, name: string)
 proc local_to_world*(self: GameObject, point: Vec3): Vec3
 proc world_to_local*(self: GameObject, point: Vec3): Vec3
-
-method is_armature*(self: GameObject): bool {.base.} =
-    return false
-method get_armature*(self: GameObject): Armature {.base.} =
-    return nil
-# End forward declarations and ob type methods
+# End forward declarations
 
 import json
 import std/math
@@ -118,6 +113,7 @@ import ../util
 import ../graphics/ubo
 import ../scene
 import ./mesh
+import ./armature
 
 proc initGameObject*(self: GameObject, engine: MyouEngine, name: string="", scene: Scene=nil): GameObject =
     # Remember to add any new mutable reference to clone()
diff --git a/src/quat.nim b/src/quat.nim
index 772554b..619bb79 100644
--- a/src/quat.nim
+++ b/src/quat.nim
@@ -52,7 +52,7 @@ func swap_quat_handness*[T](q: GQuat[T]): GQuat[T] =
     # TODO: rotate 90 to not change up-ness?
     GQuat[T](x: -q.x, y: -q.z, z: -q.y, w: q.w)
 
-func to_quat*[T](m: GMat3[T]): GQuat[T] =
+func to_quat*[T](m: GMat3[T]|GMat4[T]): GQuat[T] =
     # http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
     let trace = m[0,0] + m[1,1] + m[2,2]
     if trace > 0:
@@ -92,7 +92,7 @@ proc normalize*[T](q: GQuat[T]): GQuat[T] =
         return q
     return gquat[T](q.x / length , q.y / length, q.z / length, q.w / length)
 
-func to_mat3*[T](q: GQuat[T]): GMat3[T] =
+func to_mat[T,U](q: GQuat[T]): U =
     let xx = q.x * q.x * 2
     let yx = q.y * q.x * 2
     let yy = q.y * q.y * 2
@@ -114,6 +114,14 @@ func to_mat3*[T](q: GQuat[T]): GMat3[T] =
     result[0,2] = zx - wy
     result[1,2] = zy + wx
     result[2,2] = 1 - xx - yy
+    when U is GMat4[T]:
+        result[3,3] = 1
+
+template to_mat3*[T](q: GQuat[T]): GMat3[T] =
+    to_mat[T,GMat3[T]](q)
+
+template to_mat4*[T](q: GQuat[T]): GMat4[T] =
+    to_mat[T,GMat4[T]](q)
 
 
 func `*`*[T](q: GQuat[T], a: GVec3[T]): GVec3[T] =
@@ -267,4 +275,3 @@ proc rotationTo*(a,b: Vec3): Quat =
     else:
         let v = cross(a, b)
         return quat(v.x, v.y, v.z, 1.0+dot).normalize
-
diff --git a/src/types.nim b/src/types.nim
index 75558a4..2663c74 100644
--- a/src/types.nim
+++ b/src/types.nim
@@ -321,6 +321,36 @@ type
         tex_size*: float32
         bias*: float32
         pad0, pad1: float32
+
+    # armature.nim
+
+    Armature* = ref object of GameObject
+        bone_list*: seq[Bone]
+        deform_bones*: seq[Bone]
+        bones*: Table[string, Bone]
+    
+    Bone* = ref object
+        base_position*: Vec3
+        base_rotation*: Quat
+        position*: Vec3
+        rotation*: Quat
+        scale*: Vec3
+        final_position*: Vec3
+        final_rotation*: Quat
+        final_scale*: Vec3
+        matrix*: Mat4
+        ol_matrix*: Mat4
+        parent*: Bone
+        index*: int
+        inv_rest_matrix*: Mat4
+        deform_id*: int
+        blength*: float32
+        constraints*: seq[Constraint]
+        object_children*: seq[GameObject]
+        parent_object*: GameObject
+    
+    Constraint* = ref object
+        apply*: proc(self: Constraint)
     
     # scene.nim
 
@@ -873,15 +903,6 @@ type
 
     # INCOMPLETE
 
-    Armature* = ref object of GameObject
-        bone_list*: seq[Bone]
-        bones*: Table[string, Bone]
-    Bone = ref object
-        blength*: float
-        object_children*: seq[GameObject]
-        matrix*: Mat4
-        parent_object*: GameObject
-
     World* = ref object of RootObj
         enabled*: bool