# HG changeset patch # User Sam # Date 1686758100 -25200 # Node ID 5af702c95b16ffaa065c9a64afcb01657ceec400 # Parent 21d6d20de754a3662d760d4ebbe31fc3bf4ac183 add: what seems like a working animation system, atm integrated with entities, will add more for meshes diff -r 21d6d20de754 -r 5af702c95b16 src/semicongine.nim --- 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 diff -r 21d6d20de754 -r 5af702c95b16 src/semicongine/animation.nim --- /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) diff -r 21d6d20de754 -r 5af702c95b16 src/semicongine/core/matrix.nim --- 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)) diff -r 21d6d20de754 -r 5af702c95b16 src/semicongine/engine.nim --- 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] diff -r 21d6d20de754 -r 5af702c95b16 src/semicongine/renderer.nim --- 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 diff -r 21d6d20de754 -r 5af702c95b16 src/semicongine/resources/mesh.nim --- 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("") 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 diff -r 21d6d20de754 -r 5af702c95b16 src/semicongine/scene.nim --- 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") diff -r 21d6d20de754 -r 5af702c95b16 src/semicongine/text.nim --- 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)