Skip to content

DualQuaternionf.kt

src/jvmMain/kotlin/afield/math/linalg/DualQuaternionf.kt · 293 lines · cited by Keyframes: capturing, persisting and replaying live state

package afield.math.linalg

Rigid 3D transforms (rotation plus translation) as mutable unit dual quaternions, with blending.

DualQuaternionf is built from a rotation and a translation, composes with *, transforms points with transform or *, and converts to a Matrix4f with toMatrix. Two transforms blend with nlerp (fast, normalised component blend) or sclerp (screw-linear interpolation, which moves at constant speed along a single screw motion); pow scales a transform by a fraction. Both blends take the shorter path when the two quaternions have opposite signs.

Methods without a dest argument change the receiver and return it, following the rest of afield.math.linalg.

import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt

open class DualQuaternionf

A mutable single-precision unit dual quaternion for rigid 3D transforms.

This class intentionally follows Field's mutable naming style and provides a single-precision rigid-transform representation. The real part stores rotation. The dual part stores translation as 0.5 * translation * rotation.

val real: Quaternionf = Quaternionf()

val dual: Quaternionf = Quaternionf(0f, 0f, 0f, 0f)

constructor()

constructor(other: DualQuaternionf)

    constructor(other: DualQuaternionf) {
        set(other)
    }

constructor(real: Quaternionfc, dual: Quaternionfc)

    constructor(real: Quaternionfc, dual: Quaternionfc) {
        set(real, dual)
    }

constructor(rotation: Quaternionfc, translation: Vector3fc)

    constructor(rotation: Quaternionfc, translation: Vector3fc) {
        set(rotation, translation)
    }

fun identity(): DualQuaternionf

    fun identity(): DualQuaternionf {
        real.identity()
        dual.set(0f, 0f, 0f, 0f)
        return this
    }

fun set(other: DualQuaternionf): DualQuaternionf

    fun set(other: DualQuaternionf): DualQuaternionf =
        set(other.real, other.dual)

fun set(real: Quaternionfc, dual: Quaternionfc): DualQuaternionf

    fun set(real: Quaternionfc, dual: Quaternionfc): DualQuaternionf {
        this.real.set(real)
        this.dual.set(dual)
        return this
    }

fun set(rotation: Quaternionfc, translation: Vector3fc): DualQuaternionf

    fun set(rotation: Quaternionfc, translation: Vector3fc): DualQuaternionf {
        real.set(rotation).normalize()
        setDualFromTranslation(translation)
        return normalize()
    }

fun setTranslation(translation: Vector3fc): DualQuaternionf

    fun setTranslation(translation: Vector3fc): DualQuaternionf {
        real.normalize()
        setDualFromTranslation(translation)
        return normalize()
    }

fun getRotation(dest: Quaternionf = Quaternionf()): Quaternionf

    fun getRotation(dest: Quaternionf = Quaternionf()): Quaternionf =
        dest.set(real).normalize()

fun getTranslation(dest: Vector3f = Vector3f()): Vector3f

    fun getTranslation(dest: Vector3f = Vector3f()): Vector3f {
        val rInv = Quaternionf(real).conjugate()
        val t = Quaternionf(dual).mul(rInv)
        return dest.set(2f * t.x, 2f * t.y, 2f * t.z)
    }

fun dot(other: DualQuaternionf): Float

    fun dot(other: DualQuaternionf): Float =
        quatDot(real, other.real)

fun negate(): DualQuaternionf

    fun negate(): DualQuaternionf {
        real.negateComponents()
        dual.negateComponents()
        return this
    }

fun normalize(): DualQuaternionf

    fun normalize(): DualQuaternionf {
        val lenSq = quatDot(real, real)
        if (lenSq < EPSILON * EPSILON) {
            return identity()
        }

        val invLen = 1f / sqrt(lenSq)
        real.scaleComponents(invLen)
        dual.scaleComponents(invLen)

        // Unit dual quaternions satisfy dot(real, dual) == 0. Keep that
        // invariant after linear blends and accumulated floating-point error.
        val rd = quatDot(real, dual)
        dual.fmaComponents(-rd, real)
        return this
    }

