changeset 288:5af702c95b16

add: what seems like a working animation system, atm integrated with entities, will add more for meshes
author Sam <sam@basx.dev>
date Wed, 14 Jun 2023 22:55:00 +0700
parents 21d6d20de754
children 0eef4eba9c17
files src/semicongine.nim src/semicongine/animation.nim src/semicongine/core/matrix.nim src/semicongine/engine.nim src/semicongine/renderer.nim src/semicongine/resources/mesh.nim src/semicongine/scene.nim src/semicongine/text.nim
diffstat 8 files changed, 279 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine.nim	Sat Jun 10 00:31:51 2023 +0700
+++ b/src/semicongine.nim	Wed Jun 14 22:55:00 2023 +0700
@@ -2,6 +2,7 @@
 export core
 
 import semicongine/audio
+import semicongine/animation
 import semicongine/engine
 import semicongine/collision
 import semicongine/scene
@@ -15,6 +16,7 @@
 import semicongine/vulkan
 
 export audio
+export animation
 export engine
 export collision
 export scene
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/semicongine/animation.nim	Wed Jun 14 22:55:00 2023 +0700
@@ -0,0 +1,156 @@
+import std/tables
+import std/math
+import std/sequtils
+import std/strformat
+import std/algorithm
+
+import ./scene
+import ./core/matrix
+
+type
+  Ease* = enum
+    None
+    Linear
+    Pow2
+    Pow3
+    Pow4
+    Pow5
+    Expo
+    Sine
+    Circ
+  AnimationTime = 0'f32 .. 1'f32
+  Direction* = enum
+    Forward
+    Backward
+    Alternate
+  Keyframe[T] = object
+    timestamp: AnimationTime
+    value : T
+    easeIn: Ease
+    easeOut: Ease
+  Animation*[T] = object
+    keyframes*: seq[Keyframe[T]]
+    duration: float32
+    direction: Direction
+    iterations: int
+  AnimationPlayer*[T] = object
+    animation*: Animation[T]
+    currentTime: float32
+    playing: bool
+    currentDirection: int
+    currentIteration: int
+    currentValue*: T
+
+func easeConst(x: float32): float32 = 0
+func easeLinear(x: float32): float32 = x
+func easePow2(x: float32): float32 = x * x
+func easePow3(x: float32): float32 = x * x * x
+func easePow4(x: float32): float32 = x * x * x * x
+func easePow5(x: float32): float32 = x * x * x * x * x
+func easeExpo(x: float32): float32 = ( if x == 0: 0'f32 else: pow(2'f32, 10'f32 * x - 10'f32) )
+func easeSine(x: float32): float32 = 1'f32 - cos((x * PI) / 2'f32)
+func easeCirc(x: float32): float32 = 1'f32 - sqrt(1'f32 - pow(x, 2'f32))
+
+func `$`*(animation: Animation): string =
+  &"{animation.keyframes.len} keyframes, {animation.duration}s"
+
+const EASEFUNC_MAP = {
+    None: easeConst,
+    Linear: easeLinear,
+    Pow2: easePow2,
+    Pow3: easePow3,
+    Pow4: easePow4,
+    Pow5: easePow5,
+    Expo: easeExpo,
+    Sine: easeSine,
+    Circ: easeCirc,
+}.toTable()
+
+func makeEaseOut(f: proc(x: float32): float32 {.noSideEffect.}): auto =
+  func wrapper(x: float32): float32 =
+    1 - f(1 - x)
+  return wrapper
+
+func combine(f1: proc(x: float32): float32 {.noSideEffect.}, f2: proc(x: float32): float32 {.noSideEffect.}): auto =
+  func wrapper(x: float32): float32 =
+    if x < 0.5: f1(x * 2) * 0.5
+    else: f2((x - 0.5) * 2) * 0.5 + 0.5
+  return wrapper
+
+func interpol(keyframe: Keyframe, t: float32): float32 =
+  if keyframe.easeOut == None:
+    return EASEFUNC_MAP[keyframe.easeIn](t)
+  elif keyframe.easeIn == None:
+    return EASEFUNC_MAP[keyframe.easeOut](t)
+  else:
+    return combine(EASEFUNC_MAP[keyframe.easeIn], makeEaseOut(EASEFUNC_MAP[keyframe.easeOut]))(t)
+
+func keyframe*[T](timestamp: AnimationTime, value: T, easeIn=Linear, easeOut=None): Keyframe[T] =
+  Keyframe[T](timestamp: timestamp, value: value, easeIn: easeIn, easeOut: easeOut)
+
+func newAnimation*[T](keyframes: openArray[Keyframe[T]], duration: float32, direction=Forward, iterations=1): Animation[T] =
+  assert keyframes.len >= 2, "An animation needs at least 2 keyframes"
+  assert keyframes[0].timestamp == 0, "An animation's first keyframe needs to have timestamp=0"
+  assert keyframes[^1].timestamp == 1, "An animation's last keyframe needs to have timestamp=1"
+  var last = keyframes[0].timestamp
+  for kf in keyframes[1 .. ^1]:
+    assert kf.timestamp > last, "Succeding keyframes must have increasing timestamps"
+    last = kf.timestamp
+
+  Animation[T](
+    keyframes: keyframes.toSeq,
+    duration: duration,
+    direction: direction,
+    iterations: iterations
+  )
+
+func valueAt[T](animation: Animation[T], timestamp: AnimationTime): T =
+  var i = 0
+  while i < animation.keyframes.len - 1:
+    if animation.keyframes[i].timestamp > timestamp:
+      break
+    inc i
+
+  let
+    keyFrameDist = animation.keyframes[i].timestamp - animation.keyframes[i - 1].timestamp
+    timestampDist = timestamp - animation.keyframes[i - 1].timestamp
+    x = timestampDist / keyFrameDist
+
+  let newX = animation.keyframes[i - 1].interpol(x)
+  return animation.keyframes[i].value * newX + animation.keyframes[i - 1].value * (1 - newX)
+
+func resetPlayer*(player: var AnimationPlayer) =
+  player.currentTime = 0
+  player.currentDirection = if player.animation.direction == Backward: -1 else : 1
+  player.currentIteration = player.animation.iterations
+
+func newAnimator*[T](animation: Animation[T]): AnimationPlayer[T] =
+  result = AnimationPlayer[T]( animation: animation, playing: false,)
+  result.resetPlayer()
+
+func start*(player: var AnimationPlayer) =
+  player.playing = true
+
+func stop*(player: var AnimationPlayer) =
+  player.playing = false
+
+func advance*[T](player: var AnimationPlayer[T], dt: float32) =
+  # TODO: check, not 100% correct I think
+  if player.playing:
+    player.currentTime += float32(player.currentDirection) * dt
+    if abs(player.currentTime) > player.animation.duration:
+      dec player.currentIteration
+      if player.currentIteration <= 0 and player.animation.iterations != 0:
+        player.stop()
+        player.resetPlayer()
+      else:
+        case player.animation.direction:
+          of Forward:
+            player.currentTime = player.currentTime - player.animation.duration
+          of Backward:
+            player.currentTime = player.currentTime + player.animation.duration
+          of Alternate:
+            player.currentDirection = -player.currentDirection
+            player.currentTime += float32(player.currentDirection) * dt * 2'f32
+
+  player.currentValue = valueAt(player.animation, abs(player.currentTime) / player.animation.duration)
--- a/src/semicongine/core/matrix.nim	Sat Jun 10 00:31:51 2023 +0700
+++ b/src/semicongine/core/matrix.nim	Wed Jun 14 22:55:00 2023 +0700
@@ -101,6 +101,14 @@
   elif m is TMat34: 4
   elif m is TMat43: 3
   elif m is TMat4: 4
+template matlen*(m: typedesc): int =
+  when m is TMat2: 4
+  elif m is TMat23: 6
+  elif m is TMat32: 6
+  elif m is TMat3: 9
+  elif m is TMat34: 12
+  elif m is TMat43: 12
+  elif m is TMat4: 16
 
 
 func toString[T](value: T): string =
@@ -133,7 +141,9 @@
 func `$`*(v: TMat4[SomeNumber]): string = toString[TMat4[SomeNumber]](v)
 
 func `[]`*[T: TMat](m: T, row, col: int): auto = m.data[col + row * T.columnCount]
-proc `[]=`*[T: TMat, U](m: var T, row, col: int, value: U) = m.data[col + row * T.columnCount] = value
+func `[]=`*[T: TMat, U](m: var T, row, col: int, value: U) = m.data[col + row * T.columnCount] = value
+func `[]`*[T: TMat](m: T, i: int): auto = m.data[i]
+func `[]=`*[T: TMat, U](m: var T, i: int, value: U) = m.data[i] = value
 
 func row*[T: TMat2](m: T, i: 0..1): auto = TVec2([m[i, 0], m[i, 1]])
 func row*[T: TMat32](m: T, i: 0..2): auto = TVec2([m[i, 0], m[i, 1]])
@@ -175,6 +185,27 @@
     procType=nnkFuncDef,
   )
 
