Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
.DS_Store
**.exe
**.bbmodel
**.json
gen/**.json
gen/assets
plugin.iml
/bbconv
/assets
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.github.solid.resourcepack.bbconv.api.animation.bone

import org.bukkit.util.Transformation

/**
* Produces a Transformation based on the current animation context
*/
fun interface BoneAnimation {
fun animate(context: BoneAnimationContext): Transformation
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.github.solid.resourcepack.bbconv.api.animation.bone

import io.github.solid.resourcepack.bbconv.api.entity.RenderedBone
import io.github.solid.resourcepack.bbconv.api.entity.RenderedEntity

/**
* Context of an active BoneAnimation.
* This holds the bone itself, the current entity and the
* elapsed time for the current animation and the initial state (transformation property pre mutation) of the bone.
*
* Mutating the transformation property will apply this mutation to the [RenderedBone]
*/
data class BoneAnimationContext(
val entity: RenderedEntity,
val elapsedTime: Float,
val self: RenderedBone,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.solid.resourcepack.bbconv.api.animation.bone

import org.bukkit.util.Transformation
import org.joml.Quaternionf
import org.joml.Vector3f
import org.joml.times

class GroupedBoneAnimation(
private val animations: List<BoneAnimation>
) : BoneAnimation {
override fun animate(context: BoneAnimationContext): Transformation {
var result = Transformation(Vector3f(), Quaternionf(), Vector3f(), Quaternionf())
animations.forEach {
val transformation = it.animate(context)
result = Transformation(
result.translation.add(transformation.translation),
Quaternionf(transformation.leftRotation).times(result.leftRotation),
result.scale.add(transformation.scale),
Quaternionf()
)
}
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.solid.resourcepack.bbconv.api.animation.bone

import io.github.solid.resourcepack.bbconv.api.animation.keyframe.Timeline
import io.github.solid.resourcepack.bbconv.config.Animator
import org.bukkit.util.Transformation
import org.joml.Quaternionf
import org.joml.Vector3f

/**
* Represents a keyframe based bone animation (being read from configs or other non-procedural sources)
*/
class KeyFramedBoneAnimation(
private val animator: Animator
) : BoneAnimation {
override fun animate(context: BoneAnimationContext): Transformation {
val scale = if (animator.scale.size >= 2) Timeline.ofScale(animator.scale)
.interpolate(context.elapsedTime) else Vector3f()
val position = if (animator.position.size >= 2) Timeline.ofPosition(animator.position)
.interpolate(context.elapsedTime) else Vector3f()
val rotation = if (animator.rotation.size >= 2) Timeline.ofRotation(animator.rotation)
.interpolate(context.elapsedTime) else Quaternionf()
return Transformation(position, rotation, scale, Quaternionf())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.solid.resourcepack.bbconv.api.animation.entity

import io.github.solid.resourcepack.bbconv.api.animation.bone.BoneAnimation
import io.github.solid.resourcepack.bbconv.api.animation.bone.KeyFramedBoneAnimation
import io.github.solid.resourcepack.bbconv.config.Animation

typealias EntityAnimation = MutableMap<String, BoneAnimation>

object EntityAnimations {
fun of(animation: Animation): EntityAnimation {
return animation.animators.associate { animator -> animator.bone to KeyFramedBoneAnimation(animator) }
.toMutableMap()
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package io.github.solid.resourcepack.bbconv.api.animation.entity

import io.github.solid.resourcepack.bbconv.api.animation.bone.BoneAnimation
import io.github.solid.resourcepack.bbconv.api.animation.bone.BoneAnimationContext
import io.github.solid.resourcepack.bbconv.api.entity.RenderedBone
import io.github.solid.resourcepack.bbconv.api.entity.RenderedEntity
import io.github.solid.resourcepack.bbconv.config.OpenModelConfig
import io.github.solid.resourcepack.bbconv.util.QuaternionMath
import org.bukkit.util.Transformation
import org.joml.Quaternionf
import org.joml.Vector3f

class EntityAnimationController(
private val entity: RenderedEntity,
) {
val loadedAnimations = AnimationInfos.of(entity.config).toMutableMap()
private val activeAnimations: MutableMap<EntityAnimationData, Float> = mutableMapOf()

fun play(animation: EntityAnimation) {
val type = loadedAnimations.toList().firstOrNull { it.second == animation }?.first ?: return
activeAnimations[type] = 0f
}

fun play(animation: String) {
val type = loadedAnimations.toList().firstOrNull { it.first.name == animation }?.first ?: return
activeAnimations[type] = 0f
}

fun get(animation: String): EntityAnimationData? {
return loadedAnimations.toList().firstOrNull { it.first.name == animation }?.first
}

fun cancel(animation: EntityAnimation) {
val type = loadedAnimations.toList().firstOrNull { it.second == animation }?.first ?: return
activeAnimations.remove(type)
}

fun cancel(animation: String) {
val type = loadedAnimations.toList().firstOrNull { it.first.name == animation }?.first ?: return
activeAnimations.remove(type)
}

fun reset() {
activeAnimations.clear()
}

fun animate(delta: Float) {
val result = mutableMapOf<RenderedBone, Transformation>()
activeAnimations.forEach { (type, currentTime) ->
val animation = loadedAnimations[type] ?: return@forEach
var time = currentTime + delta
if (time > type.duration) {
if (!type.looped) {
activeAnimations.remove(type)
return@forEach
}
time = 0f;
}
val mappedBones = bonesToAnimations(entity.rootBones, animation)
mappedBones.forEach { (bone, anim) ->
result.putAll(
partialAnimate(
anim, createContext(bone, time), result[bone] ?: Transformation(
Vector3f(), Quaternionf(), Vector3f(), Quaternionf()
)
)
)
}
activeAnimations[type] = time
}
result.forEach { (bone, transformation) ->
val transformation = appendTransformation(transformation, bone.getInitialTransformation())
bone.getDisplay().transformation = transformation
}
}

private fun createContext(bone: RenderedBone, time: Float): BoneAnimationContext {
return BoneAnimationContext(
entity,
time,
bone,
)
}

private fun bonesToAnimations(
bones: List<RenderedBone>, animation: EntityAnimation
): MutableMap<RenderedBone, BoneAnimation> {
val result = bones.filter { animation.keys.contains(it.bone.id) }
.associateWith { bone -> animation.toList().first { it.first == bone.bone.id }.second }.toMutableMap()
bones.forEach { bone ->
result.putAll(bonesToAnimations(bone.children, animation))
}
return result
}

private fun partialAnimate(
animation: BoneAnimation, ctx: BoneAnimationContext, transformation: Transformation
): Map<RenderedBone, Transformation> {
val result = appendTransformation(transformation, animation.animate(ctx))
val returned = mutableMapOf<RenderedBone, Transformation>()
returned[ctx.self] = result
returned.putAll(animateChildren(result, ctx.self))
return returned
}

private fun animateChildren(
transformation: Transformation, parent: RenderedBone
): Map<RenderedBone, Transformation> {
val result = mutableMapOf<RenderedBone, Transformation>()
parent.children.forEach { child ->
val initial = child.getInitialTransformation()
val parentTrans = transformation

// Calculate rotated local translation
val rotatedTranslation = Vector3f(initial.translation)
parentTrans.leftRotation.transform(rotatedTranslation)

val deltaTranslation = Vector3f(rotatedTranslation).sub(initial.translation)

// Rotation delta: how the rotation has changed
val rotatedRotation = Quaternionf(parentTrans.leftRotation).mul(initial.leftRotation)
val deltaRotation = QuaternionMath.delta(initial.leftRotation, rotatedRotation)

// Scale delta: how the scale has changed
val deltaScale = Vector3f(parentTrans.scale).div(parent.bone.scale).mul(initial.scale)

val childTransformation = Transformation(
deltaTranslation,
deltaRotation,
deltaScale,
Quaternionf()
)

result[child] = childTransformation
result.putAll(animateChildren(childTransformation, child))
}
return result
}

private fun appendTransformation(first: Transformation, second: Transformation): Transformation {
return Transformation(
Vector3f(first.translation).add(second.translation),
Quaternionf(first.leftRotation).mul(Quaternionf(second.leftRotation)),
Vector3f(first.scale).add(Vector3f(second.scale)),
Quaternionf()
)
}
}

object AnimationInfos {

private val cache = mutableMapOf<OpenModelConfig, Map<EntityAnimationData, EntityAnimation>>()

@JvmStatic
fun of(config: OpenModelConfig): Map<EntityAnimationData, EntityAnimation> {
if (cache.containsKey(config)) {
return cache[config]!!
}
val result = config.animations.associate { animation ->
EntityAnimationData.of(animation) to EntityAnimations.of(animation)
}
cache[config] = result
return result
}

fun clearCache() {
cache.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.solid.resourcepack.bbconv.api.animation.entity

import io.github.solid.resourcepack.bbconv.config.Animation

data class EntityAnimationData(
val name: String,
/**
* Negative if the duration is indefinite/procedural
*/
val duration: Float = -1f,
val looped: Boolean = false,
val loopDelay: Float = -1f,
) {
companion object {
@JvmStatic
fun of(animation: Animation): EntityAnimationData {
return EntityAnimationData(
name = animation.name,
duration = animation.length.toFloat(),
looped = animation.loop,
loopDelay = animation.loopDelay
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.github.solid.resourcepack.bbconv.api.animation.keyframe

import io.github.solid.resourcepack.bbconv.util.Interpolation

data class Keyframe<T>(
val data: T,
val interpolation: Interpolation,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.github.solid.resourcepack.bbconv.api.animation.keyframe

import io.github.solid.resourcepack.bbconv.config.PositionKeyframe
import io.github.solid.resourcepack.bbconv.config.RotationKeyframe
import io.github.solid.resourcepack.bbconv.config.ScaleKeyframe
import io.github.solid.resourcepack.bbconv.util.Interpolation
import io.github.solid.resourcepack.bbconv.util.Interpolator
import org.joml.Quaternionf
import org.joml.Vector3f

typealias TimelineFrames<T> = Map<Float, Keyframe<T>>

class Timeline<T>(private val frames: TimelineFrames<T>, private val clazz: Class<T>) {

private val sortedFrames = frames.toSortedMap(compareBy { it })

fun interpolate(time: Float): T {
require(sortedFrames.size >= 2) { "Need at least 2 keyframes, has ${sortedFrames.size}" }
val times = sortedFrames.keys.toList()

if (time <= times.first()) return sortedFrames[times.first()]!!.data
if (time >= times.last()) return sortedFrames[times.last()]!!.data

val index = times.indexOfLast { it <= time }
val t0 = times.getOrNull(index - 1)
val t1 = times[index]
val t2 = times[index + 1]
val t3 = times.getOrNull(index + 2)

val k0 = t0?.let { frames[it] } ?: frames[t1]!!
val k1 = frames[t1]!!
val k2 = frames[t2]!!
val k3 = t3?.let { frames[it] } ?: frames[t2]!!

val localT = ((time - t1) / (t2 - t1)).coerceIn(0f, 1f)

val interpolator = Interpolator(k1.interpolation, clazz)
return interpolator.interpolate(localT, k0.data, k1.data, k2.data, k3.data)
}

companion object {
@JvmStatic
fun ofPosition(frames: List<PositionKeyframe>): Timeline<Vector3f> {
val converted = frames.associate {
it.time to Keyframe(
it.position.toVectorf().div(16f),
Interpolation.fromString(it.interpolation)
)
}
return Timeline(converted, Vector3f::class.java)
}

@JvmStatic
fun ofScale(frames: List<ScaleKeyframe>): Timeline<Vector3f> {
val converted = frames.associate {
it.time to Keyframe(
it.scale.toVectorf().div(16f),
Interpolation.fromString(it.interpolation)
)
}
return Timeline(converted, Vector3f::class.java)
}

@JvmStatic
fun ofRotation(frames: List<RotationKeyframe>): Timeline<Quaternionf> {
val converted = frames.associate {
it.time to Keyframe(
it.leftRotation.toQuaternionf(),
Interpolation.fromString(it.interpolation)
)
}
return Timeline(converted, Quaternionf::class.java)
}
}
}
Loading