KStarbound/src/main/kotlin/ru/dbotthepony/kbox2d/dynamics/joint/DistanceJoint.kt
2022-02-17 11:49:50 +07:00

337 lines
8.4 KiB
Kotlin

package ru.dbotthepony.kbox2d.dynamics.joint
import ru.dbotthepony.kbox2d.api.*
import ru.dbotthepony.kbox2d.api.B2SolverData
import ru.dbotthepony.kbox2d.api.b2Max
import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.math.times
import ru.dbotthepony.kstarbound.util.Color
class DistanceJoint(def: DistanceJointDef) : AbstractJoint(def) {
var stiffness: Double = def.stiffness
var damping: Double = def.damping
var length: Double = b2Max(def.length, b2_linearSlop)
set(value) {
impulse = 0.0
field = b2Max(value, b2_linearSlop)
}
var minLength: Double = b2Max(def.minLength, b2_linearSlop)
set(value) {
lowerImpulse = 0.0
field = b2Clamp(value, b2_linearSlop, maxLength)
}
var maxLength: Double = b2Max(def.maxLength, b2_linearSlop)
set(value) {
upperImpulse = 0.0
field = b2Max(value, minLength)
}
val currentLength: Double get() {
val pA = bodyA.getWorldPoint(localAnchorA)
val pB = bodyB.getWorldPoint(localAnchorB)
val d = pB - pA
return d.length
}
private var bias: Double = 0.0
// Solver shared
private val localAnchorA: Vector2d = def.localAnchorA
private val localAnchorB: Vector2d = def.localAnchorB
private var gamma: Double = 0.0
private var impulse: Double = 0.0
private var lowerImpulse: Double = 0.0
private var upperImpulse: Double = 0.0
private var _currentLength: Double = 0.0
// Solver temp
private var indexA: Int = 0
private var indexB: Int = 0
private var u: Vector2d = Vector2d.ZERO
private var rA: Vector2d = Vector2d.ZERO
private var rB: Vector2d = Vector2d.ZERO
private var localCenterA: Vector2d = Vector2d.ZERO
private var localCenterB: Vector2d = Vector2d.ZERO
private var invMassA: Double = 0.0
private var invMassB: Double = 0.0
private var invIA: Double = 0.0
private var invIB: Double = 0.0
private var softMass: Double = 0.0
private var mass: Double = 0.0
override fun initVelocityConstraints(data: B2SolverData) {
indexA = bodyA.islandIndex
indexB = bodyB.islandIndex
invMassA = bodyA.invMass
invMassB = bodyB.invMass
invIA = bodyA.rotInertiaInv
invIB = bodyB.rotInertiaInv
localCenterA = bodyA.sweep.localCenter
localCenterB = bodyB.sweep.localCenter
val cA = data.positions[indexA].c
val aA = data.positions[indexA].a
var vA = data.velocities[indexA].v
var wA = data.velocities[indexA].w
val cB = data.positions[indexB].c
val aB = data.positions[indexB].a
var vB = data.velocities[indexB].v
var wB = data.velocities[indexB].w
val qA = Rotation(aA)
val qB = Rotation(aB)
rA = b2Mul(qA, localAnchorA - localCenterA)
rB = b2Mul(qB, localAnchorB - localCenterB)
u = cB + rB - cA - rA
// Handle singularity.
_currentLength = u.length
if (_currentLength > b2_linearSlop) {
u *= 1.0f / _currentLength
} else {
u = Vector2d.ZERO
mass = 0.0
impulse = 0.0
lowerImpulse = 0.0
upperImpulse = 0.0
}
val crAu = b2Cross(rA, u)
val crBu = b2Cross(rB, u)
var invMass = invMassA + invIA * crAu * crAu + invMassB + invIB * crBu * crBu
mass = if (invMass != 0.0) 1.0 / invMass else 0.0
if (stiffness > 0.0 && minLength < maxLength) {
// soft
val C = _currentLength - length
val d = damping
val k = stiffness
// magic formulas
val h = data.step.dt
// gamma = 1 / (h * (d + h * k))
// the extra factor of h in the denominator is since the lambda is an impulse, not a force
gamma = h * (d + h * k)
gamma = if (gamma != 0.0) 1.0 / gamma else 0.0
bias = C * h * k * gamma
invMass += gamma
softMass = if (invMass != 0.0) 1.0 / invMass else 0.0
} else {
// rigid
gamma = 0.0
bias = 0.0
softMass = mass
}
if (data.step.warmStarting) {
// Scale the impulse to support a variable time step.
impulse *= data.step.dtRatio
lowerImpulse *= data.step.dtRatio
upperImpulse *= data.step.dtRatio
val P = (impulse + lowerImpulse - upperImpulse) * u
vA -= invMassA * P
wA -= invIA * b2Cross(rA, P)
vB += invMassB * P
wB += invIB * b2Cross(rB, P)
} else {
impulse = 0.0
}
data.velocities[indexA].v = vA
data.velocities[indexA].w = wA
data.velocities[indexB].v = vB
data.velocities[indexB].w = wB
}
override fun solveVelocityConstraints(data: B2SolverData) {
var vA = data.velocities[indexA].v
var wA = data.velocities[indexA].w
var vB = data.velocities[indexB].v
var wB = data.velocities[indexB].w
if (minLength < maxLength) {
if (stiffness > 0.0) {
// Cdot = dot(u, v + cross(w, r))
val vpA = vA + b2Cross(wA, rA)
val vpB = vB + b2Cross(wB, rB)
val Cdot = b2Dot(u, vpB - vpA)
var impulse = -softMass * (Cdot + bias + gamma * impulse)
impulse += impulse
val P = impulse * u
vA -= invMassA * P
wA -= invIA * b2Cross(rA, P)
vB += invMassB * P
wB += invIB * b2Cross(rB, P)
}
// lower
run {
val C = _currentLength - minLength
val bias = b2Max(0.0, C) * data.step.inv_dt
val vpA = vA + b2Cross(wA, rA)
val vpB = vB + b2Cross(wB, rB)
val Cdot = b2Dot(u, vpB - vpA)
var impulse = -mass * (Cdot + bias)
val oldImpulse = lowerImpulse
lowerImpulse = b2Max(0.0, lowerImpulse + impulse)
impulse = lowerImpulse - oldImpulse
val P = impulse * u
vA -= invMassA * P
wA -= invIA * b2Cross(rA, P)
vB += invMassB * P
wB += invIB * b2Cross(rB, P)
}
// upper
run {
val C = maxLength - _currentLength
val bias = b2Max(0.0, C) * data.step.inv_dt
val vpA = vA + b2Cross(wA, rA)
val vpB = vB + b2Cross(wB, rB)
val Cdot = b2Dot(u, vpA - vpB)
var impulse = -mass * (Cdot + bias)
val oldImpulse = upperImpulse
upperImpulse = b2Max(0.0, upperImpulse + impulse)
impulse = upperImpulse - oldImpulse
val P = -impulse * u
vA -= invMassA * P
wA -= invIA * b2Cross(rA, P)
vB += invMassB * P
wB += invIB * b2Cross(rB, P)
}
} else {
// Equal limits
// Cdot = dot(u, v + cross(w, r))
val vpA = vA + b2Cross(wA, rA)
val vpB = vB + b2Cross(wB, rB)
val Cdot = b2Dot(u, vpB - vpA)
var impulse = -mass * Cdot
impulse += impulse
val P = impulse * u
vA -= invMassA * P
wA -= invIA * b2Cross(rA, P)
vB += invMassB * P
wB += invIB * b2Cross(rB, P)
}
data.velocities[indexA].v = vA
data.velocities[indexA].w = wA
data.velocities[indexB].v = vB
data.velocities[indexB].w = wB
}
override fun solvePositionConstraints(data: B2SolverData): Boolean {
var cA = data.positions[indexA].c
var aA = data.positions[indexA].a
var cB = data.positions[indexB].c
var aB = data.positions[indexB].a
val qA = Rotation(aA)
val qB = Rotation(aB)
val rA = b2Mul(qA, localAnchorA - localCenterA)
val rB = b2Mul(qB, localAnchorB - localCenterB)
var u = cB + rB - cA - rA
u.isFiniteOrThrow {
"u is invalid, $cB, $rB, $cA, $rA"
}
val length = u.length
u = u.normalized
val C: Double
if (minLength == maxLength) {
C = length - minLength
} else if (length < minLength) {
C = length - minLength
} else if (maxLength < length) {
C = length - maxLength
} else {
return true
}
val impulse = -mass * C
val P = impulse * u
P.isFiniteOrThrow {
"P is not finite, impulse: $impulse, u: $u, mass: $mass, C: $C"
}
cA -= invMassA * P
aA -= invIA * b2Cross(rA, P)
cB += invMassB * P
aB += invIB * b2Cross(rB, P)
data.positions[indexA].c = cA
data.positions[indexA].a = aA
data.positions[indexB].c = cB
data.positions[indexB].a = aB
return b2Abs(C) < b2_linearSlop
}
override fun getReactionForce(inv_dt: Double): Vector2d {
return inv_dt * (impulse + lowerImpulse - upperImpulse) * u
}
override fun getReactionTorque(inv_dt: Double): Double {
return 0.0
}
override val anchorA: Vector2d get() = bodyA.getWorldPoint(localAnchorA)
override val anchorB: Vector2d get() = bodyB.getWorldPoint(localAnchorB)
override fun draw(draw: IDebugDraw) {
val pA = b2Mul(bodyA.transform, localAnchorA)
val pB = b2Mul(bodyB.transform, localAnchorB)
val axis = (pB - pA).normalized
draw.drawSegment(pA, pB, c4)
val pRest = pA + length * axis
draw.drawPoint(pRest, 8.0, c1)
if (minLength != maxLength) {
if (minLength > b2_linearSlop) {
val pMin = pA + minLength * axis
draw.drawPoint(pMin, 4.0, c2)
}
if (maxLength < Double.MAX_VALUE) {
val pMax = pA + maxLength * axis
draw.drawPoint(pMax, 4.0, c3)
}
}
}
companion object {
private val c1 = Color(0.7f, 0.7f, 0.7f)
private val c2 = Color(0.3f, 0.9f, 0.3f)
private val c3 = Color(0.9f, 0.3f, 0.3f)
private val c4 = Color(0.4f, 0.4f, 0.4f)
}
}