+proc createMatMatAdditionOperator(theType: typedesc): NimNode =
+  var data = nnkBracket.newTree()
+  for i in 0 ..< matlen(theType):
+    data.add(
+      infix(
+        nnkBracketExpr.newTree(ident("a"), newLit(i)),
+        "+",
+        nnkBracketExpr.newTree(ident("b"), newLit(i)),
+    ))
+
+  return newProc(
+    postfix(nnkAccQuoted.newTree(ident("+")), "*"),
+    params=[
+      ident("auto"),
+      newIdentDefs(ident("a"), ident(theType.name)),
+      newIdentDefs(ident("b"), ident(theType.name))
+    ],
+    body=nnkObjConstr.newTree(ident(theType.name), nnkExprColonExpr.newTree(ident("data"), data)),
+    procType=nnkFuncDef,
+  )
+
 proc createVecMatMultiplicationOperator(matType: typedesc, vecType: typedesc): NimNode =
   var data = nnkBracket.newTree()
   for i in 0 ..< matType.rowCount:
@@ -276,6 +307,14 @@
   result.add(createMatMatMultiplicationOperator(TMat4, TMat43, TMat43))
   result.add(createMatMatMultiplicationOperator(TMat4, TMat4, TMat4))
 
+  result.add(createMatMatAdditionOperator(TMat2))
+  result.add(createMatMatAdditionOperator(TMat23))
+  result.add(createMatMatAdditionOperator(TMat32))
+  result.add(createMatMatAdditionOperator(TMat3))
+  result.add(createMatMatAdditionOperator(TMat34))
+  result.add(createMatMatAdditionOperator(TMat43))
+  result.add(createMatMatAdditionOperator(TMat4))
+
   result.add(createVecMatMultiplicationOperator(TMat2, TVec2))
   result.add(createVecMatMultiplicationOperator(TMat3, TVec3))
   result.add(createVecMatMultiplicationOperator(TMat4, TVec4))
