diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/math/Quat.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/math/Quat.kt index 3800fcb47..aa62f715f 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/math/Quat.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/math/Quat.kt @@ -2,9 +2,7 @@ package de.fabmax.kool.math import de.fabmax.kool.util.Float32Buffer import de.fabmax.kool.util.MixedBuffer -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.sqrt +import kotlin.math.* fun QuatF.toQuatD() = QuatD(x.toDouble(), y.toDouble(), z.toDouble(), w.toDouble()) fun QuatF.toMutableQuatD(result: MutableQuatD = MutableQuatD()) = result.set(x.toDouble(), y.toDouble(), z.toDouble(), w.toDouble()) @@ -113,11 +111,31 @@ open class QuatF(open val x: Float, open val y: Float, open val z: Float, open v * [MutableVec4f]: result = that * weight + this * (1 - weight). */ fun mix(that: QuatF, weight: Float, result: MutableQuatF = MutableQuatF()): MutableQuatF { - result.x = that.x * weight + x * (1f - weight) - result.y = that.y * weight + y * (1f - weight) - result.z = that.z * weight + z * (1f - weight) - result.w = that.w * weight + w * (1f - weight) - return result.norm() + val dot = x * that.x + y * that.y + z * that.z + w * that.w + val absCosom = abs(dot) + + val scale0: Float + val scale1: Float + + if (1.0f - absCosom > FUZZY_EQ_F) { + val sinSqr = 1.0f - absCosom * absCosom + val sinom = 1.0f / sqrt(sinSqr) + val omega = atan2(sqrt(sinSqr), absCosom) + scale0 = sin((1.0f - weight) * omega) * sinom + scale1 = sin(weight * omega) * sinom + } else { + scale0 = 1.0f - weight + scale1 = weight + } + + val adjustedScale = if (dot >= 0.0f) scale1 else -scale1 + + result.x = scale0 * x + adjustedScale * that.x + result.y = scale0 * y + adjustedScale * that.y + result.z = scale0 * z + adjustedScale * that.z + result.w = scale0 * w + adjustedScale * that.w + + return result } /** @@ -434,11 +452,31 @@ open class QuatD(open val x: Double, open val y: Double, open val z: Double, ope * [MutableVec4d]: result = that * weight + this * (1 - weight). */ fun mix(that: QuatD, weight: Double, result: MutableQuatD = MutableQuatD()): MutableQuatD { - result.x = that.x * weight + x * (1.0 - weight) - result.y = that.y * weight + y * (1.0 - weight) - result.z = that.z * weight + z * (1.0 - weight) - result.w = that.w * weight + w * (1.0 - weight) - return result.norm() + val dot = x * that.x + y * that.y + z * that.z + w * that.w + val absCosom = abs(dot) + + val scale0: Double + val scale1: Double + + if (1.0 - absCosom > FUZZY_EQ_D) { + val sinSqr = 1.0 - absCosom * absCosom + val sinom = 1.0 / sqrt(sinSqr) + val omega = atan2(sqrt(sinSqr), absCosom) + scale0 = sin((1.0 - weight) * omega) * sinom + scale1 = sin(weight * omega) * sinom + } else { + scale0 = 1.0 - weight + scale1 = weight + } + + val adjustedScale = if (dot >= 0.0) scale1 else -scale1 + + result.x = scale0 * x + adjustedScale * that.x + result.y = scale0 * y + adjustedScale * that.y + result.z = scale0 * z + adjustedScale * that.z + result.w = scale0 * w + adjustedScale * that.w + + return result } /** diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt index d4dae38ab..1856ffffe 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt @@ -225,7 +225,7 @@ data class GltfFile( val modelAnim = Animation(anim.name) modelAnimations += modelAnim - val animNodes = mutableMapOf() + val animNodes = mutableMapOf() anim.channels.forEach { channel -> val nodeGrp = modelNodes[channel.target.nodeRef] if (nodeGrp != null) { @@ -240,7 +240,7 @@ data class GltfFile( } } - private fun makeTranslationAnimation(animCh: GltfAnimation.Channel, animNd: AnimationNode, modelAnim: Animation) { + private fun makeTranslationAnimation(animCh: GltfAnimation.Channel, animNd: AnimatedTransformGroup, modelAnim: Animation) { val inputAcc = animCh.samplerRef.inputAccessorRef val outputAcc = animCh.samplerRef.outputAccessorRef @@ -261,6 +261,7 @@ data class GltfFile( } modelAnim.channels += transChannel + val bindTranslation = animNd.initTranslation val inTime = FloatAccessor(inputAcc) val outTranslation = Vec3fAccessor(outputAcc) for (i in 0 until min(inputAcc.count, outputAcc.count)) { @@ -269,16 +270,21 @@ data class GltfFile( val startTan = outTranslation.next() val point = outTranslation.next() val endTan = outTranslation.next() - CubicTranslationKey(t, point, startTan, endTan) + CubicTranslationKey( + t, + point - bindTranslation, + startTan - bindTranslation, + endTan - bindTranslation + ) } else { - TranslationKey(t, outTranslation.next()) + TranslationKey(t, outTranslation.next() - bindTranslation) } transKey.interpolation = interpolation transChannel.keys[t] = transKey } } - private fun makeRotationAnimation(animCh: GltfAnimation.Channel, animNd: AnimationNode, modelAnim: Animation) { + private fun makeRotationAnimation(animCh: GltfAnimation.Channel, animNd: AnimatedTransformGroup, modelAnim: Animation) { val inputAcc = animCh.samplerRef.inputAccessorRef val outputAcc = animCh.samplerRef.outputAccessorRef @@ -299,6 +305,7 @@ data class GltfFile( } modelAnim.channels += rotChannel + val bindRotation = animNd.initRotation val inTime = FloatAccessor(inputAcc) val outRotation = Vec4fAccessor(outputAcc) for (i in 0 until min(inputAcc.count, outputAcc.count)) { @@ -307,16 +314,21 @@ data class GltfFile( val startTan = outRotation.next().toQuatF() val point = outRotation.next().toQuatF() val endTan = outRotation.next().toQuatF() - CubicRotationKey(t, point, startTan, endTan) + CubicRotationKey( + t, + bindRotation.inverted().mul(point), + bindRotation.inverted().mul(startTan), + bindRotation.inverted().mul(endTan) + ) } else { - RotationKey(t, outRotation.next().toQuatF()) + RotationKey(t, bindRotation.inverted().mul(outRotation.next().toQuatF())) } rotKey.interpolation = interpolation rotChannel.keys[t] = rotKey } } - private fun makeScaleAnimation(animCh: GltfAnimation.Channel, animNd: AnimationNode, modelAnim: Animation) { + private fun makeScaleAnimation(animCh: GltfAnimation.Channel, animNd: AnimatedTransformGroup, modelAnim: Animation) { val inputAcc = animCh.samplerRef.inputAccessorRef val outputAcc = animCh.samplerRef.outputAccessorRef @@ -337,6 +349,7 @@ data class GltfFile( } modelAnim.channels += scaleChannel + val bindScale = animNd.initScale val inTime = FloatAccessor(inputAcc) val outScale = Vec3fAccessor(outputAcc) for (i in 0 until min(inputAcc.count, outputAcc.count)) { @@ -345,9 +358,9 @@ data class GltfFile( val startTan = outScale.next() val point = outScale.next() val endTan = outScale.next() - CubicScaleKey(t, point, startTan, endTan) + CubicScaleKey(t, bindScale / point, bindScale / startTan, bindScale / endTan) } else { - ScaleKey(t, outScale.next()) + ScaleKey(t, bindScale / outScale.next()) } scaleKey.interpolation = interpolation scaleChannel.keys[t] = scaleKey diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt index 7531c9c46..9b997a25d 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt @@ -30,6 +30,8 @@ class Model(name: String? = null) : Node(name) { } fun applyAnimation(deltaT: Float) { + animations.forEach(Animation::reset) + var firstActive = true for (i in animations.indices) { if (animations[i].weight > 0f) { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/animation/Animation.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/animation/Animation.kt index 96824744b..6966a6d17 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/animation/Animation.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/animation/Animation.kt @@ -4,6 +4,7 @@ import de.fabmax.kool.math.* import de.fabmax.kool.scene.MatrixTransformF import de.fabmax.kool.scene.Mesh import de.fabmax.kool.scene.Node +import de.fabmax.kool.scene.TrsTransformF import de.fabmax.kool.util.TreeMap import de.fabmax.kool.util.logE import kotlin.math.min @@ -26,23 +27,21 @@ class Animation(val name: String?) { animationNodes += channels.map { it.animationNode }.distinct() } - fun apply(deltaT: Float, firstWeightedTransform: Boolean = true) { - progress = (progress + duration + deltaT * speed) % duration - + fun reset() { for (i in animationNodes.indices) { animationNodes[i].initTransform() } + } + + fun apply(deltaT: Float, firstWeightedTransform: Boolean = true) { + progress = (progress + duration + deltaT * speed) % duration + for (i in channels.indices) { channels[i].apply(progress) } - if (weight == 1f) { - for (i in animationNodes.indices) { - animationNodes[i].applyTransform() - } - } else { - for (i in animationNodes.indices) { - animationNodes[i].applyTransformWeighted(weight, firstWeightedTransform) - } + + for (i in animationNodes.indices) { + animationNodes[i].applyTransformWeighted(weight, firstWeightedTransform) } } @@ -55,7 +54,7 @@ class Animation(val name: String?) { } } -abstract class AnimationChannel>(val name: String?, val animationNode: AnimationNode) { +abstract class AnimationChannel>(val name: String?, val animationNode: AnimationNode) { val keys = TreeMap() val lastKeyTime: Float get() = keys.lastKey() @@ -75,47 +74,51 @@ abstract class AnimationChannel>(val name: String?, val anima println("$indent${animKeys[i]}") } if (animKeys.size > 5) { - println("$indent ...${animKeys.size-5} more") + println("$indent ...${animKeys.size - 5} more") } } } -class TranslationAnimationChannel(name: String?, animationNode: AnimationNode): AnimationChannel(name, animationNode) +class TranslationAnimationChannel(name: String?, animationNode: AnimationNode) : + AnimationChannel(name, animationNode) -class RotationAnimationChannel(name: String?, animationNode: AnimationNode): AnimationChannel(name, animationNode) +class RotationAnimationChannel(name: String?, animationNode: AnimationNode) : + AnimationChannel(name, animationNode) -class ScaleAnimationChannel(name: String?, animationNode: AnimationNode): AnimationChannel(name, animationNode) +class ScaleAnimationChannel(name: String?, animationNode: AnimationNode) : + AnimationChannel(name, animationNode) -class WeightAnimationChannel(name: String?, animationNode: AnimationNode): AnimationChannel(name, animationNode) +class WeightAnimationChannel(name: String?, animationNode: AnimationNode) : + AnimationChannel(name, animationNode) interface AnimationNode { val name: String - fun initTransform() { } + fun initTransform() {} fun applyTransform() fun applyTransformWeighted(weight: Float, firstWeightedTransform: Boolean) - fun setTranslation(translation: Vec3f) { } - fun setRotation(rotation: QuatF) { } - fun setScale(scale: Vec3f) { } + fun setTranslation(translation: Vec3f) {} + fun setRotation(rotation: QuatF) {} + fun setScale(scale: Vec3f) {} - fun setWeights(weights: FloatArray) { } + fun setWeights(weights: FloatArray) {} } -class AnimatedTransformGroup(val target: Node): AnimationNode { +class AnimatedTransformGroup(val target: Node) : AnimationNode { override val name: String get() = target.name - private val initTranslation = MutableVec3f() - private val initRotation = MutableQuatF() - private val initScale = MutableVec3f(Vec3f.ONES) + val initTranslation = MutableVec3f() + val initRotation = MutableQuatF() + val initScale = MutableVec3f(Vec3f.ONES) private val animTranslation = MutableVec3f() private val animRotation = MutableQuatF() - private val animScale = MutableVec3f() + private val animScale = MutableVec3f(1f, 1f, 1f) - private val quatRotMat = MutableMat4f() - private val weightedTransformMat = MutableMat4f() + private val baseRotation = MutableQuatF() + private val baseScale = MutableVec3f(Vec3f.ONES) init { val vec4 = MutableVec4f() @@ -129,9 +132,12 @@ class AnimatedTransformGroup(val target: Node): AnimationNode { } override fun initTransform() { - animTranslation.set(initTranslation) - animRotation.set(initRotation) - animScale.set(initScale) + var t = target.transform + if (t !is TrsTransformF) { + t = TrsTransformF() + target.transform = t + } + t.setCompositionOf(initTranslation, initRotation, initScale) } override fun applyTransform() { @@ -139,40 +145,17 @@ class AnimatedTransformGroup(val target: Node): AnimationNode { } override fun applyTransformWeighted(weight: Float, firstWeightedTransform: Boolean) { - weightedTransformMat.setIdentity() - weightedTransformMat.translate(animTranslation) - weightedTransformMat.rotate(animRotation) - weightedTransformMat.scale(animScale) - - var t = target.transform as? MatrixTransformF - if (t == null) { - t = MatrixTransformF() + var t = target.transform + if (t !is TrsTransformF) { + t = TrsTransformF() target.transform = t } - val wm = if (firstWeightedTransform) 0f else 1f - - t.matrixF.m00 = t.matrixF.m00 * wm + weightedTransformMat.m00 * weight - t.matrixF.m01 = t.matrixF.m01 * wm + weightedTransformMat.m01 * weight - t.matrixF.m02 = t.matrixF.m02 * wm + weightedTransformMat.m02 * weight - t.matrixF.m03 = t.matrixF.m03 * wm + weightedTransformMat.m03 * weight - - t.matrixF.m10 = t.matrixF.m10 * wm + weightedTransformMat.m10 * weight - t.matrixF.m11 = t.matrixF.m11 * wm + weightedTransformMat.m11 * weight - t.matrixF.m12 = t.matrixF.m12 * wm + weightedTransformMat.m12 * weight - t.matrixF.m13 = t.matrixF.m13 * wm + weightedTransformMat.m13 * weight - - t.matrixF.m20 = t.matrixF.m20 * wm + weightedTransformMat.m20 * weight - t.matrixF.m21 = t.matrixF.m21 * wm + weightedTransformMat.m21 * weight - t.matrixF.m22 = t.matrixF.m22 * wm + weightedTransformMat.m22 * weight - t.matrixF.m23 = t.matrixF.m23 * wm + weightedTransformMat.m23 * weight - - t.matrixF.m30 = t.matrixF.m30 * wm + weightedTransformMat.m30 * weight - t.matrixF.m31 = t.matrixF.m31 * wm + weightedTransformMat.m31 * weight - t.matrixF.m32 = t.matrixF.m32 * wm + weightedTransformMat.m32 * weight - t.matrixF.m33 = t.matrixF.m33 * wm + weightedTransformMat.m33 * weight + t.translate(animTranslation.mul(weight)) + t.rotate(baseRotation.mix(animRotation, weight)) + t.scale(baseScale.mix(animScale, weight)) - target.transform.markDirty() + t.markDirty() } override fun setTranslation(translation: Vec3f) { @@ -188,7 +171,7 @@ class AnimatedTransformGroup(val target: Node): AnimationNode { } } -class MorphAnimatedMesh(val target: Mesh): AnimationNode { +class MorphAnimatedMesh(val target: Mesh) : AnimationNode { override val name: String get() = target.name diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/animation/AnimationController.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/animation/AnimationController.kt new file mode 100644 index 000000000..1479d16b1 --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/animation/AnimationController.kt @@ -0,0 +1,94 @@ +package de.fabmax.kool.scene.animation + +import de.fabmax.kool.scene.Model + +class AnimationController(val model: Model) { + var initialState = "" + private val states = mutableListOf() + + private var currentState = initialState + + fun update(deltaTime: Float) { + states.forEach { state -> + val newState = state.update(deltaTime, state.name == currentState) + if(newState != currentState) { + currentState = newState + state.onExit() + states.find { it.name == newState }?.onEnter?.invoke() + } + } + model.applyAnimation(deltaTime) + } + + fun state(stateName: String, body: AnimationState.() -> Unit) = AnimationState(model) + .apply(body).apply { name = stateName; states.add(this) } +} + +class AnimationState(val model: Model) { + private val animations = mutableListOf() + private val transitions = mutableListOf() + private var weight = 0f + + var name = "" + var blendTransition = 0.2f + var onEnter: () -> Unit = {} + var onExit: () -> Unit = {} + var onUpdate: (Float) -> Unit = {} + + fun update(deltaTime: Float, isActiveState: Boolean): String { + updateWeight(deltaTime, isActiveState) + onUpdate(deltaTime) + animations.forEach { it.update(deltaTime, weight) } + return transitions.firstOrNull { it.predicate() }?.nextState ?: name + } + + private fun updateWeight(deltaTime: Float, activeState: Boolean) { + if (activeState) weight += deltaTime / blendTransition + else weight -= deltaTime / blendTransition + weight = weight.coerceIn(0f, 1f) + } + + fun animations(vararg names: String, blendTransition: Float = 0.2f, condition: () -> Boolean = { true }) { + names.forEach { + animations.add(Animation(model).apply { + this.blendTransition = blendTransition + this.predicate = condition + this.name = it + }) + } + } + + fun transition(transitionState: String, condition: () -> Boolean) = Transition().apply { + nextState = transitionState + predicate = condition + transitions.add(this) + } + + class Animation(val model: Model) { + var name = "" + set(value) { + field = value + animation = model.animations.find { it.name == this.name } ?: error("Animation $name not found!") + } + lateinit var animation: de.fabmax.kool.scene.animation.Animation + var predicate: () -> Boolean = { true } + var blendTransition = 0.2f + private var weight = 0f + + fun update(deltaTime: Float, stateWeight: Float) { + updateWeight(deltaTime, predicate()) + animation.weight = stateWeight * weight + } + + private fun updateWeight(deltaTime: Float, activeState: Boolean) { + if (activeState) weight += deltaTime / blendTransition + else weight -= deltaTime / blendTransition + weight = weight.coerceIn(0f, 1f) + } + } + + class Transition { + var nextState = "" + var predicate: () -> Boolean = { true } + } +} \ No newline at end of file