ClothSimulationExample.kt
src/jvmMain/kotlin/afield/examples/ClothSimulationExample.kt · 162 lines
package afield.examples
import afield.simulation.physics.Cloth
import afield.simulation.physics.SimdVerlet
import afield.simulation.physics.addCloth
import afield.geometry.path.FLine
import afield.math.linalg.vec
object ClothSimulationExample
Demo of Cloth — the reusable grid-cloth builder over SimdVerlet's 4-slot distance
constraints. All of the physics lives in Cloth now; what is left here is the part that is
genuinely example-shaped: standing up a simulation, wrapping the frame loop so the grabbed
particle gets a red crosshair, and the process-level "most recent simulation" handle that
lets an interactive box call grabNearest without holding one.
object ClothSimulationExample {
@Volatile
private var activeSimulation: InteractiveClothSimulation? = null
class InteractiveClothSimulation internal constructor
Interactive cloth simulation handle with externally callable grab controls.
class InteractiveClothSimulation internal constructor(
val cloth: Cloth,
val frames: Sequence<List<FLine>>
) {
val sim: SimdVerlet get() = cloth.sim
fun grabNearest(worldX: Float, worldY: Float, worldZ: Float): Boolean
fun grabNearest(worldX: Float, worldY: Float, worldZ: Float): Boolean =
cloth.grabNearest(worldX, worldY, worldZ) >= 0
fun grabNearest(worldX: Float, worldY: Float): Boolean = grabNearest(worldX, worldY, 0f)
fun moveGrabbed(worldX: Float, worldY: Float, worldZ: Float): Boolean
fun moveGrabbed(worldX: Float, worldY: Float, worldZ: Float): Boolean =
cloth.moveGrabbed(worldX, worldY, worldZ)
fun moveGrabbed(worldX: Float, worldY: Float): Boolean = moveGrabbed(worldX, worldY, 0f)
fun releaseGrabbed(): Boolean = cloth.releaseGrabbed()
fun grabNearest(worldX: Float, worldY: Float, worldZ: Float): Boolean
Convenience passthrough for external callers that do not hold the simulation handle. Routes to the most recently created interactive simulation.
fun grabNearest(worldX: Float, worldY: Float, worldZ: Float): Boolean =
activeSimulation?.grabNearest(worldX, worldY, worldZ) ?: false
fun grabNearest(worldX: Float, worldY: Float): Boolean
fun grabNearest(worldX: Float, worldY: Float): Boolean =
grabNearest(worldX, worldY, 0f)
fun moveGrabbed(worldX: Float, worldY: Float, worldZ: Float): Boolean
fun moveGrabbed(worldX: Float, worldY: Float, worldZ: Float): Boolean =
activeSimulation?.moveGrabbed(worldX, worldY, worldZ) ?: false
fun moveGrabbed(worldX: Float, worldY: Float): Boolean
fun moveGrabbed(worldX: Float, worldY: Float): Boolean =
moveGrabbed(worldX, worldY, 0f)
fun releaseGrabbed(): Boolean
fun releaseGrabbed(): Boolean =
activeSimulation?.releaseGrabbed() ?: false
fun createClothSimulation
Backward-compatible API that returns only the frame sequence.
fun createClothSimulation(
clothWidth: Int = 20,
clothHeight: Int = 20,
spacing: Float = 0.5f,
enableCurvatureForce: Boolean = true,
curvatureStrength: Float = 0.05f
): Sequence<List<FLine>> =
createInteractiveClothSimulation(
clothWidth = clothWidth,
clothHeight = clothHeight,
spacing = spacing,
enableCurvatureForce = enableCurvatureForce,
curvatureStrength = curvatureStrength
).frames
fun createInteractiveClothSimulation
Create and run a cloth simulation with externally callable interaction controls.
fun createInteractiveClothSimulation(
clothWidth: Int = 20,
clothHeight: Int = 20,
spacing: Float = 0.5f,
enableCurvatureForce: Boolean = true,
curvatureStrength: Float = 0.05f
): InteractiveClothSimulation {
val sim = SimdVerlet(SimdVerlet.State3.empty())
val cloth = sim.addCloth(
width = clothWidth,
height = clothHeight,
spacing = spacing,
stiffness = 0.8f,
curvatureStrength = if (enableCurvatureForce) curvatureStrength else 0f
)
val frames = sequence {
while (true) {
cloth.step(dt = 1f / 60f, damping = 0.99f, gravity = 0f, iterations = 8)
yield(cloth.lines() + grabHighlight(cloth))
}
}
val interactiveSimulation = InteractiveClothSimulation(cloth, frames)
activeSimulation = interactiveSimulation
return interactiveSimulation
}
A red cross on the grabbed particle, or nothing.
private fun grabHighlight(cloth: Cloth): List<FLine> {
val index = cloth.grabbedIndex
if (index < 0) return emptyList()
val state = cloth.sim.state
val pos = vec(state.x[index], state.y[index], state.z[index])
val line = FLine()
line.moveTo(pos.x - 0.2f, pos.y, pos.z)
line.lineTo(pos.x + 0.2f, pos.y, pos.z)
line.moveTo(pos.x, pos.y - 0.2f, pos.z)
line.lineTo(pos.x, pos.y + 0.2f, pos.z)
line.properties["color"] = vec(1, 0, 0, 1)
line.properties["pointSize"] = 8f
return listOf(line)
}
fun main(args: Array<String>)
@JvmStatic
fun main(args: Array<String>) {
println("Cloth Simulation Example - demonstrating afield.simulation.physics.Cloth")
println("Running simulation for 100 frames...")
val simulation = createInteractiveClothSimulation(
clothWidth = 15,
clothHeight = 15,
spacing = 0.5f,
enableCurvatureForce = true,
curvatureStrength = 0.05f
)
val cloth = simulation.cloth
val iterator = simulation.frames.iterator()
// Grab a corner and drag it, so the PID controller and the constraint solve both get
// exercised rather than just the initial rest state.
val grabbed = cloth.grabNearest(cloth.position(0, 0).x, cloth.position(0, 0).y, 0f)
println("Grabbed particle $grabbed of ${cloth.count}")
repeat(100) { frame ->
val lines = iterator.next()
cloth.moveGrabbed(-1f + frame * 0.01f, -1f, 0.5f)
if (frame % 10 == 0) {
val corner = cloth.position(0, 0)
println("Frame $frame: ${lines.size} FLines, corner at $corner")
}
}
cloth.releaseGrabbed()
println("Simulation complete!")
}
}