--- a/src/semicongine/engine.nim	Sat Jun 10 00:31:51 2023 +0700
+++ b/src/semicongine/engine.nim	Wed Jun 14 22:55:00 2023 +0700
@@ -21,7 +21,6 @@
     Starting
     Running
     Shutdown
-    Destroyed
   Input = object
     keyIsDown: set[Key]
     keyWasPressed: set[Key]
@@ -56,7 +55,6 @@
     engine.debugger.destroy()
   engine.window.destroy()
   engine.instance.destroy()
-  engine.state = Destroyed
 
 
 proc initEngine*(
@@ -104,13 +102,11 @@
   startMixerThread()
 
 proc setRenderer*(engine: var Engine, renderPass: RenderPass) =
-  assert engine.state != Destroyed
   if engine.renderer.isSome:
     engine.renderer.get.destroy()
   engine.renderer = some(engine.device.initRenderer(renderPass))
 
 proc addScene*(engine: var Engine, scene: Scene, vertexInput: seq[ShaderAttribute], samplers: seq[ShaderAttribute], transformAttribute="transform") =
-  assert engine.state != Destroyed
   assert transformAttribute == "" or transformAttribute in map(vertexInput, proc(a: ShaderAttribute): string = a.name)
   assert engine.renderer.isSome
   engine.renderer.get.setupDrawableBuffers(scene, vertexInput, samplers, transformAttribute=transformAttribute)
@@ -122,6 +118,11 @@
   engine.renderer.get.updateUniformData(scene)
   engine.renderer.get.render(scene)
 
+proc updateAnimations*(engine: var Engine, scene: var Scene, dt: float32) =
+  assert engine.state == Running
+  assert engine.renderer.isSome
+  engine.renderer.get.updateAnimations(scene, dt)
+
 proc updateInputs*(engine: var Engine): EngineState =
   assert engine.state in [Starting, Running]
 
--- a/src/semicongine/renderer.nim	Sat Jun 10 00:31:51 2023 +0700
+++ b/src/semicongine/renderer.nim	Wed Jun 14 22:55:00 2023 +0700
@@ -80,9 +80,9 @@
   # create index buffer if necessary
   var indicesBufferSize = 0'u64
   for mesh in allMeshes:
-    if mesh.indexType != None:
+    if mesh.indexType != MeshIndexType.None:
       let indexAlignment = case mesh.indexType
-        of None: 0'u64
+        of MeshIndexType.None: 0'u64
         of Tiny: 1'u64
         of Small: 2'u64
         of Big: 4'u64
@@ -140,7 +140,7 @@
         if perLocationOffsets[attribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT != 0:
           perLocationOffsets[attribute.memoryPerformanceHint] += VERTEX_ATTRIB_ALIGNMENT - (perLocationOffsets[attribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT)
 
-    let indexed = mesh.indexType != None
+    let indexed = mesh.indexType != MeshIndexType.None
     var drawable = Drawable(
       elementCount: if indexed: mesh.indicesCount else: mesh.vertexCount,
       bufferOffsets: offsets,
@@ -149,7 +149,7 @@
     )
     if indexed:
       let indexAlignment = case mesh.indexType
-        of None: 0'u64
+        of MeshIndexType.None: 0'u64
         of Tiny: 1'u64
         of Small: 2'u64
         of Big: 4'u64
@@ -223,7 +223,7 @@
     # if mesh transformation attribute is enabled, update the model matrix
     if renderer.scenedata[scene].transformAttribute != "":
       let transform = mesh.entity.getModelTransform()
-      if not (mesh in renderer.scenedata[scene].entityTransformationCache) or renderer.scenedata[scene].entityTransformationCache[mesh] != transform or mesh.areInstanceTransformsDirty:
+      if not (mesh in renderer.scenedata[scene].entityTransformationCache) or renderer.scenedata[scene].entityTransformationCache[mesh] != transform or mesh.areInstanceTransformsDirty :
         var updatedTransform = newSeq[Mat4](int(mesh.instanceCount))
         for i in 0 ..< mesh.instanceCount:
           updatedTransform[i] = transform * mesh.getInstanceTransform(i)
@@ -237,6 +237,10 @@
     var m = mesh
     m.clearDataChanged()
 
+proc updateAnimations*(renderer: var Renderer, scene: var Scene, dt: float32) =
+  for animation in allComponentsOfType[EntityAnimation](scene.root):
+    animation.update(dt)
+
 proc updateUniformData*(renderer: var Renderer, scene: var Scene) =
   assert scene in renderer.scenedata
 
--- a/src/semicongine/resources/mesh.nim	Sat Jun 10 00:31:51 2023 +0700
+++ b/src/semicongine/resources/mesh.nim	Wed Jun 14 22:55:00 2023 +0700
@@ -201,8 +201,10 @@
 
   # transformation
   if node.hasKey("matrix"):
-    for i in 0 .. node["matrix"].len:
-      result.transform.data[i] = node["matrix"][i].getFloat()
+    var mat: Mat4
+    for i in 0 ..< node["matrix"].len:
+      mat[i] = node["matrix"][i].getFloat()
+    result.transform = mat
   else:
     var (t, r, s) = (Unit4F32, Unit4F32, Unit4F32)
     if node.hasKey("translation"):
@@ -240,7 +242,7 @@
 proc loadScene(root: JsonNode, scenenode: JsonNode, mainBuffer: var seq[uint8]): Scene =
   var rootEntity = newEntity("<root>")
   for nodeId in scenenode["nodes"]:
-    let node = loadNode(root, root["nodes"][nodeId.getInt()], mainBuffer)
+    var node = loadNode(root, root["nodes"][nodeId.getInt()], mainBuffer)
     node.transform = node.transform * scale3d(1'f32, -1'f32, 1'f32)
     rootEntity.add node
 
--- a/src/semicongine/scene.nim	Sat Jun 10 00:31:51 2023 +0700
+++ b/src/semicongine/scene.nim	Wed Jun 14 22:55:00 2023 +0700
@@ -7,6 +7,7 @@
 import std/typetraits
 
 import ./core
+import ./animation
 
 type
   Scene* = object
@@ -25,18 +26,33 @@
 
   Entity* = ref object of RootObj
     name*: string
-    transform*: Mat4 # todo: cache transform + only update VBO when transform changed
+    internal_transform: Mat4 # todo: cache transform + only update VBO when transform changed
     parent*: Entity
     children*: seq[Entity]
     components*: seq[Component]
 
-func getModelTransform*(entity: Entity): Mat4 =
-  assert not entity.isNil
-  result = Unit4
-  var currentEntity = entity
-  while currentEntity != nil:
-    result = currentEntity.transform * result
-    currentEntity = currentEntity.parent
+  EntityAnimation* = ref object of Component
+    player: AnimationPlayer[Mat4]
+
+func newEntityAnimation*(animation: Animation[Mat4]): EntityAnimation =
+  result = EntityAnimation(player: newAnimator(animation))
+  result.player.currentValue = Unit4
+
+func setAnimation*(entityAnimation: EntityAnimation, animation: Animation[Mat4]) =
+  entityAnimation.player.animation = animation
+  entityAnimation.player.resetPlayer()
+
+func start*(animation: var EntityAnimation) =
+  animation.player.start()
+
+func stop*(animation: var EntityAnimation) =
+  animation.player.stop()
+
+func update*(animation: var EntityAnimation, dt: float32) =
+  animation.player.advance(dt)
+
+func getValue*(animation: var EntityAnimation): Mat4 =
+  return animation.player.currentValue
 
 func addShaderGlobal*[T](scene: var Scene, name: string, data: T) =
   scene.shaderGlobals[name] = newDataList(thetype=getDataType[T]())
@@ -93,24 +109,8 @@
 method `$`*(entity: Entity): string {.base.} = entity.name
 method `$`*(component: Component): string {.base.} =
   "Unknown Component"
-
-proc prettyRecursive*(entity: Entity): seq[string] =
-  var compList: seq[string]
-  for comp in entity.components:
-    compList.add $comp
-
-  var trans = entity.transform.col(3)
-  var pos = entity.getModelTransform().col(3)
-  result.add "- " & $entity & " [" & $trans.x & ", " & $trans.y & ", " & $trans.z & "] ->  [" & $pos.x & ", " & $pos.y & ", " & $pos.z & "]"
-  if compList.len > 0:
-    result.add "  [" & compList.join(", ") & "]"
-
-  for child in entity.children:
-    for childLine in child.prettyRecursive:
-      result.add "  " & childLine
-
-proc pretty*(entity: Entity): string =
-  entity.prettyRecursive.join("\n")
+method `$`*(animation: EntityAnimation): string =
+  &"Entity animation: {animation.player.animation}"
 
 proc add*(entity: Entity, child: Entity) =
   child.parent = entity
@@ -130,7 +130,7 @@
 func newEntity*(name: string = ""): Entity =
   result = new Entity
   result.name = name
-  result.transform = Unit4
+  result.internal_transform = Unit4
   if result.name == "":
     result.name = &"Entity[{$(cast[ByteAddress](result))}]"
 
@@ -140,7 +140,7 @@
   for child in children:
     result.add child
   result.name = name
-  result.transform = Unit4
+  result.internal_transform = Unit4
   if result.name == "":
     result.name = &"Entity[{$(cast[ByteAddress](result))}]"
 
@@ -152,7 +152,7 @@
     result.add component
   if result.name == "":
     result.name = &"Entity[{$(cast[ByteAddress](result))}]"
-  result.transform = Unit4
+  result.internal_transform = Unit4
 
 iterator allEntitiesOfType*[T: Entity](root: Entity): T =
   var queue = @[root]
@@ -223,3 +223,35 @@
     for child in next.children:
       queue.add child
     yield next
+
+func transform*(entity: Entity): Mat4 =
+  result = entity.internal_transform
+  for component in entity.components.mitems:
+    if component of EntityAnimation:
+      result = result * EntityAnimation(component).getValue
+
+func `transform=`*(entity: Entity, value: Mat4) =
+  entity.internal_transform = value
+
+func getModelTransform*(entity: Entity): Mat4 =
+  result = entity.transform
+  if not entity.parent.isNil:
+    result = entity.transform * entity.parent.getModelTransform()
+
+proc prettyRecursive*(entity: Entity): seq[string] =
+  var compList: seq[string]
+  for comp in entity.components:
+    compList.add $comp
+
+  var trans = entity.transform.col(3)
+  var pos = entity.getModelTransform().col(3)
+  result.add "- " & $entity & " [" & $trans.x & ", " & $trans.y & ", " & $trans.z & "] ->  [" & $pos.x & ", " & $pos.y & ", " & $pos.z & "]"
+  if compList.len > 0:
+    result.add "  [" & compList.join(", ") & "]"
+
+  for child in entity.children:
+    for childLine in child.prettyRecursive:
+      result.add "  " & childLine
+
+proc pretty*(entity: Entity): string =
+  entity.prettyRecursive.join("\n")
--- a/src/semicongine/text.nim	Sat Jun 10 00:31:51 2023 +0700
+++ b/src/semicongine/text.nim	Wed Jun 14 22:55:00 2023 +0700
@@ -85,7 +85,7 @@
   result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs)
   result.mesh.setInstanceTransforms(@[Unit4F32])
   result.name = $text
-  result.transform = Unit4F32
+  Entity(result).transform = Unit4F32
 
   # wrap the text mesh in a new entity to preserve the font-scaling
   var box = newEntity("box", result.mesh)