Skip to content

MotionView.kt

src/jvmMain/kotlin/afield/animation/mocap/MotionView.kt · 70 lines

@file:afield.runtime.completion.CompletionCategory("Animation")

package afield.animation.mocap

import afield.document.graph.Box
import afield.geometry.path.FLine
import afield.graphics.render.color
import afield.graphics.render.thickness
import afield.graphics.three.three
import afield.math.linalg.Vec4

fun Box.showMotion(frame: MotionFrame, skeleton: MotionSkeleton, channel: String = "motion",

Existing Field 3D viewer/camera, streamed FLine geometry; repeated draws preserve the user's orbit.

fun Box.showMotion(frame: MotionFrame, skeleton: MotionSkeleton, channel: String = "motion",
    reference: Boolean = true, contacts: Boolean = true, overlays: List<FLine> = emptyList(), height: Int = 420,
    follow: Boolean = false, followHalfLife: Double = .4, followVertical: Boolean = false, followEpoch: String = frame.episode.toString()) {
    val actual = frame.executed.bones(skeleton).apply { color = Vec4(.9f, .94f, 1f, 1f); thickness = 3.0 }
    val intended = if (reference) frame.intended?.bones(skeleton)?.apply { color = Vec4(.15f, .7f, .85f, .65f); thickness = 1.5 } else null
    val points = FLine().apply {
        repeat(skeleton.size) { i ->
            val p = frame.executed.position(i)
            moveTo(p.x - .012f, p.y, p.z).lineTo(p.x + .012f, p.y, p.z)
            moveTo(p.x, p.y - .012f, p.z).lineTo(p.x, p.y + .012f, p.z)
        }
        color = Vec4(.9f, .94f, 1f, 1f); thickness = 2.0
    }
    val contactMarks = FLine().apply {
        // CLoSD exports a contact probability for each runtime body/joint, in skeleton order.
        // Other contact layouts need their explicit runtime mapping before display.
        if (contacts && frame.contacts.size == skeleton.size && frame.contactProvenance == "simulator.measured") {
            frame.contacts.forEachIndexed { i, value -> if (value > .5) {
                val p = frame.executed.position(i)
                moveTo(p.x - .035f, p.y, p.z).lineTo(p.x + .035f, p.y, p.z)
                moveTo(p.x, p.y, p.z - .035f).lineTo(p.x, p.y, p.z + .035f)
            } }
        }
        color = Vec4(.3f, 1f, .5f, 1f); thickness = 4.0
    }
    three(channel = channel, name = "motion", height = height, width = 700) {
        lines(actual, points, contactMarks)
        intended?.let { lines(it) }
        lines(overlays)
        camera(yaw = 35.0, pitch = 12.0, distance = 2.6)
        if (follow) followCamera(frame.executed.root.let { afield.math.linalg.Vec3(it[0].toFloat(), it[1].toFloat(), it[2].toFloat()) },
            halfLifeSeconds=followHalfLife, epoch="${skeleton.hash}:$followEpoch", horizontalOnly=!followVertical)
        title("${skeleton.id} · ${"%.2f".format(frame.time)} s · episode ${frame.episode} · revision ${frame.revision}")
        helpers(true)
    }
}

fun TetherObservation.overlays(forceScale: Double = .00015): List<FLine>

Red spring/anchor and an amber submitted-force vector, scaled in metres per Newton for drawing.

fun TetherObservation.overlays(forceScale: Double = .00015): List<FLine> {
    require(forceScale.isFinite() && forceScale >= 0)
    if (!active || point.size != 3 || anchor.size != 3 || force.size != 3) return emptyList()
    val spring = FLine().apply {
        moveTo(point[0], point[1], point[2]); lineTo(anchor[0], anchor[1], anchor[2])
        val r = .06
        moveTo(anchor[0]-r, anchor[1], anchor[2]); lineTo(anchor[0]+r, anchor[1], anchor[2])
        moveTo(anchor[0], anchor[1]-r, anchor[2]); lineTo(anchor[0], anchor[1]+r, anchor[2])
        moveTo(anchor[0], anchor[1], anchor[2]-r); lineTo(anchor[0], anchor[1], anchor[2]+r)
        color = Vec4(1f, .3f, .4f, 1f); thickness = 3.0
    }
    val forceLine = FLine().apply {
        val scale = minOf(forceScale, .6 / forceN.coerceAtLeast(1e-9))
        moveTo(point[0], point[1], point[2])
        lineTo(point[0]+force[0]*scale, point[1]+force[1]*scale, point[2]+force[2]*scale)
        color = Vec4(1f, .75f, .2f, 1f); thickness = 4.0
    }
    return listOf(spring, forceLine)
}