fun normalized(dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf

    fun normalized(dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf =
        dest.set(this).normalize()

fun conjugate(): DualQuaternionf

    fun conjugate(): DualQuaternionf {
        real.conjugate()
        dual.conjugate()
        return this
    }

fun invert(): DualQuaternionf

    fun invert(): DualQuaternionf =
        normalize().conjugate()

fun inverted(dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf

    fun inverted(dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf =
        dest.set(this).invert()

fun mul(right: DualQuaternionf): DualQuaternionf

    fun mul(right: DualQuaternionf): DualQuaternionf =
        mul(right, this)

fun mul(right: DualQuaternionf, dest: DualQuaternionf): DualQuaternionf

    fun mul(right: DualQuaternionf, dest: DualQuaternionf): DualQuaternionf {
        val outReal = Quaternionf(real).mul(right.real)
        val outDual = Quaternionf(real).mul(right.dual)
        outDual.addComponents(Quaternionf(dual).mul(right.real))
        dest.real.set(outReal)
        dest.dual.set(outDual)
        return dest
    }

operator fun times(right: DualQuaternionf): DualQuaternionf

    operator fun times(right: DualQuaternionf): DualQuaternionf =
        mul(right, DualQuaternionf())

operator fun times(position: Vector3fc): Vector3f

    operator fun times(position: Vector3fc): Vector3f =
        transform(position)

operator fun unaryMinus(): DualQuaternionf

    operator fun unaryMinus(): DualQuaternionf =
        DualQuaternionf(this).negate()

fun transform(position: Vector3fc, dest: Vector3f = Vector3f()): Vector3f

    fun transform(position: Vector3fc, dest: Vector3f = Vector3f()): Vector3f {
        real.transform(position, dest)
        return dest.add(getTranslation(TEMP_VEC.get()))
    }

fun toMatrix(dest: Matrix4f = Matrix4f()): Matrix4f

    fun toMatrix(dest: Matrix4f = Matrix4f()): Matrix4f {
        val translation = getTranslation(TEMP_VEC.get())
        return dest.identity().translate(translation.x, translation.y, translation.z).rotate(real)
    }

fun nlerp(other: DualQuaternionf, t: Float, dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf

    fun nlerp(other: DualQuaternionf, t: Float, dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf {
        val sign = if (dot(other) < -EPSILON) -1f else 1f
        val oneMinusT = 1f - t
        dest.real.set(
            real.x * oneMinusT + other.real.x * sign * t,
            real.y * oneMinusT + other.real.y * sign * t,
            real.z * oneMinusT + other.real.z * sign * t,
            real.w * oneMinusT + other.real.w * sign * t
        )
        dest.dual.set(
            dual.x * oneMinusT + other.dual.x * sign * t,
            dual.y * oneMinusT + other.dual.y * sign * t,
            dual.z * oneMinusT + other.dual.z * sign * t,
            dual.w * oneMinusT + other.dual.w * sign * t
        )
        return dest.normalize()
    }

fun sclerp(other: DualQuaternionf, t: Float, dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf

    fun sclerp(other: DualQuaternionf, t: Float, dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf {
        // At exactly 180 degrees the quaternion sign is numerically ambiguous.
        // Preserve the caller's axis choice instead of flipping on float roundoff.
        val alignedOther = if (dot(other) < -EPSILON) DualQuaternionf(other).negate() else other
        val relative = inverted(TEMP_DQ_A.get()).mul(alignedOther, TEMP_DQ_B.get()).normalize()
        val step = relative.pow(t, TEMP_DQ_C.get())
        return mul(step, dest).normalize()
    }

fun pow(exponent: Float, dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf

body — 73 lines, from line 181
    fun pow(exponent: Float, dest: DualQuaternionf = DualQuaternionf()): DualQuaternionf {
        val omega = TEMP_VEC.get()
        val velocity = TEMP_VEC_2.get()
        logTwist(omega, velocity)
        omega.mul(exponent)
        velocity.mul(exponent)
        return fromTwist(omega, velocity, dest)
    }

    private fun setDualFromTranslation(translation: Vector3fc) {
        val tx = translation.x()
        val ty = translation.y()
        val tz = translation.z()
        val rx = real.x
        val ry = real.y
        val rz = real.z
        val rw = real.w

        dual.set(
            0.5f * (tx * rw + ty * rz - tz * ry),
            0.5f * (-tx * rz + ty * rw + tz * rx),
            0.5f * (tx * ry - ty * rx + tz * rw),
            0.5f * (-tx * rx - ty * ry - tz * rz)
        )
    }

    private fun logTwist(omega: Vector3f, velocity: Vector3f) {
        val rotation = Quaternionf(real).normalize()
        if (rotation.w < -EPSILON) {
            rotation.negateComponents()
        }

        val imagLen = sqrt(rotation.x * rotation.x + rotation.y * rotation.y + rotation.z * rotation.z)
        val translation = getTranslation(TEMP_VEC_3.get())
        if (imagLen < EPSILON) {
            omega.set(0f, 0f, 0f)
            velocity.set(translation)
            return
        }

        val theta = 2.0 * atan2(imagLen.toDouble(), rotation.w.toDouble())
        val thetaF = theta.toFloat()
        omega.set(rotation.x / imagLen, rotation.y / imagLen, rotation.z / imagLen).mul(thetaF)

        val omegaCrossP = Vector3f(omega).cross(translation)
        val omegaCrossOmegaCrossP = Vector3f(omega).cross(omegaCrossP)
        val coeff = ((1.0 - theta * rotation.w / (2.0 * imagLen)) / (theta * theta)).toFloat()

        velocity.set(translation)
            .fma(-0.5f, omegaCrossP)
            .fma(coeff, omegaCrossOmegaCrossP)
    }

    private fun fromTwist(omega: Vector3f, velocity: Vector3f, dest: DualQuaternionf): DualQuaternionf {
        val theta = omega.length()
        if (theta < EPSILON) {
            return dest.set(Quaternionf(), velocity)
        }

        val axis = Vector3f(omega).mul(1f / theta)
        val rotation = Quaternionf().fromAxisAngleRad(axis, theta)

        val omegaCrossV = Vector3f(omega).cross(velocity)
        val omegaCrossOmegaCrossV = Vector3f(omega).cross(omegaCrossV)
        val thetaD = theta.toDouble()
        val a = ((1.0 - cos(thetaD)) / (thetaD * thetaD)).toFloat()
        val b = ((thetaD - sin(thetaD)) / (thetaD * thetaD * thetaD)).toFloat()
        val translation = Vector3f(velocity)
            .fma(a, omegaCrossV)
            .fma(b, omegaCrossOmegaCrossV)

        return dest.set(rotation, translation)
    }

override fun toString(): String

    override fun toString(): String =
        "DualQuaternionf[real=(${"%.4f".format(real.x)}, ${"%.4f".format(real.y)}, ${"%.4f".format(real.z)}, ${"%.4f".format(real.w)}), " +
            "dual=(${"%.4f".format(dual.x)}, ${"%.4f".format(dual.y)}, ${"%.4f".format(dual.z)}, ${"%.4f".format(dual.w)})]"

companion object

    companion object {
        private const val EPSILON = 1e-6f

        private val TEMP_VEC = ThreadLocal.withInitial { Vector3f() }
        private val TEMP_VEC_2 = ThreadLocal.withInitial { Vector3f() }
        private val TEMP_VEC_3 = ThreadLocal.withInitial { Vector3f() }
        private val TEMP_DQ_A = ThreadLocal.withInitial { DualQuaternionf() }
        private val TEMP_DQ_B = ThreadLocal.withInitial { DualQuaternionf() }
        private val TEMP_DQ_C = ThreadLocal.withInitial { DualQuaternionf() }
    }
}

private fun quatDot(a: Quaternionf, b: Quaternionf): Float =
    a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w

private fun Quaternionf.scaleComponents(scale: Float): Quaternionf {
    set(x * scale, y * scale, z * scale, w * scale)
    return this
}

private fun Quaternionf.fmaComponents(scale: Float, other: Quaternionf): Quaternionf {
    set(x + scale * other.x, y + scale * other.y, z + scale * other.z, w + scale * other.w)
    return this
}

private fun Quaternionf.addComponents(other: Quaternionf): Quaternionf {
    set(x + other.x, y + other.y, z + other.z, w + other.w)
    return this
}

private fun Quaternionf.negateComponents(): Quaternionf {
    set(-x, -y, -z, -w)
    return this
}