Armature: Add initial Armature object, Bones, armature deform vertex modifier.

This commit is contained in:
Alberto Torres 2025-03-18 18:42:39 +01:00
parent ba70b1addb
commit 427c458fcf
7 changed files with 316 additions and 26 deletions

View file

@ -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

View file

@ -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

View file

@ -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()

189
src/objects/armature.nim Normal file
View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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