Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

SoftBody

Makes an entity a deformable body: a jelly, a cushion, a balloon, a cloth, a soft version of the entity's own model, or a shell that crumples and keeps the dent, like a car body after a crash. The body is a set of particles held together by elastic elements, simulated in the same step as rigid bodies, so it rests on and pushes rigid bodies, rigid bodies rest on, bounce off and dent it, and soft bodies collide with each other.

A soft body is its own body and its own collision surface: it goes on an entity with no Physics and no collider component, and whichever is added second is refused with an error naming the conflict (logged, and readable as the refused component's disabledReason). The body fills the entity's unit box (-0.5 to 0.5 on each axis) times its world scale, so it sits where a default cube or sphere primitive of that scale would. shape = "mesh" instead fills every closed part of the entity's model with tetrahedra of cellSize, and the model is drawn carried by them; a model with no closed part, or a cell size past the 60000-cell cap, is refused with the reason and the fields that change it. While the simulation runs, the entity's position and rotation follow the body as a whole, and a Model on the entity moves with it; the deformed surface is drawn by the physics view (capture with pass = "physics") with the body's elements over it coloured by load and its pinned and attached particles marked, the same lines Physics.debugLines returns and the edit-mode collider overlay draws, and read by Physics.softBodyState (its diagnostics count inverted cells, self-crossings, loaded elements, pinned and attached particles).

Public fields: shape ("box", "sphere", "balloon", "cloth", "mesh", "custom", "rope"), cellSize (for mesh: the cells' edge in metres, 0 takes the model's longest extent divided by resolution), collideWith (for mesh: "cage", the cells' boundary, or "surface", the model's own surface), resolution (particles along the longest edge, 2 to 24), mass (kg; the default 0 weighs it at 1 kg per cubic metre of its shape, like a rigid body), stiffness (Young's modulus in pascals for a box or sphere, default 300; spring stiffness for a balloon or cloth), damping (0 rings on, 1 settles without overshoot), model (a solid's cells: "corotational", the default, "neoHookean" or the cheap "volume"), solver ("constraints", the default, or "fem" for stiff materials, below), poisson (Poisson's ratio, 0 up to 0.5), plasticYield (the strain past which a dent stays; 0, the default, is fully elastic), plasticCreep (how fast per second the strain past the yield sets, default 10), plasticMax (the most permanent strain a part takes, default 0.5), inflation (the share of its built volume a closed shape is held at), shapeMatching (hertz of the pull back toward the body's rest shape as a whole, 0 for none), selfContact (whether the surface collides with itself), repairs, preserveVolume, friction, restitution, collisionMemberships, collisionFilter, canSleep, attachments, pins, the tear fields tearStrain, tearForce, tearSmoothing, interiorToughness, maxTearsPerStep, minPiece and piece, for a custom body particles, elements, cells, triangles and particleRadius (below), for a rope from, to, length, segments, thickness, stretchStiffness, bendStiffness and breakForce (below), and for any body regions and plane (below). The material (stiffness, damping, poisson, the plastic fields, inflation, shapeMatching, preserveVolume, friction, restitution, the collision groups), the solver and repairs take effect on the running body, waking it. Writing shape, resolution, cellSize, collideWith, mass, model, selfContact or canSleep rebuilds the body; in play mode a rebuild of the same particles (a new model, mass or selfContact, or the entity moved while the clock is paused) keeps the deformation that stayed, and one while the simulation runs keeps every particle where it is and how it moves. A reload of this component's code keeps the body as it stands: the reloaded component takes it over, dents, motion and pins included.

Permanent deformation. With plasticYield above 0, a part of the body strained further than the yield flows toward the shape it is held in, so a dent, a crumple or a crease stays once the load is gone: a car shell that keeps its crash, clay that keeps a print, a sheet that keeps a fold. Physics.softBodyState(id).plastic reads how deep it is, in metres; :repair() (or Physics.repairSoftBody(id)) restores the shape the body was built in, the body put back in it where it stands and moving on as it moved. Leaving play for edit mode puts the body back in that shape too.

Writing particles. Physics.softParticles(id, region?) reads a body's particles (indices from 0, world positions, velocities and restPositions, where each stands in the rest shape) and Physics.setSoftParticles(id, particles) writes them, the read taken back as it is: a rewind, a replay, a saved damaged object, a level that starts with something bent, a network correction, one dent pushed out. positions and velocities move the particles and the body springs back toward its rest shape from there; restPositions changes the rest shape, the permanent part of it (a read's restPositions are where each particle rests, which a crumple's elements pull away from, so to keep the shape a body stands in after a crash write its positions as restPositions), which stays once the body settles the way a dent past plasticYield stays (plastic reads it, :repair() undoes it). indices names the particles written, every particle in order without it. The write applies at once: a read straight after sees it, the next step simulates from it, and from Physics.onBeforeStep it is the state that step starts from. Positions are written before the rest places, which are measured through the frame the written positions give; each element takes the rest shape its particles' places give it, which can differ from the one a crumple's elements flowed to, so a deep crumple written back keeps its dent and settles a little way from the written shape, into it or out of it. Rest places that state the shape the body was built in (a read taken before its first dent) put that shape back exactly, the way :repair() does, so a rewind to before a crash replays it the same way every time. A write that places every particle (a whole read written back) is the whole state the body steps on from, so two rewinds to one read fall the same way, and the frame after any write draws the body where it was written, paused or not. A pinned particle stands where it is written, held there by its pin whatever velocity is written to it. A rewind starts from the read alone, while the run the read was taken from carried on with what the solver had built up, so the two can part by rounding where a hit amplifies it (a box balancing on an edge); writing the read back the moment it is taken (from Physics.onBeforeStep) makes that run a replay of it too. Each element of a crumple keeps a flow of its own that a read's rest places do not carry: written back over the body while it still holds the read's dent, a read leaves those elements as they are, and over any other state it gives each element the rest its particles' places give, so a body read with a deep crumple steps on from the two a little apart.

Stiff materials. The default solver (solver = "constraints") sweeps the body's elements with its contacts a few times a substep, which leaves a stiff body (stiffness 1e6 and up: a beam, a chassis rail, a stiff panel) with more give than its material has, and past a point flying apart. solver = "fem" solves the whole body's elasticity at once each substep, so a 2 m beam of stiffness 1e8 held by one end stands out within 3 cm of straight on it where it swings from its pin on the default solver. Contacts, attachments, pins, plasticity, tearing and cutting all work on it, a Joint on one of its regions is answered through the whole body, and Physics.softBodyState(id).solver reads which solver the body runs on. It costs more per step, most for a body resting on many contacts (a resolution-6 box on the ground about 5 to 6 ms on native against 1 to 1.5 ms), so keep it for the parts that need it. The physics guide's "Stiff materials: the FEM solver" has the measured costs and the world settings that tune it (Physics.softBodySettings: femMaxDenseDofs, femMaxIterations, femTolerance).

Tearing and cutting. With tearStrain (a stretch, as a share of an element's rest length) or tearForce (a pull in newtons) above 0, the body tears where it is loaded past it: a cloth rips, a net gives under a heavy ball, a panel comes off a car that hits a wall. Physics.cutSoftBody(id, { point = p, normal = n }) cuts it along a plane (or { a, b, c }, a world-space blade triangle), and Physics.tearSoftBody(id, region) tears it at a region, both in play mode. The torn entity keeps the largest piece, and every piece split off becomes an entity of its own, a copy of the torn one without its children, standing at the root of the scene so it flies free of whatever the torn one hangs from, carrying this component with the same fields, its share of the model, the material, and the velocity, dents and pins that fell on its side; its piece field names the piece its body is. Physics.onSoftBodyTear(fn) hears each tear with the torn entity, the pieces' entities, where and why. A piece is play state: left out of every save, and leaving play takes it away and gives the torn entity back its whole body. tearSmoothing averages the load over time before the test, interiorToughness makes the inside of the body harder to tear than its surface, maxTearsPerStep bounds how many edges tear in one step, and minPiece is the smallest piece a tear splits off. The tear fields take effect on the running body.

A body authored particle by particle (shape = "custom"). A truss, a bridge deck, a crane, a cable net, a soft robot, a car frame of nodes and beams: list the particles and link them with elements of their own. particles is a list of { position = { x, y, z }, mass = kg } (or just positions) in the entity's local space, metres before its scale; elements a list of { a, b, kind = ..., ... } linking particles a and b by their number in that list (from 1). An element's kind is "rod" (the default: it holds its length against a pull and a push), "cable" (a pull only: it goes slack when its ends close in), "strut" (a push only), "spring" or "actuator" (a rod whose length is driven). Each takes its own law in SI units, 0 meaning none: stiffness (newtons per metre; 0 holds the element as stiff as the solver holds anything), damping (newton-seconds per metre, damping its own spring, so it needs a stiffness), maxForce (it gives way at this force and springs back once the load drops), yieldForce (past this force it stays bent: it carries the yield force while its length flows to where the load puts it, up to plasticMax of its length squeezed and plasticMaxStretch stretched, then holds again where it stands (a hit fast enough to carry it past that bound within one physics substep can leave it set a little past it); it gives way as fast as the load moves its ends, and plasticCreep does not pace it; a load it cannot carry takes its ends to that bound, where it stops with a jolt its breakForce has to carry as well as the standing load: a member held rigid stops within one substep, at several times that load), breakForce (it breaks under this pull or push), breakStrain (it breaks stretched past this share of its length), group (a name regions select it by and its breaks report), breakGroup (when one element of the group breaks, every element of it breaks in the same step), collide (rigid and soft bodies hit it as a segment of particleRadius and push its two particles; true by default) and, for an actuator, targetLength and speed (metres, metres per second). cells (tetrahedra, { a, b, c, d }) give it solid material of the body's stiffness and poisson, and triangles ({ a, b, c }) a surface other bodies collide with: a deck a wheel drives on. Pins and attachments hold its particles as they hold any body's, { nearest = point } naming one particle; plane = { normal, point } puts the whole body on a plane and keeps it there, the way a side-on bridge stays flat (a body built flat in the plane: a solid held to one is pressed flat into it). A structure carrying a load heavy against its particles (a vehicle on a bridge) holds its shape on solver = "fem"; the default solver carries such a load along a chain of members only slowly, and the structure sags (the physics guide's "Structures from particles and elements" has the numbers). Physics.softElements(id) (or :elementStates()) reads every element's force (newtons, positive pulling and negative pushing), strain, length, rest length, permanent set, load against its break limit, whether it broke and where its two ends stand (endA, endB, world space), all in one call; Physics.driveSoftElements(id, { [n] = length }, { speed }) (or :drive) drives element lengths, a ram or a muscle. A break removes the element whole, its particles staying; a part it disconnects becomes a piece of its own, and Physics.onSoftBodyTear hears each break with the broken elements in elements (index, group, breakGroup, force, load; the element whose own load broke it first) and the physics step it broke in. The element laws apply to the running body; the particles and the list of links rebuild it. A model on the entity is carried by the cells or the surface triangles; a body of links alone has neither, and is drawn as its elements: each element a round tube of its thickness (0 takes twice particleRadius) with the entity's material, a ball where three or more meet, carried by the particles every frame. An element listed hidden = true is left out of the drawing (a brace or a bending spring that is not seen), and an element that breaks stops being drawn. Physics.softBodyState(id).render reads drawnAs = "elements" and how many elements it draws. After a break, the read of the body's entity and the read of each piece's are the same, every element wherever the break left it, so one read covers the whole structure. A piece a break splits off is a copy of the entity, its other components included, so a component that drives the structure runs once by checking that the SoftBody's piece is empty.

A rope or a cable (shape = "rope"). A tow line, a climbing rope, a power line, a grapple rope, a bridge's cable stays: from and to are its ends in the entity's local space, length its length (0 takes the distance between them, a rope built taut; a longer one is built hanging between them in a catenary), segments the elements along it (it has one particle more), thickness its diameter (what it collides through and the tube it is drawn as) and mass its weight (1 kg a metre when 0). It is built from those as a chain of particles and elements and stepped on the FEM solver: it hangs, drapes and wraps over what it touches, its segments colliding with rigid bodies, terrain and soft bodies. Its segments hold their length both ways, like a chain's links, and bend freely at the particles; stretchStiffness is its axial stiffness E A in newtons (default 1e6, a centimetre-thick polyester rope; 0 as stiff as the solver holds anything), bendStiffness its flexural rigidity E I (0 hangs limp), damping damps its stretch and breakForce snaps a segment past that pull. pins hold its particles in the world and attachments tie them to rigid bodies, { nearest = from } and { nearest = to } naming its ends. Physics.softElements(id).force[k] is the pull in segment k counted from from. It is drawn as a round tube of its thickness with the entity's material. The physics guide's "Ropes and cables" has the measured loads, sags, swings and costs.

Regions with a material of their own. regions gives parts of any soft body their own material: a crumple zone that gives way while the cabin holds, a soft tip on a stiff tool, a padded skin over a hard frame, the weak members of a structure. Each entry is { name, region = { box = { min, max } } | { nearest = point }, stiffness, damping, poisson, plasticYield, plasticCreep, plasticMax, tearStrain, tearForce, tearSmoothing, density }: region selects particles the way attachments and pins do (in the shape the body was built in, in the entity's unit box, for a mesh body the model's own local coordinates and for a custom body those of its particles; a space = "world" region where the body stood in the world when it was built), and an element (a cell, an edge) takes the material when all its particles are inside; on a custom body group = "name" selects the elements of that group instead. On a custom body stiffness and poisson are those of its cells, so a region of links (a group, or a body with no cells) states neither, and is refused naming the field when it does: its links take the stiffness of their own elements. A value an entry leaves out is the body's own, and later entries win over earlier ones. density (kilograms per cubic metre of the region's cells) weighs its particles when the body is built; everything else applies to the running body in place. Physics.softBodyState(id).regions reads each back: its values, the elements and cells that took them, the particles it covers, and plastic, the largest permanent strain of its elements (0 where it stayed elastic). :setRegion(name, values) changes one on the running body.

attachments holds regions of the body to rigid bodies, two-way: a list of { target, region }, target the entity id of a body with Physics, region { box = { min, max } } (every particle between the corners) or { nearest = point } (the one particle nearest the point) in the entity's unit box, space = "world" for world coordinates, empty for the whole body. Each region follows its rigid body and the rigid body carries its weight and pull. Changing the list re-ties only the entries that changed; a rebuilt body is tied again, and removing either side releases the hold. Physics.softBodyState(id).attachments reads each entry back with the force it pulls with, or why it holds nothing. To join soft bodies to each other, or to anchor a joint on a soft body, use a Joint, whose ends each take a region.

Methods: :state(opts?) returns the same report as Physics.softBodyState(entityId, opts). :elementStates() reads every element at once (Physics.softElements), :drive(targets, opts?) drives element lengths (Physics.driveSoftElements), and :setRegion(name, values) changes a region on the running body and returns its entry. :repair() restores the shape the body was built in and returns the new repairs count. :attach(target, region?) adds an attachment and returns its index (the same as Physics.attachSoftRegion); :detach(target?) releases the regions held to target, or all of them, and returns how many it removed.

The body is built at the start of the frame after the component is added. Until then Physics.softBodyState(id).status is "pending" (with exists = false, and waitingFor naming what the build waits on); once built it is "built", and a component that was not built reads "refused" with the reason in refusal.

Physics.softBodyState(id).cage reports a mesh body's fill: cellSize, cached (taken from the cage cache, as a second instance of a model is), buildMs, generateMs, filledParts, strayParts, openParts, seams (springs sewing touching parts together), pieces, enclosedVolume and digest.

local jelly = entity.spawn("jelly")
jelly.position = { 0, 2, 0 }
jelly.component.add("SoftBody", { shape = "box", resolution = 6 })

print(Physics.softBodyState(jelly).status)   -- "pending" in the call that added it

-- on a later frame:
local s = Physics.softBodyState(jelly)
print(s.status, s.particleCount, s.volumeRatio, s.sleeping)   -- "built", 216, ...
-- a cloth whose left edge hangs from a pole
flag.component.add("SoftBody", { shape = "cloth" })
flag.component.get("SoftBody"):attach(pole, { box = { min = { -0.5, -1, -0.5 }, max = { -0.45, 1, 0.5 } } })
print(Physics.softBodyState(flag).attachments[1].forceMagnitude)   -- on a later frame
-- a cloth that rips when pulled past 1.4 times its length
sheet.component.add("SoftBody", { shape = "cloth", resolution = 12, tearStrain = 0.4 })
Physics.onSoftBodyTear(function(e)
    print(e.entity, "tore", e.kind, "at", e.position, "into", #e.pieces + 1, "pieces")
end)

-- a jelly cut in two along the plane x = 0
Physics.cutSoftBody(jelly, { point = vector.create(0, 0, 0), normal = vector.create(1, 0, 0) })
-- a stiff beam held by one end, on the FEM solver
beam.localScale = { 2, 0.2, 0.2 }
beam.component.add("SoftBody", { shape = "box", resolution = 16, stiffness = 1e8, mass = 8, solver = "fem",
    pins = { { name = "root", region = { box = { min = { -0.5, -1, -1 }, max = { -0.45, 1, 1 } } } } } })
print(Physics.softBodyState(beam).solver)   -- "fem"
-- a slab of clay that keeps the print of whatever lands on it
clay.component.add("SoftBody", { shape = "box", stiffness = 3000, plasticYield = 0.02 })
-- on a later frame, after something landed on it and left:
print(Physics.softBodyState(clay).plastic)   -- metres of dent that stayed
clay.component.get("SoftBody"):repair()      -- and gone again
-- a side-on bridge of eight one-metre panels: a top chord that yields past
-- 4 kN, breaks past 9 kN and drops as one, over a stronger deck and web
local particles, elements = {}, {}
for i = 0, 8 do table.insert(particles, { position = { i, 0, 0 }, mass = 20 }) end
for i = 0, 7 do table.insert(particles, { position = { i + 0.5, 1, 0 }, mass = 20 }) end
for i = 1, 8 do table.insert(elements, { i, i + 1, group = "deck" }) end
for i = 1, 7 do
    table.insert(elements, { 9 + i, 10 + i, yieldForce = 4000, plasticMax = 0.03, breakForce = 9000, group = "chord", breakGroup = "chord" })
end
for i = 1, 8 do
    table.insert(elements, { i, 9 + i, group = "web" })
    table.insert(elements, { 9 + i, i + 1, group = "web" })
end
bridge.component.add("SoftBody", {
    shape = "custom", particles = particles, elements = elements, solver = "fem",
    pins = { { name = "left", region = { nearest = { 0, 0, 0 } } }, { name = "right", region = { nearest = { 8, 0, 0 } } } },
    plane = { normal = { 0, 0, 1 }, point = bridge.position },
})
-- each frame: colour the members by load
local e = Physics.softElements(bridge)
for i = 1, e.count do print(i, e.force[i], e.load[i]) end   -- newtons (+ pull, - push), share of break force
Physics.onSoftBodyTear(function(t)
    for _, b in t.elements do print("member", b.index, b.group, "broke at", b.force, "N, step", t.step) end
end)
-- a car shell whose front crumples while the cabin holds
shell.component.add("SoftBody", { shape = "box", resolution = 10, stiffness = 2e5, solver = "fem",
    regions = {
        { name = "crumple", region = { box = { min = { -0.5, -0.5, 0.3 }, max = { 0.5, 0.5, 0.5 } } },
          stiffness = 1e4, plasticYield = 0.01 },
    } })
print(Physics.softBodyState(shell).regions[1].plastic)          -- the crumple zone's permanent strain
shell.component.get("SoftBody"):setRegion("crumple", { stiffness = 2e4 })