Marrow
Volumetric soft bodies for Blender, solved on the GPU and ready for the shot.
Marrow fills a mesh with a tetrahedral cage, solves a stable co-rotational XPBD system in GLSL compute shaders, and deforms the original render mesh by barycentric interpolation from that cage. Get believable volume, contact, muscle fibers and rig-driven motion without replacing your topology or compiling a platform-specific extension.
Diagnostic key
Find the symptom. The reading is what the solver is actually doing.
| Symptom | Reading | Turn to |
|---|---|---|
| A tentacle tip or fingertip stretches into a spike | Those render vertices have no tet of their own and are clipped onto the nearest one | Cage coverage |
| The body turns inside out on the first frame | It started overlapping a sticky collider; buried nodes were seized and welded to scattered faces | Sticky colliders |
| The body launches violently at the start | It started below the ground plane. A straddling ball left its first substep at 226 m/s | Ground plane |
| Fibers refuse to bake, and nothing says why | The curve has a bevel or Cyclic U, so it resolves to a tube or a ring, not one open path | Fiber |
| The wave reads like clockwork | Noise is at 0, which is the exact periodic wave | Noise |
| The body squeezes but will not bend | Bend is at 0, so the whole cross-section contracts together | Bending |
| Stiffness Group has no effect | The solver rebuilds on a return to the start frame, not on the edit | Per-region stiffness |
| The body simulates where the object used to be | The cage is built in a world frame; it rebases on the next rebuild | Moving the object |
| Marrow refuses to restart after a scale | Resizing the cage under an older rest shape would change mass and what Stiffness means | Moving the object |
| The mesh is deformed twice, or trails a spike | A Hook or Armature is still shown; modifiers are muted only while Attachment is on | Attachment |
| A pinned region will not follow its Empty | Follows Animation is off, or Attach Stiffness is above 0 and holding the body back | Follows Animation |
| Material fails under a press you wanted intact | A volume-preserving squash stretches sideways, and lateral stretch counts | Failure |
| A bake is gone after reopening the file | The mesh was edited or re-tetrahedralized, so the cache no longer fits and was refused | Baked caches |
| The body passes through itself where it folds | Self Collision is off, or Thickness is below about 1.0 and folds slip between nodes | Self collision |
| The body sinks into a collider that deforms | Its Shape is Mesh, which is captured once at the start and never re-baked. Set it to Animated Mesh | Animated Mesh |
| An Animated Mesh collider stops the simulation | Its vertex or triangle connectivity changed, or its scale was animated through zero | Animated Mesh |
| Stick Break lets go on first touch, or never | Older versions read it every substep, where the impact transient swamps the pull. It is now read once a frame | Sticky colliders |
| Self Collision acts as if it were off | Thickness is below 0.71, so a surface point can sit between nodes and be missed by every pair | Self collision |
| Blender freezes for a minute when Live goes on | A mesh collider is baking its distance field; it is kept beside the .blend afterwards | Colliders |
| A sticky collider will not hold anything | Something depenetrated the node first, so check whether the ground plane sits at or above the sticky surface | Sticky colliders |
| Every GPU test in the suite fails | The suite was pointed at Blender 4.5, which has no GPU context in background mode | Development |
What Marrow is
Pure Python against Blender's bundled interpreter, with no compiled extension, no external dependency and no per-platform build.
The cage is a separate hidden object, and the render mesh is deformed by interpolation from it, so UVs, shape keys and material slots come through untouched, and De-tetrahedralize gives you back the mesh you modelled, bit for bit.
Install
blender --command extension install-file -r user_default --enable dist/marrow-2.23.1.zip
Or in Blender: Edit > Preferences > Get Extensions > Install from Disk.
Building from source
blender --command extension build --source-dir marrow --output-dir dist
Quick start
- Select a mesh. Open the Marrow tab in the 3D viewport sidebar (
N). - Set Resolution and press Tetrahedralize. A hidden wire cage is created and parented to your object.
- Optionally add something to collide against: select it, then the soft body, and under Simulation > Colliders press +. Toggle the magnet on its row to make it sticky, so material it touches is dragged along with it rather than only pushed away.
- Press play.
That is the whole loop. Live is on by default: each frame is simulated as the timeline reaches it and cached, so scrubbing back is free and a second pass costs nothing.
Tetrahedralize takes the whole selection. Select several meshes and it cages every one of them, in name order, under a single progress bar: a rig of twenty muscles is one press rather than twenty. Each object keeps its own Resolution, Shell and fiber settings; the batch only decides which objects get built, never how. Selected cages are skipped, so re-running over a selection that includes them is safe. There is still one solver per body: what makes several bodies act as one system is colliding two bodies and Glue, not the way they were tetrahedralized.
A batch is partial-then-report: an object that cannot be filled is named in the report and the rest still get their cages, rather than one bad mesh costing the whole selection. Only when nothing at all succeeded does it come back as an error. Esc cancels the batch at whatever object it had reached, keeping the cages already built.
To change how it behaves, edit a setting and return to the start frame. That restarts the simulation without freeing the cache or rebaking it. Stiffness, Volume Preservation and the fiber settings are kernel uniforms, so they land on the next frame without a restart and can be driven from a rig. Measured on an RTX 5050, a live frame costs 1 to 2 ms from a 125-node cage up to a 2,744-node one, so the edit loop stays interactive.
Material recipes
Recipe sits just below Cage, and is available before the first Tetrahedralize. Choose one and press Apply to set Stiffness, Volume Preservation, Damping and Substeps together. These are starting points, not locked modes: edit any slider afterwards. They do not change Resolution, contacts, gravity or the cage.
| Recipe | Stiffness | Volume | Damping | Substeps | Starting point |
|---|---|---|---|---|---|
| Balanced | 10,000 | 100,000 | 0.999 | 10 | Original defaults |
| Gel | 2,000 | 40,000 | 0.997 | 12 | Soft gel |
| Rubber | 8,000 | 140,000 | 0.9995 | 12 | Elastic, lightly damped |
| Foam | 50,000 | 500,000 | 0.995 | 16 | Firm, faster decay |
| Wobble | 1,500 | 25,000 | 0.9995 | 16 | Soft, lightly damped |
| Soft Tissue | 4,000 | 90,000 | 0.998 | 14 | Medium-soft, volume-supporting |
| Sponge | 1,500 | 15,000 | 0.975 | 18 | Soft, strongly damped |
| Silicone | 30,000 | 300,000 | 0.9995 | 18 | Firm, lightly damped |
Return to the start frame to restart Live with the new values; for an existing bake, Free This Body and bake again. Damping means velocity retained per substep, so nearer 1 is less damped. The toolbar says Bake All Bodies because it bakes every Marrow body in the scene, while Free This Body only frees the active body's cache.
Grabbing the body
While a live simulation is running, press Drag in the Marrow panel, then click and hold on the body in the 3D viewport. The cage node nearest the cursor is pulled towards the mouse with a firm, springy grip while the simulation continues stepping. Release it and the body keeps the pose and momentum left by the fling.
Any live body in the scene, not just the selected one. One press of Drag arms the whole scene: each click grabs whichever live body is under the cursor, so you can throw one, catch another and pile them up without going back to the outliner. Where two bodies overlap on screen the front one wins. The rest of the scene keeps simulating while you hold a body: a drag freezes the timeline, so every live body is stepped by the drag's own tick, and bodies that collide or are glued still advance together as one group.
The button stays lit while a drag session is running, the way Live does, so it is clear that Drag is armed and that Esc ends it.
The drag plane faces the view through the pick point. A pin, collider or the ground plane still wins on the grabbed node, so pulling a body into a wall squashes it instead of clipping through. Drag needs a live simulation; the control is disabled without one.
Dragging empty space orbits the view. Wheel zoom, middle-mouse orbit and numpad views keep working until a body is actually held; navigation pauses during a live grab so the drag plane stays aligned with the view used to pick it.
Getting your object back
De-tetrahedralize, in the Cage box, restores the shape you modelled, deletes the cage, strips every attribute Marrow wrote and releases the session. The mesh comes back bit-identical.
It works because Tetrahedralize records the rest shape in a marrow_rest attribute first. That also means re-tetrahedralizing after playing the timeline rebuilds from the original shape rather than from the deformed pose, so changing Resolution mid-project is safe.
Free Bake is not De-tetrahedralize. Free Bake discards the cache and the GPU memory and leaves the object tetrahedralised and deformed, ready to simulate again.
Editing the mesh's topology between the two invalidates the stored shape, since the attribute is per-point. Re-tetrahedralize after any Edit Mode change.
Example scenes
Seven ready-made .blend files ship in demos/. Each opens at frame 1 with a camera, lighting, materials, a selected Marrow body and Live on.
| File | What it shows |
|---|---|
animated_hand_squeeze.blend | A low-poly hand proxy rigged to an Armature curls four fingers and a thumb around a soft cube, squeezes it to about 87% of its width and releases. 120 frames at 24 fps, friction 0.5. See Animated Mesh. |
soft_body_drop_contact.blend | A deformable impactor strikes a pinned, volumetric cushion beside an identical no-impact control. Both bodies deform through two-way body contact. The display pedestals are not colliders. |
pinned_hanging_beam.blend | An open curve bakes longitudinal fibers into a pinned beam; keyed wave phase contracts it beside a weightless, fiber-free control. |
fiber_showcase.blend | Five lanes compare longitudinal, radial, helical, a travelling bend wave and a passive control. Play from frame 1 and inspect frame 28. |
force_fields_showcase.blend | Force, Wind, Vortex and Turbulence panels beside an unpowered control, with ghost outlines of the start positions. Gravity and contact are off; play through frame 80. |
multipin_creature.blend | Four Hook controllers drive four painted pin regions on one connected soft creature. The floor is illustrative, not a collider: a choreographed gait, not simulated traction. Play through frame 108. |
octopus_multipin_showcase.blend | Eight pinned tentacle tips follow independent Hook targets while the body deforms under gravity and Marrow's actual ground collision. Tip placement is choreographed, not autonomous walking. Play through frame 128. |
Press Play from frame 1. The physics has frame history, so jumping straight to a later frame is not equivalent. Each file carries its instructions in a README Text datablock. These are live examples, not baked caches. The tools/make_demo_*.py and tools/verify_demo_*.py scripts regenerate each scene and check its motion against its control.
Cage coverage
The cage keeps a cell when its centre is inside the mesh, and that alone loses anything thinner than a cell. A tentacle tip, a fingertip, a horn: no cell centre ever lands inside one, so the cage stops short. Those render vertices get no tet of their own; binding clips them onto whichever tet was nearest and they ride it, which is what stretches a tip into a spike or collapses it to a point.
Cells holding a mesh vertex are therefore kept too, whether or not their centre is inside; part 5 in Plate I. That covers a thin feature exactly where the mesh carries the detail to describe it, and it costs one pass over vertices the voxeliser has already loaded, with no extra ray casts and no change in Resolution.
| Shape | Resolution | Tets before | Tets after | Cost | Outside before | Outside after |
|---|---|---|---|---|---|---|
| Cube | 0.25 | 2,560 | 2,560 | 0% | 0% | 0% |
| UV sphere | 0.15 | 6,220 | 7,755 | +25% | 50% | 0% |
| Suzanne | 0.12 | 6,235 | 7,160 | +15% | 60% | 0% |
| Tapered tentacle | 0.05 | 7,655 | 9,880 | +29% | 58% | 0% |
A cube pays nothing: its cells already cover it. Curved and thin shapes pay 15–30% more tets and go from half their render vertices being dragged to none of them. Reaching the same coverage on that tentacle by Resolution alone took 0.02 and 121k tets, sixteen times the cage for the same result.
The fix reaches further than thin features. Half the surface vertices of any curved mesh used to sit outside the cage and be clipped onto it rather than interpolated inside it.
The Adaptive cage is gone
This replaced an Adaptive octree cage that used to live in the Cage box. Its job was to spend fine cells only where a shape is thin, and vertex cells do that job for nothing, so it was measured against the uniform fill it was built to beat and lost on every axis. On a solid sphere at its own best settings it wanted 31,726 nodes against 23,871 for a uniform cage at the same surface size, took 5× as long to build, and left 49% of render vertices outside the cage because the octree never got vertex cells of its own. It is gone, along with the hanging-node blend pass that existed only to hold its cages together.
A .blend saved with an adaptive cage is refused with a message asking for a rebuild rather than simulated without that glue, which would quietly tear the cage apart. Existing cages keep the old behaviour until you Tetrahedralize again.
Shell
The inside of a body is never seen. Nothing renders it, nothing collides with it, and every tet in it costs exactly what a tet at the skin costs. Shell fills only that many cells inwards from the surface and leaves the core empty. 0, the default, fills solid as Marrow always has.
| Shape | Resolution | Shell | Tets | Nodes | ms/frame |
|---|---|---|---|---|---|
| UV sphere | 0.10 | 0 (solid) | 23,600 | 5,789 | 1.08 |
| UV sphere | 0.10 | 1 | 5,840 | 3,012 | 0.77 |
| UV sphere | 0.10 | 2 | 10,040 | 3,702 | 0.95 |
| Suzanne | 0.08 | 0 (solid) | 24,065 | 6,211 | 1.07 |
| Suzanne | 0.08 | 1 | 8,950 | 4,044 | 0.97 |
| Cylinder | 0.08 | 0 (solid) | 13,680 | 3,624 | 1.02 |
| Cylinder | 0.08 | 1 | 4,425 | 2,309 | 0.71 |
| UV sphere | 0.05 | 0 (solid) | 176,340 | 39,571 | 1.36 |
| UV sphere | 0.05 | 1 | 21,720 | 12,284 | 0.74 |
The coarse cages barely move. At 24,000 tets a frame is mostly dispatch overhead, so dropping three quarters of the material buys 0.3 ms and no more. The last pair is the one to read: at Resolution 0.05 the same sphere goes from 176,340 tets to 21,720, and from 1.36 ms to 0.74. Shell is what makes a fine cage affordable, not what makes a coarse one faster. Use it when Resolution is what the shape needs and the tet count is what stops you.
A shell keeps every cell holding a render vertex, whatever the erosion says. A vertex can sit a full cell inside the occupancy boundary, since the cage keeps a cell by its centre, and a vertex whose own cell was dropped binds to whichever tet was nearest instead of the one containing it: the same clipping Cage coverage exists to prevent. Measured on Suzanne at Resolution 0.08 and Shell 1, without that rule the worst render vertex sat 0.104 outside the cage; with it, none sit outside at all. It is also why a shell of a thin feature is the feature itself. A body thinner than the shell everywhere erodes to nothing and comes back solid, so Shell can never empty a cage a solid fill would have filled.
A hollow body is softer
There is less material holding the shape, and the rind can buckle inwards where a solid core would have stopped it.
| Setup | Height kept |
|---|---|
| Solid, 10 substeps | 0.970 |
| Shell 1, 10 substeps | 0.737 |
| Shell 1, 30 substeps | 0.861 |
| Shell 1, Stiffness ×4, 30 substeps | 0.948 |
| Shell 1, Stiffness ×4, 10 substeps | 0.218 |
Raise Substeps before Stiffness, and never raise Stiffness without it. The last row is not a typo: stiffer constraints at a fixed iteration budget converge less, and an under-converged rind folds where a solid body could not, so the body that was asked to be four times stiffer ended up three times flatter. Self collision helps the same buckling for less: on at the default 10 substeps it measured 0.752, and at 30 substeps 0.915.
Fibers in a shell
Fibers still work in a shell, and a radial one works less hard.
| Cage | Muscle | Along the axis | Across it |
|---|---|---|---|
| Solid | Longitudinal | 0.89 | 1.17 |
| Solid | Radial | 1.19 | 1.31 |
| Shell 1 | Longitudinal | 0.85 | 1.13 |
| Shell 1 | Radial | 1.11 | 1.33 |
Longitudinal is barely touched. Radial elongates the body by displacing the material it squeezes out of each cross-section along the axis, and a hollow body has less to displace: 1.11 against 1.19, so a bit over half the elongation. A hydrostat that has to reach wants the solid cage.
Mass follows the material. Node mass is volume times density, so a shelled body weighs less than the solid one. It falls at the same rate, since gravity is an acceleration, but it lands on a collider with less momentum and a force field moves it further.
Shell is baked at Tetrahedralize, like Resolution: it decides which cells exist. Change it and tetrahedralize again.
tools/smart_tissue_demo.py builds all three of these on one rig – a shell cage, a glued layer and a driven Stiffness, each against the same body with that one setting off – and prints the table it measured off the scene it just built. Run it from the Scripting tab and press play.
Live or Bake
| Mode | Behaviour |
|---|---|
| Live (default) | Simulates as you play, caching as it goes. Returning to the start frame restarts and re-reads the sliders. Scrubbing below the start frame resets the body to rest. |
| Bake | Simulates every Marrow body in the scene over the whole scene range up front, whichever object is active. The result is then fixed: replaying it, including from the start frame or below it, never re-simulates, so it survives slider changes until you free it. |
| Free | Discards the cache and releases GPU memory. |
Moving the playhead before the start frame resets a live body: the cache is dropped, the mesh snaps back to its rest shape and False Color repaints at rest. Paused or playing makes no difference. A baked body is exempt and keeps replaying its cache.
Turning Live off stops simulation entirely for that object.
A skip of up to 8 frames is caught up, so playback that drops frames does not stall. A larger jump plays whatever is cached and otherwise leaves the mesh where it is, because chasing hundreds of frames inside a frame handler would lock the UI. A body that has never simulated is the exception: with no history to protect, the first jump catches up from the start frame in one go, so pressing play mid-timeline just works.
Baked caches on disk
A bake survives closing the file. It is written to a blendcache_<name> folder beside the .blend when you save, the same convention Blender's own point caches use, and for the same reason: a bake of a real body is hundreds of megabytes of float, and putting that inside the .blend would make every save carry it.
Only the cage node positions are stored. Render positions are barycentric combinations of them and rebuild exactly on load (measured at 1.4e-7 against what the skin kernel produced), so storing both would be storing the same thing twice. On a body with more render vertices than cage nodes, which is the usual shape of things, that is the smaller half.
Only bakes are written. A live cache is disposable by design and is rebuilt as the timeline plays, so writing it on every save would be pure cost.
Reopening validates before trusting: the stored vertex count, tet count and node count all have to match the cage that exists now. Editing the mesh or re-tetrahedralizing leaves a cache that would unpack into the wrong shapes, and playing that back would look like a solver fault rather than a stale file, so it is refused and the body comes back unbaked.
Free deletes the sidecar along with the in-memory cache, and so does De-tetrahedralize. Leaving the file behind would have the next session load back the bake you just discarded. An unsaved .blend has nowhere to put one, so nothing is written until the file has a path.
Every setting
The full panel, in panel order. Material recipes set the first few together.
| Setting | What it does |
|---|---|
| Resolution | Cage cell size in world units. Smaller fills finer detail and costs more. See Cage coverage. |
| Shell | Fill only this many cells inwards from the surface and leave the core hollow. 0 fills solid. See Shell. |
| Substeps | XPBD substeps per frame. More is stabler and slower, and this is the most expensive knob here. Replaced by Iterations when Quasistatic is on. |
| Quasistatic | Solve for the equilibrium shape implied by the pose and discard inertia. No bounce, jiggle or overshoot. See Quasistatic. |
| Iterations | Constraint sweeps per frame in Quasistatic, in place of Substeps. |
| Step Scale | Multiplies the frame length for the quasistatic solve. Leave it at 1.0; values from 2.0 measured unstable. See Quasistatic. |
| Stiffness | Resistance to distortion (co-rotational deviatoric compliance). Re-read every frame, so a driver can own it. See Rig-driven stiffness. |
| Thrust | Acceleration along the current fiber direction, following the body's deformed shape rather than its rest pose. See Thrust. |
| Top Speed | Caps fiber-driven motion by fading Thrust as a node approaches the target speed. |
| Volume Preservation | Resistance to volume change (hydrostatic compliance). Re-read every frame, like Stiffness. |
| Stiffness Group | Vertex group saying where the body is stiff. Scales both sliders above, per tet. See Per-region stiffness. |
| Softest | Stiffness multiplier where that group's weight is 0. |
| Damping | Velocity retained each substep. 1.0 is undamped. |
| Gravity | Multiplies the scene gravity for this body alone. 0 is weightless, negative falls upwards. |
| Force Fields | Collection of Force, Wind, Vortex and Turbulence fields that push this body. See Force fields. |
| Fiber | Contract along baked fiber directions. See Fiber. |
| Curve | Curve running along the body. Its tangent is the fiber direction, its arclength the wave phase. |
| Muscle | How fibers are arranged around the curve: Longitudinal, Radial or Helical. Baked at Tetrahedralize. See Muscle arrangement. |
| Pitch | Lean of a helical fiber. 0 is longitudinal; a right angle is a pure hoop. Helical only. |
| Fiber Stiffness | Resistance to stretch along the fiber, and how hard it pulls. |
| Bend | How much the wave bends the body rather than only squeezing it. 0 is a pure travelling squeeze. See Bending. |
| Amplitude | Peak contraction. 0.3 shortens to 70% of rest length. |
| Wavelength | Distance between crests, along the curve. Floored at 1e-4: both the oracle and the kernel divide by it unguarded, so this is not a soft suggestion. |
| Speed | Cycles per second. Travel velocity is Wavelength × Speed. |
| Phase | Where in the contraction cycle the body sits, in cycles. 0 is rest, 0.5 is fully contracted. Drive it from a bone to make the rig own the contraction. See Rig-driven activation. |
| Waveform | Smooth cosine, or hard on/off square. |
| Noise | Irregularity, 0–1. Jitters when each crest arrives and how hard it bites. 0 is the exact clockwork wave. |
| Friction | Resistance to sliding, for the ground plane, self-collision and body-to-body contact. 0 slides freely. Colliders carry their own value instead. See Friction. |
| Ground Plane / Height | An infinite horizontal plane the body cannot fall through. |
| Failure / Fail Strain | Largest stretch a tet survives, in any direction. 1.5 fails at 1.5× rest length. See Failure. |
| Self Collision | Stop the body passing through itself where it folds. |
| Collide With Bodies | Collide with other Marrow objects that also have it on. Both deform. |
| Thickness | Contact gap for both of the above, as a multiple of Resolution. |
| Attachment | An armature or other deforming modifiers drive the sim from the inside. See Attachment. |
| Attach Stiffness | How hard the flesh follows the animation. 1.0 rides it exactly; lower lags, jiggles and overshoots. |
| Attach Group | Vertex group saying where the flesh is bound. Weight 1 keeps Attach Stiffness as set, weight 0 drops it to Loosest. See Attachment. |
| Loosest | Share of Attach Stiffness where that group's weight is 0. 0 is material the armature never reaches. |
| Glue To | Another Marrow body this one rides, instead of its own modifiers. Uses Attach Stiffness and Attach Group. See Glue. |
| Pin Group / Additional Groups | Vertex groups whose combined weights hold material in place. 1.0 pins solid; below that is a heavier node, not a partial hold. See Pinning. |
| Follows Animation | Let all pinned regions ride the animation instead of staying put. Still rigid. Needs Attachment. |
| Colliders | The collection of objects this body collides against. Shape (Mesh, Animated Mesh, Sphere, Box, Capsule), Sticky and Friction are set on each object. |
| Stick Break | How far material may drag a sticky contact before it lets go. 0 never lets go. |
| False Color | Off / Stretch rainbow display of how far the material is stretched. See False color. |
Mass and density
Mass is a property of the object, not of the cage. Each node carries the volume of material it represents at a fixed density (64 mass units per cubic metre), so an object weighs the same however finely it is tetrahedralized, and Stiffness and Volume Preservation keep their meaning across a Resolution change.
The density is chosen so the average node at the default Resolution of 0.25 weighs the 1 mass unit older versions gave every node. A very fine cage can still read a touch firmer at the same Substeps (a fixed iteration budget converges more constraints less), so raise Substeps if a resolution change needs to match to the last percent.
Colliders
Each collider carries its own Shape. Mesh is the default and uses the object's actual shape, so anything works: a bowl, a hand, a floor with a lip. Sphere and Box are cheaper primitives. Capsule is a pill aligned to local Z: radius comes from X and Y scale and half-length from Z, making it useful for fingers, limbs and rolling pins. Keep X and Y scale equal.
A mesh collider is baked once into a signed distance field, in the object's own local space. That means moving, rotating and scaling it costs nothing: the field rides the transform exactly as the primitives do. It also means concavity works properly: a node in the hole of a torus is correctly outside the solid, which a bounding box or a convex hull would get wrong.
The field's grid tracks your Resolution, since it only needs to resolve detail the cage can represent. There is no separate setting.
Node is outside Sphere. Signed distance +0.00; no correction applied.
The field is baked once and kept
It costs seconds to build, measured at 11 s for a 22 m terrain on the 96-cell grid the bake caps at, so it is written beside the .blend in the same blendcache_<name> folder a bake uses, keyed by a digest of the collider's own vertices. Reopening the file reloads it in a third of a second; editing the collider misses that file and rebakes.
This bake ran at every Blender start and took 75 s inside the frame handler, with no progress and no Esc. Windows marks a window Not Responding after about five seconds, so it read as a crash.
What made it slow was the sign, not the distance: find_nearest costs 0.4 µs a cell and the ray-parity inside test beside it costs 92.8 µs. Parity still decides the sign, but only in a band one cell wide around the surface, which is the only place its accuracy is load-bearing. Everywhere else the answer is whether the cell connects to the outside of the padded box, which no ray can graze.
That is also more accurate than asking parity everywhere. On that terrain it removed 21 of 6,000 sampled cells that parity had called inside while sitting up to 21 cells from any surface, each one with its three rays split two to one.
A Mesh collider is not re-baked. Shape keys, an armature or Geometry Nodes on it are captured once when the simulation starts and never revisited. Transform animation is free; deformation is not. For a deforming contact surface, use Animated Mesh.
Animated Mesh
Animated Mesh is the Shape for an animated hand or any other deforming contact surface. It reads the evaluated mesh once per simulated frame, interpolates its motion across the frame's substeps, and checks fast surface crossings between samples. It pushes only the Marrow body: the collider stays kinematic and is never changed by physics.
It is meant for a closed, outward-facing low-poly collision proxy, not the render mesh. Its vertex and triangle connectivity must stay unchanged: Armatures, Shape Keys and topology-preserving modifiers work, while a topology change stops the simulation and names the collider. A negative object scale is supported, but do not animate through zero into or out of a reflection; the winding change is rejected as a topology change.
Sticky is unavailable in this mode, because a rigid local-space anchor cannot follow skinned triangles. Friction does follow the proxy's surface motion. Bodies sharing one proxy share its evaluated mesh sample each frame, while keeping separate collision history and GPU resources.
In demos/animated_hand_squeeze.blend the hand proxy is parented to the armature object. The Armature modifier deforms in mesh space, so translating the armature alone would not move the evaluated proxy.
Collections, not lists
Colliders come from a collection. The body points at one, and every object in it is a collider, nested collections included, so you can group them however the shot wants.
The quickest way in: select the objects you want to collide against, then the soft body last so it is active, and press + in Simulation > Colliders. Marrow makes a <body> Colliders collection if there is none yet, and links them in. − unlinks the highlighted row. You can also just point the field at a collection you already have, and drag objects in and out of it in the outliner.
Because the settings live on the collider and not on the body, an object dropped into two bodies' collections is described once. Change its Shape and both bodies follow.
The shape is a unit primitive driven entirely by the picked object's transform, so a default Blender sphere or cube maps exactly, and position, rotation and scale all animate. An Empty works just as well as a mesh, and is often tidier: a primitive collider needs a transform and nothing else.
Collider transforms are re-sampled every frame in both live and baked modes, so a falling ball genuinely lands on a jelly rather than sitting still. An empty collection, or a body sitting in its own collider collection, is skipped rather than treated as an error. Both are just a half-finished edit.
Friction
Contact resists sliding, not just interpenetration. Friction is one coefficient per contact surface: 0 slides freely, around 0.5 grips on a gentle slope, 1 and above holds almost anywhere it can get a grip.
There are two places to set it, and which one applies depends on what the material is touching. Each collider in the list carries its own Friction, set on the collider object beside its Shape and Sticky, so a slippery floor and a grippy hand can be in the same scene. Everything with no collider slot of its own (the ground plane, the body against itself, and contact with other Marrow bodies) reads the body's Friction slider instead.
One coefficient covers both static and sliding friction. Below the limit the entire sideways step is given back and the material simply holds; above it the contact slips, at a rate the coefficient sets. The limit scales with how hard the contact is pressed, which is what makes it read as weight: the same slider grips harder under a heavy landing than a light touch.
Friction is applied as a position correction inside the contact pass, from the correction that pass already computed, so it needs no separate normal, and no second pass. The ground plane and rigid collider shapes measure it against a collider held still for the substep. Animated Mesh measures relative motion against the moving triangle surface. Self-collision and body-to-body measure the pair, so two parts of one body travelling together are not braked for merely touching.
0 is the default everywhere and is bit-identical to having no friction at all, so nothing that was authored before this existed moves differently.
Sticky is not friction. Friction resists sliding along a surface; Sticky welds material to it and drags it wherever the collider goes. A sticky collider ignores Friction entirely, and the panel greys the value out to say so.
Sticky colliders
The magnet toggle on a collider makes it sticky. Material that touches it is held to the surface and dragged along as the collider moves, instead of only being pushed out of it.
Plain non-penetration can only push, so a collider that lifts away leaves the body behind. Sticky is what makes squash-and-stretch possible: press a plate into a blob, lift it, and the material is drawn up into a column. The contact point is recorded in the collider's own local space, so the anchor rides the animated transform for free and rotation and scale come along with it.
Stick Break is how far the material may drag a contact point before it lets go, in world units. Zero never lets go. There is no universal value: the distance a contact settles at depends on Stiffness and Substeps, so tune it against the shot.
It is read once a frame, on the last substep. Every substep was the old behaviour, and it made the slider useless: mid-frame the gap between a node and its anchor is dominated by the impact transient of the contact itself, not by how hard anything is pulling. Measured on a 1 m block at 10 substeps, the frame a plate first touches peaks at 0.325 while a body hauled 4 m off a stationary plate with Drag never reads more than 0.156, so every setting either let go on first touch or never let go at all. Read after the frame's solve has settled, the same block gives 0.047 on the contact frame, 0.024 hanging idle, 0.052 dragged 1 m and 0.156 dragged 4 m, which is a range a slider can address.
Sticky is not available on an Animated Mesh collider.
Nothing may depenetrate first
A sticky collider can only grab a node that is still inside it. Until 2.5.1 the ground plane was resolved before every collider, so a sticky collider sitting at or below the ground plane never held anything at all, whatever Stick Break was set to. Measured, a body on a sticky floor at z = 0 with the ground plane also at z = 0 followed that floor 0.000 of 0.600. The ground plane runs last now and defers to a node a collider is already holding, so the anchor still outranks the floor.
It welds material to a surface and drags it wherever that surface goes, which is what a plate lifting a blob needs. A body crawling over a stationary floor wants Friction instead, because a sticky floor nails it down.
Every buried node is grabbed on the first frame and welded to whichever face happened to be nearest. They scatter across different faces and turn the body inside out. Measured on a sphere half-buried in a sticky box: 219 of 461 nodes seized immediately, 12% of tets inverted, render mesh shredded.
Move the collider clear at the start frame, or leave Sticky off. A collider that presses in during the simulation is fine, and is the intended way to set a stretch shot up.
Self collision
Self collision is off by default, because it is the second most expensive knob after Substeps.
| Resolution | Cage nodes | Surface nodes | Off | On, before 2.14 | On |
|---|---|---|---|---|---|
| 0.25 | 461 | 314 | 1.9 ms | 3.0 ms | 2.6 ms |
| 0.15 | 1,707 | 848 | 2.0 ms | 4.9 ms | 3.5 ms |
| 0.10 | 5,233 | 1,898 | 2.1 ms | 8.4 ms | 4.5 ms |
| 0.07 | 14,226 | 3,866 | 2.5 ms | 15.2 ms | 4.7 ms |
Only nodes on the surface of the cage take part, so the cost grows with the square of an area rather than of a volume. 2.14 added a broad phase: surface nodes are sorted along a Morton curve once, at rest, into blocks of 32 that each carry a bounding box, rebuilt from live positions every substep. Two image loads and a box test then skip 32 candidates at once. The box is expanded by exactly the thickness at which the node-against-node test stops rejecting, so a pair really in contact can never be skipped: the pass is bit-identical to not having it, which the suite checks rather than assumes. There is still no spatial hash, because integer images and imageAtomicAdd do not work in Blender's Python GPU API and a CPU-side hash would mean reading the whole cage back every frame.
Thickness is the contact gap, as a multiple of Resolution. 1.0 keeps the skinned render surface from visibly interpenetrating. Below about 1.0 a fold can slip between cage nodes, since contact is node against node and nothing checks the space between them.
A surface point can sit at the centre of the opposing cell, sqrt(2)/2 of a Resolution from the nearest node there, so a gap under that can be missed by every pair at once. Measured on two slabs at Resolution 0.05 pressed face to face and offset half a cell: Thickness 0.70 marked no nodes in contact where 1.00 marked 147. It reads exactly like Self Collision being switched off, and the panel now says so. Leave Thickness at 1.0 unless there is a reason not to.
Parts of the body that are already close together in the rest pose are left alone. This is what stops the cage fighting its own tets, whose nodes sit exactly one Resolution apart, but it also means a shape authored with two surfaces already touching will let those two surfaces keep passing through each other.
Fast motion can still tunnel: a node moving more than the thickness in one substep can cross to the far side before it is ever tested. Raise Substeps or Thickness if you see it.
Colliding two bodies
Turn Collide With Bodies on for each object you want in the pile. There is no pairing list: everything with it on collides with everything else that has it on, and both sides deform. The correction splits by mass, so a pinned body pushes without being pushed.
Because they push each other, they have to be simulated together. Three consequences worth knowing before you use it:
- Baking one bakes all of them. A two-way bake of a single body would leave the other absent from its own contact.
- The group runs at its highest Substeps. A body's own setting can be raised by the company it keeps. Marrow says so on the console when it happens.
- Adding a body mid-playback restarts the group. There is no sound way to splice a body into a simulation already in progress.
Contact uses the same Thickness as self-collision, and the group takes the largest value among its members so both sides of a contact agree on the gap. Unlike self-collision there is no rest-pose exemption: two bodies share no rest state, so every contact inside the thickness is a real one.
Cost is surface nodes of one body times surface nodes of the other, per pair, with N bodies making N² pairs. Two or three bodies at Resolution 0.1 is comfortable. A crowd is not.
Ground plane
A cage that starts below the ground plane is lifted onto it, rigidly, before the first frame, and Marrow says so on the console.
This is not cosmetic. Collision resolves penetration by moving the predicted position, and the integrator reads that move as velocity of depth divided by the substep length. Mid-simulation that is harmless, because a substep can only sink a node so far. The starting state has no such bound: a unit ball authored straddling the plane left its first substep at 226 m/s, which is past any tear threshold and shreds the body. Lifting rigidly rather than clamping each node matters too, since clamping flattens the buried half and the stored energy launches it nearly as hard.
Failure
This was called Tearing, and the name promised something it can never do. A tetrahedron past the strain threshold stops resisting distortion, permanently, so the material gives way and lets go. It does not split the mesh. Marrow never modifies your topology (that is what keeps your UVs, shape keys and material slots intact), so however much of the cage fails, the render mesh stays one connected piece.
What it does buy is release. Measured on a 2 m bar pinned at the top under heavy gravity, over 40 frames: with Failure off the bar necks and holds, its free end reaching −5.11. With Failure on at Fail Strain 1.4 the neck gives and the rest drops to −64.37, twelve times further. Nothing else in the solver ever lets material go.
What it does not buy is a break, or permanent damage. The fallen part is still joined to the pinned part by a filament stretched sixty units and far thinner than a pixel. And failure is not plasticity: stretched to 4.5× and released, a body with 80% of its tets failed sprang back keeping 2.9% of the stretch, against 3.0% for a body with none. Failed material goes slack; it does not stay deformed.
Breaking it for real
The marrow_torn attribute is how you actually break it. While Failure is on, every render vertex carries a FLOAT attribute, 1.0 where the tet holding it has failed by this frame. Feed it to a Geometry Nodes Delete Geometry and the failed material disappears, leaving a real gap: Marrow supplies the selection and you own the topology change. It is written from the recorded failure frames rather than read live, so scrubbing a baked cache shows what had failed by that frame instead of what fails by the end.
What Fail Strain measures
Fail Strain is the largest principal stretch a tet survives, so it reads the same in every direction: 1.5 means failure once anything is pulled to 1.5× its rest length, whether that is a pull along one axis, a uniform swell or a shear. Rotation is not strain and never fails.
One consequence worth knowing: a volume-preserving squash stretches the material sideways, and that counts. Press a blob to a quarter of its height and it has stretched 2× laterally, which fails a 1.5 threshold. If a heavy press is failing material you wanted intact, raise Fail Strain or switch Failure off for that shot.
Two things a failed tet still does
- It keeps a volume constraint, targeting the volume it had at the instant it broke. Broken material is not new material. Without this the cage inflated without bound, measured at 3.1× on a stretch test.
- It cannot be the last tet holding a node. A node whose every tet has torn has no constraint at all: it free-falls, and because the render topology is fixed it drags a spike behind it instead of becoming debris. The tear is refused instead. On a stretch test this took 324 orphaned nodes to zero while still tearing four fifths of the cage.
Ending a sticky stretch
Material held by a sticky collider that pulls away necks without limit, because nothing else in the solver can fail. Stick Break ends it by releasing the contact, so the body drops away whole rather than necking and parting: measured on a 1 m block with a sticky plate lifted 0.8 m, the body drops off at 0.02 and 0.05 and rides the plate up from 0.08, while pulling it off by hand with Drag frees it anywhere in the slider, since a lift pulls far more gently than a hand does. The useful range sits below about a quarter of the body, which is why the slider stops there. Failure ends it by giving way in the material instead, which is the one that leaves a neck. Neither produces a gap on its own; that is what marrow_torn and a Delete Geometry node are for.
Per-region stiffness
Stiffness Group makes one body two materials. Point it at a vertex group on the object and paint that group where the body should be firm; the weight scales Stiffness and Volume Preservation together, per tet.
The mapping is anchored at the top. Weight 1 leaves both sliders exactly as set, and weight 0 drops to Softest, so the sliders stay the material you tuned and the group says where the body gives. Anchoring it the other way, with the multiplier equal to the raw weight, would make every unpainted vertex zero-stiffness: paint one stiff region and the rest of the body turns to soup. Softest defaults to 0.1, a tenth as stiff, and 0 does mean no resistance at all.
Both terms take the same multiplier. Stiff material is stiff in shear and in volume alike, and scaling only one would change the material's Poisson ratio rather than its stiffness.
The group is painted on the render mesh and read by the cage underneath through the same k-nearest-vertex map Pinning and Attachment use, then averaged over each tet's four corners. Nothing is baked onto the cage, so unlike the fiber curve a repaint needs no re-tetrahedralize: return to the start frame in Live, or Free and bake again, and the solver is rebuilt with the new weights. An empty group name, a renamed group, or a group with no weights painted all leave the body uniform and cost nothing.
The fiber term keeps its own Fiber Stiffness and is not scaled by the group. A soft region with fibers in it still pulls at full strength; it just resists less around them.
Rig-driven stiffness
Tissue is not one stiffness for a whole shot. A muscle firms when it fires and goes soft when it stops, and a body that has to be stiff in a landing is not the body that has to jiggle a frame later. Stiffness and Volume Preservation are re-read once per frame, alongside the seven fiber settings, so an ordinary Blender driver on either lands on the frame it was evaluated for: right-click the slider, Add Driver, point it at a bone.
Nothing is baked and nothing is rebuilt. Both are kernel uniforms pushed on every dispatch, so driving them costs an assignment: measured on an RTX 5050, a 23,600-tet sphere ran at 1.14 ms a frame holding the pair still and 1.13 ms re-assigning both every frame.
How far a driver may push it is set by Substeps. A stiffer constraint needs more iterations to converge, and an under-converged solve is not a stiffer body but a worse one.
| Stiffness | 10 substeps | 30 substeps |
|---|---|---|
| ×1 (default) | 0.978 | 0.959 |
| ×2 | 1.046 | 0.976 |
| ×4 | 1.041 | 0.988 |
| ×8 | 1.089 | 0.994 |
| ×16 | 0.302 | 1.087 |
Above 1.0 the body is standing taller than its rest height: a stiff body still has momentum at four seconds and is bouncing rather than settled. The row to read is ×16 at 10 substeps. 0.302 is a collapse, not a stiff body, and it is what a driver that runs off the end of its range looks like. A driver may raise Stiffness about eightfold at the default 10 Substeps. Past that, raise Substeps with it or clamp the driver.
The Stiffness Group does not move live. The per-tet multiplier is a texture baked when the solver is built, so a repaint still takes a restart. Only the two global sliders are uniforms, which is what makes them drivable: where the body is stiff is painted, how stiff it is at all can be driven.
Quasistatic
Dynamic simulation asks where the body goes next from its current motion. Quasistatic asks what equilibrium shape the current pose implies. It discards inertia, so there is no bounce, overshoot or jiggle. Use it when a pose must resolve predictably for a rig, blendshape or deformer training set; do not use it for motion that depends on momentum.
Iterations replaces Substeps. A dynamic frame marches through small timesteps. A quasistatic frame predicts once, then sweeps the constraints repeatedly against that state while carrying XPBD multipliers across the sweeps. Contacts, colliders, attachment and fibers stay in the loop. If one body in a colliding group is quasistatic, the whole group runs quasistatic.
Both specimens are at rest. Set the shared load, then apply it.
| Setup | Measured ms/frame |
|---|---|
| Dynamic, 10 substeps | 1.67 |
| Quasistatic, 10 iterations | 1.74 |
| Quasistatic, 20 iterations | 3.49 |
| Quasistatic, 40 iterations | 6.92 |
On the measured 1,600-tet cage, the result settled to four decimals by 10 iterations; the default 20 leaves margin. One sweep costs about one dynamic substep.
Leave Step Scale at 1.0. The tet cage measured unstable from 2.0 upward, and extra Iterations did not rescue it. Quasistatic also wants a soft material: stiff constraints remained under-converged at 200 iterations and no longer ordered like a meaningful material.
Fiber
Every tet in Marrow is otherwise isotropic: it resists distortion equally in all directions. Fiber adds one direction that is different, and drives it, the tet is told to shorten along its fiber, and the volume-preservation constraint puts the material it displaces out sideways. That sideways bulge is what makes it read as a muscle instead of a shrinking rod.
Travelling at the documented defaults: Activation 0.30, Wavelength 1.00 m, Bend 1.00.
Directions come from a Curve you point at the body. Each tet takes the tangent at the nearest point on that curve as its fiber direction, and the arclength at that point as its place in the wave, so the contraction travels from one end of the body to the other instead of the whole thing pulsing at once. Both are sampled at Tetrahedralize and frozen: a fiber direction is a property of the rest shape, so an animated curve has no meaning as a source. Change the curve and tetrahedralize again.
Setting the curve takes two Tetrahedralize passes. The Curve field lives below the "Tetrahedralize to simulate" gate, so it does not exist until the object already has a cage. The flow is Tetrahedralize, then set Curve, then Tetrahedralize again; the first pass only builds the cage the field needs to appear on, and the second is the one that samples the curve and bakes fibers.
A bevelled or cyclic curve bakes no fibers, and says nothing about it. The curve is evaluated to a polyline that must resolve to one open path: a bevel or extrude turns it into a tube, a cyclic curve into a ring, and neither has an unambiguous direction to hand a fiber. Either one bakes nothing, silently: the panel keeps reading "Tetrahedralize to bake fibers" exactly as if no curve had been set at all. If fibers refuse to bake, check the curve for a bevel or Cyclic U before looking anywhere else.
The wave
The wave itself is procedural, not keyframed. Wavelength and Speed set its shape and how fast it travels: the crest moves at Wavelength × Speed in world units per second, and a negative Speed sends it the other way. Amplitude is how hard it squeezes: 0.3 shortens a fiber to 70% of its rest length at the crest. Waveform picks a smooth cosine or a hard on/off square.
Noise
Noise is what stops it looking like a machine. A pure travelling wave is periodic in space and in time: every crest arrives exactly one Wavelength after the last and squeezes exactly as hard, and nothing alive moves like that. Noise jitters both: when a crest arrives, and how deep it goes.
The last column is the one that matters. An activation that depends only on position − speed × time is a rigid travelling wave: one fixed profile sliding along the body, the same picture every frame. Making that profile wobbly does not help; it still slides unchanged, and still reads as clockwork. The number below is how far the body's shape at t = 0.5 s differs from its shape at t = 0 slid along by the best possible amount. Near zero means rigid.
| Noise | Beat interval | Depth at the crest | Rigid-shift residual |
|---|---|---|---|
| 0.00 | 1.000 ± 0.000 s | 0.500 every time | 0.001 |
| 0.35 | 0.999 ± 0.074 s | 0.416 to 0.575 | 0.103 |
| 0.70 | 0.998 ± 0.139 s | 0.337 to 0.637 | 0.206 |
| 1.00 | 0.876 ± 0.265 s | 0.280 to 0.883 | 0.301 |
The noise is therefore sampled in position and time, at a drift rate that is deliberately not the wave's own: it slides through the wave rather than along with it, and no two cycles come out the same. Up to 0.7 the mean beat interval stays at the Wavelength and Speed you asked for, so Noise perturbs the wave rather than quietly retuning it. At 1.0 it is strong enough to merge and split crests outright, which is why the mean drops; that is the intended extreme, not a bug.
It skews the pulse
A cosine crest eases in and eases out at exactly the same rate, and no muscle does that; it snaps into the contraction and drifts out of it. The warp that skews it is driven by its own field, so no two crests share a profile.
| Noise | Mean | Range |
|---|---|---|
| 0.00 | 1.000 | 0.998 to 1.002 |
| 0.35 | 1.003 | 0.741 to 1.392 |
| 1.00 | 1.128 | 0.312 to 4.088 |
It drifts the stroke rate
Everything above perturbs a crest (when it arrives, how hard it bites, what shape it is) and none of it touches the rhythm underneath. A travelling wave at a fixed Speed strikes the same pose again every 1/Speed seconds however each crest is dressed, and that is what still reads as repetition.
| Noise | Repeats itself at | Slowest stroke rate at Speed 1.2 |
|---|---|---|
| 0.00 | 0.94 | 1.200 (metronome) |
| 0.35 | 0.70 | 1.015 |
| 0.70 | 0.40 | 0.831 |
| 1.00 | 0.20 | 0.672 |
The drift is a function of time alone, so the whole body speeds up and slows down together: an animal changing gait, where a position-dependent term would only be more phase jitter. It scales with Speed rather than being absolute, which keeps it meaningful at any Speed and keeps the wave travelling forwards: at Noise 1.0 the instantaneous rate swings between 56% and 143% of the number you set, and never reaches zero. A standing wave at Speed 0 has no rhythm to drift, so this term goes quiet and the rest keep working.
The jitter is bounded so the wave's phase always advances along the body, and the skew is bounded so a crest can never fold into two. A jitter that ran the phase backwards would hand neighbouring tets unrelated points in the cycle and shred the body instead of undulating it, and the coefficients are picked against that bound rather than by eye.
Zero is the clockwork wave exactly (the noise arithmetic is skipped, not multiplied by zero), so an older scene keeps the motion it had. New bodies default to 0.35.
Bending
A wave keyed on arclength alone contracts a whole cross-section at once. That is a squeeze travelling down a straight body, an accordion ripple, and it cannot ask for a bend however it is shaped, because there is nothing asymmetric in it to bend towards. Any bending you get from it is Euler buckling, which goes wherever it likes.
Real undulation is contralateral: one flank contracts while the other releases, and that pairing travels. Bend is how much of that the wave does. Each tet is baked with a signed lateral offset from the curve, and Bend offsets its place in the wave by that: a quarter cycle either way, so the two flanks end up half a cycle apart. Tets on the curve itself read zero and stay in phase with both, which is the right answer for the neutral axis of a bending body: it neither leads nor lags.
Measured on a 6 m cylinder, gravity only, Bend 0 against Bend 1 with every other setting identical: peak vertex difference 0.46 m, rms 0.16 m. It is not a subtle knob.
Sideways is left and right, measured about the world's up axis, because that is the plane a body lying on the ground undulates in. A spine running straight up has no left or right in that plane, so those tets fall back to bending towards world +X; a tentacle standing on end still bends rather than silently going rigid.
The side is baked at Tetrahedralize alongside the direction and arclength, so a cage built before this existed has no side column and reads as unfibered. Tetrahedralize again to get one; the panel will keep saying "Tetrahedralize to bake fibers" until you do.
Set Bend to 0 for the muscle case: a bicep bulging along its own length wants the whole section contracting together, not one side leading the other.
Contraction alone moves nothing. Locomotion is contraction plus grip, so a crawling body needs Friction above zero and something to push against; tools/fiber_demo.py builds a working scene to start from.
A torn tet loses its fiber along with its stiffness, so torn muscle does not pull.
Thrust
Fiber contraction changes a body's shape; it does not carry the body anywhere. Thrust adds the missing push: an acceleration along the current fiber direction, from the tail end towards the head. It follows the body's deformed shape every substep, so a coiled or bending body pushes along its coil instead of along the straight direction it had at rest.
Thrust alone does not make a snake climb. It drives the body around its coil, while contraction holds the coil and a rising Force field gives it something to follow. A practical starting recipe is Fiber on, Bend 1, Phase 0.25, Amplitude 0.3, Thrust 20, friction 1 and a negative-strength attractor paced to the climb. The attractor must not outrun the body or it pulls the snake into a ball.
Top Speed prevents runaway acceleration. As a node approaches the target speed, Marrow fades its Thrust contribution linearly; it never brakes a node that is already faster because of gravity or another force. Leave it at 0 for unlimited thrust, or set it to the speed the shot should sustain. It has no effect in Quasistatic mode, where nodes have no velocity.
Thrust is off with the Fiber toggle, and a cage tetrahedralized without a fiber curve has no node pairs to push along. Like the other fiber settings, it is a kernel uniform re-read every frame, so it can be keyed or driven without rebuilding the cage. demos/pole_climb.blend shows three paced variants beside controls.
Muscle arrangement
Fibers do not have to run along the body. Muscle picks one of the three arrangements a muscular hydrostat uses – a tentacle, a trunk, a worm – after Tan, Turk and Liu, Soft Body Locomotion (SIGGRAPH 2012). The solver never learns which is which: a fiber direction is an arbitrary unit vector in rest space either way, and what each arrangement does falls out of the volume-preservation constraint rather than a second mechanism.
- Longitudinal runs along the body. Contracting shortens it, and with Bend above 0 it bends. This is what Fiber has always done, and it is still the default.
- Radial runs straight out from the curve. Squeezing every cross-section has to put the material somewhere and the axis is the only way left, so contracting elongates the body. Phased along the curve it is peristalsis, the wave a worm crawls on.
- Helical wraps the body at Pitch, leaning from the body's own direction towards the way round it. 0 is longitudinal exactly and a right angle is a pure hoop with no twist in it, so the twist lives in between.
| Muscle | Along the axis | Across it |
|---|---|---|
| Longitudinal | 0.94 | 1.03 |
| Radial | 1.05 | 0.98 |
The two move the axis in opposite directions, which is the whole point of having both.
A rod, not a cube, because the size of the body decides the answer. These arrangements differ by what volume preservation does with the material they displace, and a body one lattice cell across has no interior to displace anything into. Measured on a single cell, radial reported elongation at 20 steps and the opposite sign at 40. Sixteen cells long by four across holds its answer as the step count grows.
A tet sitting on the curve carries no radial or helical fiber. Every direction around it is equally outward, so there is no answer to give, and it is baked as unfibered rather than handed an arbitrary tie-break. That is the right result for the neutral axis of a hydrostat, and it means a radial body wants a curve that actually runs through it.
Baked at Tetrahedralize, like the curve. Muscle and Pitch are read when fibers are sampled, so changing either means tetrahedralizing again. One fiber field per object still, so one arrangement per object: a body that needs longitudinal and radial muscles at once is two objects today.
Rig-driven activation
Speed advances the wave with the clock. That is what a swimming or crawling body wants, and it is the wrong shape for a bicep, which should tighten when an elbow bends and hold there for as long as the elbow is held. Phase is the same number left for something else to move: it adds straight onto the wave's phase, so a body with Speed at 0 sits wherever Phase puts it. 0 is rest, 0.5 is the crest, and Amplitude still says how deep that crest bites.
Nothing here is a new kind of driver. Phase and Amplitude are ordinary Blender properties, so right-click one, Add Driver, and point it at a bone's rotation the same way you would drive anything else. Marrow re-reads all seven fiber settings once per frame – they are shader uniforms, so a changed value costs an upload and never a rebuild – which is what makes the driver's value land on the frame it was evaluated for rather than at the start of the shot.
For a single muscle, set Speed to 0 and Wavelength longer than the body. Wavelength is what spreads a wave across the body's stations, so a wavelength that comfortably exceeds the body's length puts every tet at effectively the same station, and the whole muscle contracts together instead of a ripple running through it. Set Bend to 0 for the same reason. Noise defaults to 0.35, which is irregularity you want in a tentacle and not in a bicep; set it to 0 for a clean flex.
A driver mapping an elbow from straight to fully bent onto Phase 0 to 0.5 gives a muscle that tightens as the joint closes. Past 0.5 the cosine comes back down, so a driver that runs beyond it will read as the muscle releasing while the joint keeps closing: clamp the driver, or use a mapping that stops at 0.5.
One curve per object, so one fiber field per body. A limb with a bicep and a triceps that contract against each other is two objects, each with its own curve and its own driver.
A bake picks up drivers, but only advances the scene frame when it has a reason to. Colliders and Attachment are reasons; so is a driver or a keyframe on any Marrow setting. A body with none of those bakes without touching the timeline, which is faster and is why the check exists.
Gravity and force fields
Gravity comes from the scene. Marrow used to hardcode −9.81 and ignore Scene Properties, so turning gravity off in the place every other Blender physics system reads it did nothing here. It now obeys both the direction and the switch, and the per-body Gravity multiplier rides on top: 0 makes one body weightless while the rest of the shot still falls, and a negative value falls upwards.
Force Fields takes a collection, the same way Colliders does. Every Force, Wind, Vortex and Turbulence object in it pushes the body, nested collections included, and their strength, falloff power and max distance are read from the field settings on each object rather than duplicated into the Marrow panel.
| Field | What it does |
|---|---|
| Force | Pushes along the radius from the field object. |
| Wind | Blows along the object's local +Z. |
| Vortex | Turns around that same axis. |
| Turbulence | Swirls, answering to Size, Flow and Seed. |
The panel reports how many objects in the collection actually drive the body, so a collection of the wrong field type says so instead of doing nothing quietly. Other field types are ignored: Blender exposes no way to ask it for the combined field at a point, so the supported ones are reimplemented against their documented falloff; Magnetic and the rest need models nothing here has, and guessing at one would be worse than skipping it.
Marrow's Turbulence is three octaves of sine read through oblique directions: a smooth swirling field that varies across the body, drifts with Flow and shifts with Seed, but will not match a particle sim driven by the same field object. That is a deliberate trade for correctness elsewhere: lattice noise has to floor a coordinate to an integer, and float32 on the card floors a value a hair either side of a lattice line differently from the float64 reference solver every kernel here is diffed against. A whole cell of difference against a 2e-5 tolerance would fail at random, so the fiber wave and this both use sines instead.
Fields are evaluated in the kernel, where the node actually is, from a texture holding three texels per field. An animated field costs one upload of a few pixels per frame however large the cage is; the cost does not scale with the body.
Attachment
Attachment makes an armature, or any deforming modifier, drive the simulation from the inside instead of bending the result afterwards. Every frame Marrow evaluates the object's modifier stack against the stored rest shape and pulls each cage node towards where the stack puts it, so the bones lead and the flesh follows with the full XPBD behaviour: inertia, jiggle, collision and tearing all stay in the loop.
Attach Stiffness is how hard the flesh follows. 1.0 rides the animation exactly; lower values let the bones lead and the flesh lag, jiggle and overshoot.
While Attachment is on, modifiers that keep the vertex count (Armature, Simple Deform and the like) are muted in the display, viewport and render. They feed the targets, and the written simulation is the display; leaving them shown would deform the result a second time, measured at exactly twice the bone travel. Modifiers that change the count (Subdivision, Decimate) are the opposite: their vertices have no per-base-vertex meaning, so they cannot feed the targets, and they stay shown to smooth the simulated shape. Toggling Attachment off or De-tetrahedralize hands the original visibility back.
Toggling Attachment on or changing Attach Stiffness mid-simulation restarts the sim on the next frame instead of waiting for a trip to the start frame. The targets come from Blender's evaluated mesh, so linear and dual quaternion skinning both work without Marrow reimplementing either.
Contacts keep the last word: the attachment pull runs before collision, so a ground plane or collider still stops a body the bone drags into it.
Attachment needs the object to keep its vertex count: the weight table is per base vertex, which is why count-changing modifiers are kept out of the targets and left on the display only. Weights are synthesized once against the rest shape, in object space, so moving or rotating the object afterwards cannot scramble them; like the rest of Marrow the simulation itself stays in the world frame it was tetrahedralized in. Tetrahedralize fills the cage from the modelled shape whichever frame it runs on: the capture parks the modifier stack, so a pose playing at tet time cannot wedge the posed silhouette between the lattice and the bind. Editing the mesh needs a re-tetrahedralize, same rule as the bind data.
Glue
Attachment drives a body from an armature. Glue To drives it from another Marrow body: point a fat layer at the muscle underneath it and its cage rides that muscle's material, through the same pass and the same Attach Stiffness. Muscle on bone, fat on muscle, skin on fat.
Tetrahedralize both bodies, then set Glue To on the follower and give it an Attach Stiffness. There is no second checkbox: naming a partner is what turns the pass on, since a fat layer has no armature of its own to tick Attachment for. A glued body stops reading its own modifier stack for targets and the partner supplies them instead. Attach Group still says where the grip is tight and where it is loose, and a pin still outranks both.
Glue is one way. The layer follows the partner; the partner never feels the layer. It is an attachment, not a two-way constraint, so a heavy fat layer does not drag on the muscle it hangs from. Turn on Collide With Bodies for the two-way half: the two compose, and glue does not replace contact.
Both bodies step together, at the group's highest Substeps, the same rule Colliding two bodies follows. A partner left out of the group would stop advancing and leave the layer following a frozen pose.
Baking one bakes the pair. Bake covers every Marrow body in the scene, read off the scene rather than off the live sessions, so a layer is never baked without the partner it follows, which would leave it baking at rest.
Targets are read once a frame and carried forward one frame. Once a frame is what collider transforms and armature targets already get; the difference here is that the partner is itself simulating, so the material read at the start of a frame is a frame stale. The carry-forward takes the step the partner just made and extends it. Measured on a sphere in free fall, one second in, at 24 fps: without it the layer trailed the material it is bound to by 0.357 m, with it by 0.014 m.
| Attach Stiffness | Mean lag | Worst lag |
|---|---|---|
| 1.0 | 0.0135 | 0.0136 |
| 0.5 | 0.1480 | 0.3289 |
| 0.25 | 0.1670 | 0.3673 |
| 0.1 | 0.1769 | 0.4128 |
Below 1.0 the lag is the feature, not the error: that is the layer hanging back and swinging, which is where a fat layer's jiggle comes from. 1.0 reads as bound.
| Scene | ms/frame |
|---|---|
| Two bodies, no glue | 4.6 |
| The same pair, glued | 7.2 |
The 2.6 ms is one readback of the partner's nodes, one interpolation per glued node and one target upload, per frame, for a 7,755-tet sphere carrying a 14,040-tet layer. The bind itself – every cage node of the layer placed in the partner's rest cage – measured 1.2 s for 3,571 nodes against 7,755 tets. It is paid once when the session starts and kept across restarts, so returning to the start frame does not stall; re-tetrahedralizing either body pays it again.
A layer is mostly outside the body it wraps, and that material rides the nearest tet rather than being interpolated inside one: the same clipping Cage coverage describes, one level up.
| Fat layer | Nodes | Outside the partner cage | Worst |
|---|---|---|---|
| Solid, radius 1.25 | 3,571 | 56% | 0.355 |
| Shell 1, radius 1.25 | 2,076 | 95% | 0.355 |
| Shell 1, radius 1.10 | 1,709 | 64% | 0.233 |
Those nodes inherit the local motion of the nearest surface tet and nothing about the interior, which reads as a stiff shell carried rigidly rather than as material with its own depth. Keeping the layer close to the partner is what fixes it: at radius 1.10 the worst node sits 0.233 outside instead of 0.355. A Shell layer is the cheap way to build one, and note it makes the share worse, not better – a hollow layer is all outer surface.
Pinning
Pin Group holds material in place. Point it at a vertex group on the object and paint that group wherever the body must not move; weight 1.0 is a solid pin.
Use multiple groups without making a combined vertex group. In Pin > Additional Groups, click + and select another vertex group for each pin region. The original Pin Group remains active, so existing files keep their pins; it can also be left empty when using the list alone. Select a row and click − to remove that slot without deleting its vertex group.
Overlapping groups use the highest weight at each render vertex, then the combined mask is blended onto the cage. Duplicate entries do not strengthen a pin, and empty or missing groups are ignored. Adding, removing or changing a slot during Live takes effect on the next frame; a baked simulation must be baked again.
For separate handles, give each region its own Hook modifier and Empty, and add each vertex group to Marrow's pins. Enable Attachment and Follows Animation with Attach Stiffness at 0 to move every region independently while the rest of the body follows elastically. Follows Animation is shared by all pin groups, not a per-slot setting. Play the timeline or bake the Hook animation: moving an Empty while playback is paused does not advance the simulation. demos/multipin_creature.blend and demos/octopus_multipin_showcase.blend are built this way.
The group is painted on the render mesh, because that is the geometry you can see and select; cage nodes are interior lattice points and carry no vertex groups of their own. Each node reads the group through the same k-nearest-vertex map the attachment pass uses, so a node is held exactly when the surface around it is, and pinning works whether or not Attachment is on.
A pin is zero inverse mass, and every pass in the solver already defers to it. Predict skips the gravity step, integrate holds the position and the velocity both, and the collider and attachment kernels each check it first: a pin outranks a collider, and a pin outranks the armature. Nothing pushes a pinned node anywhere.
A weight below 1.0 is not a partial hold. It scales inverse mass, so 0.5 is a node with twice the mass, not a node half held; gravity is an acceleration, so a heavy node still falls at g. What the falloff buys is the boundary. The blend across the edge of a painted region makes the transition nodes progressively heavier instead of dropping a hard mass discontinuity into the middle of the material, which shows as a stress ring. Paint the region you want held at a solid 1.0 and let the blend feather the edge.
Moving the object
Moving the object takes effect on the next restart. The cage is built in the world frame Tetrahedralize saw, and until 2.1 it stayed there: move the object and the body went on simulating and drawing where it used to be, visibly detached from its own origin. Now the solver rebases into the object's current transform every time it is rebuilt, so the flow is move it, scrub to the start frame, and the body is there.
Sampled at the rebuild, not per frame, which is what keeps the object transform out of the animation. An animated object still cannot drive the simulation; only the transform standing at restart is read. Rotation and translation are followed; a scale is refused with a message, because resizing the cage under a rest shape measured before it would silently change both the mass and what Stiffness means. Apply the scale, or Tetrahedralize again.
Pins anchor in the world frame the body was tetrahedralized in (the same rule as the bind and attachment data), so moving the object afterwards does not drag the pin along with it. That is the point on an animated character: a pinned foot stays nailed where it was while the rest of the body is free to be driven.
Changing the Pin Group or Follows Animation mid-simulation takes effect on the next frame, the same as Attachment; all three change what the solver is built from. Repainting weights inside a group that is already selected does not: the setting itself has not changed, so nothing knows to rebuild. Return to the start frame in Live, or Free and bake again after a Bake.
Follows Animation
A pin holds; Attachment carries it. By default a pin is a static anchor, so hooking a pinned region to an Empty and moving it does nothing: the pin is exactly what the solver holds still, and the attachment kernel skips a node with no inverse mass. Tick Follows Animation and the pin rides the animation instead: still rigid, still outranking every collider, but driven rather than frozen.
| Setup | Top follows | Bottom |
|---|---|---|
| Pin Group only, no Attachment | −0.004 | 0.102 |
| Attachment 0.5, no pin | 2.600 | 0.929 |
| Pin + Attachment, Follows Animation off | 0.240 | 0.092 |
| Pin + Attachment, Follows Animation on | 2.874 | 0.815 |
Set Attach Stiffness to 0 to let a driven pin carry the body
This is the setting the mode is built for, and it is worth understanding why. Attachment aims every node at its evaluated position, and for material the animation does not reach, that aim is the rest pose, so the same pass that supplies a pin its target is otherwise nailing the rest of the body down. At 0 the pass runs for the pins alone and leaves the free material entirely to the simulation.
| Attach Stiffness | Pin travel | Body travel |
|---|---|---|
| 0.50 | 1.473 | 0.026 |
| 0.05 | 1.473 | 0.040 |
| 0.01 | 1.473 | 0.374 |
| 0.00 (pins only) | 1.473 | 1.552 |
The pin tracks perfectly at every setting. What changes is whether the body comes with it, and nothing about mass, gravity, ground contact or material Stiffness was ever the limit; releasing the attachment grip was the whole of it. A high Attach Stiffness also switches the physics off entirely: at 1.0 the pass hard-snaps every node to its evaluated position after the elastic solve, so Marrow just replays the modifier stack and Stiffness stops mattering: measured identical body travel to three decimals across a 400× change in it.
Use a stiffness above 0 only when you want the whole body to follow the animation with soft-body lag, which is what Attachment is for on its own. Mixing that with a driven pin fights itself.
Follows Animation needs Attachment on, because the attachment pass is where per-node targets come from; the checkbox greys out otherwise. Targets are sampled once per frame, the same treatment collider transforms get, so a driven pin reaches the frame's position on the first substep and holds.
Leave Follows Animation off to nail a region in world space while an armature drives the rest: a foot planted on the floor while the body moves over it. That is the case the flag exists to preserve.
Modifiers are only muted in the display while Attachment is on. With a pin alone, a Hook or Armature stays shown, so Marrow writes the simulation into the mesh and the modifier then bends that result a second time. The stretched spike this produces is display only; the simulation underneath is unharmed.
False color
False Color, in the Display box, rainbow-shades the render surface by how far the material is stretched, in the style of Vellum's false color mode. The value is the edge stretch ratio: 1 at rest, hot where pulled past rest length, cold where compressed.
While stretch display is active a generated emission material sits in slot 0; choosing Off puts the object's own material back exactly as it was, including on an object that had no material at all, and De-tetrahedralize cleans up too. The scalar is computed per tet on the CPU from the cached cage positions, so switching the mode on after a bake still colours every cached frame.
Cage debug view
Cage, beside False Color in the Display box, draws the tetrahedral cage as a diagnostic view without touching the render mesh or solver. Strain moves with the simulated cage and colours each tet by the same stretch ratio False Color uses. Quality holds the cage at rest and colours mean-ratio shape quality: 1.0 is regular, while cold values expose near-degenerate tets in thin or coarsely filled features.
Tetrahedra renders each tet as a separate solid cell, shrunk to 85% around its centroid. It follows the simulation and hides the skin so coarse packing and bad regions remain visible. Topology and colour are built once; later frames only move corners, measured at 2.5 ms instead of the original 34 ms full rebuild on a 3.3k-tet cage.
Off removes generated edges, attributes, materials and the separate solid-view object, returning the plain rest-pose cage.
How it works
| Stage | Where |
|---|---|
| Tetrahedralize | CPU, numpy, once |
| Tet data | Mesh vertices, ID properties and POINT attributes, surviving save and load |
| Rest shape | A marrow_rest POINT attribute, so De-tetrahedralize can undo the whole thing |
| Pack to textures | CPU to GPUTexture, once per simulation start |
| Solve | GLSL compute, 6 kernels × substeps × constraint colours |
| Skin and readback | GPU blend, then only the render vertices cross PCIe |
The cage uses a cube-split lattice with checkerboard parity, not conforming Delaunay; part 2 in Plate I. Boundary recovery is fragile on real meshes and replaces the render mesh; this approach never touches your topology and tolerates messy input, because the only question asked of the mesh is inside or outside. The accepted cost is that the cage does not hug concave detail, so a model with deep concavities is filled more generously than it strictly needs.
Tets are graph-coloured at build time so each colour dispatches race-free with no atomics. Interior cage nodes never cross PCIe; only render vertices are read back.
Long runs
Tetrahedralize and Bake report progress and can be cancelled. Both show the stage and a percentage in the status bar while they work, and Esc stops them. Neither blocks the window any more, so a dense cage no longer looks like a hang, which it did, and people killed Blender over it.
The two cancel differently, because the work is different. Esc during Tetrahedralize discards the cage it was building: nothing is written to the file until that cage is complete, so there is no half-built state to leave behind. Over a selection it discards only the object in hand; the cages finished before it are already in the file and stay. Esc during a Bake keeps the frames it already simulated: the cache is keyed by frame, so a bake stopped at 96 of 250 is playable to 96 and is not a wasted wait.
Cost scales sharply with Resolution: halving it is roughly eight times the cage. On a 34,000-vertex mesh, Resolution 0.25 builds in about 7 seconds, 0.12 in 22, and 0.08 in 66, and the voxel pass, which asks inside-or-outside about every cell in the bounding box, is most of that at fine settings. Tetrahedralize also names the cage size when it finishes, and warns there if the cage is over the node budget, rather than letting Bake be the first to mention it.
tools/estimate_cage.py reports what a setting will cost before you commit to it: run it from the Scripting tab with the object selected.
GPU reliability
Blender's Python GPU API exposes no memory barrier; gpu.compute offers only dispatch. Marrow works around this. Readbacks are verified against a generation mark stamped by the writing kernel, uploads are read back and confirmed, and the bind texture is re-checked before every dispatch because it has been observed losing its contents after a verified upload.
When a repair happens Marrow says so on the console rather than staying quiet. If you see corrupt frames, please report them along with that console output.
Limitations
- Self-collision and body-to-body contact are node against node only. They have no edge-edge or node-triangle contact and do not scale past roughly 20,000 surface nodes. Rigid Mesh colliders use a distance field; Animated Mesh instead tests body surface nodes against a low-poly triangle proxy, so its cost follows proxy triangles and body surface nodes.
- A rigid Mesh collider is baked once, and features thinner than one SDF cell are missed. Use Animated Mesh for an Armature, Shape Keys or topology-stable modifiers; its evaluated proxy is sampled every simulated frame. See Animated Mesh.
- Rigid-collider friction does not ride a moving collider. For Sphere, Box, Capsule and rigid Mesh, Sticky is how a moving collider carries material. Animated Mesh is the exception: it measures relative triangle motion each substep, so friction can drag material without Sticky. Self-collision and body-to-body also measure relative motion.
- A body must not start inside a sticky collider. See Sticky colliders. Only the ground plane depenetrates its starting state.
- A live cache lives only in memory. Bakes are written beside the .blend and reload with it; a live cache is rebuilt as the timeline plays, which is what makes it free to throw away. See Baked caches on disk.
- Very thin features can still outrun the cage. The cage keeps every cell whose centre is inside the mesh, plus every cell holding a mesh vertex, so a feature thinner than a cell is covered as long as the mesh carries vertices there. What it does not cover is a large flat triangle spanning cells with no vertex in them, which is a coarse mesh rather than a thin one. Tetrahedralize warns when more than 1% of render vertices land more than a Resolution outside the cage and names how far the worst one is; those vertices are not simulated, they are glued to whichever tet was nearest and dragged by it. Subdivide the mesh, or lower Resolution.
- Glue is one way and a frame coarse. A glued body follows its partner; the partner never feels it, and the targets are read once a frame and extrapolated rather than solved together. Two bodies that must push back on each other need Collide With Bodies as well. See Glue.
- No plasticity. Deformation is purely elastic: the rest shape is baked once and never drifts, so a dent springs back rather than staying dented.
muandlamvary per tet through a Stiffness Group, but every tet always wants its original shape. - Quasistatic needs a soft material. Past roughly Stiffness 1e4 the solve is under-converged at any Iterations count, and a stiffer body reads as giving further rather than less. Measured against the float64 oracle, so it is the method and not the card. See Quasistatic.
- Attachment weights are synthesized once, against the rest shape. Editing the mesh without re-tetrahedralizing leaves them stale, same rule as the bind data.
- Measured on the OpenGL backend. Blender is moving to Vulkan, and the kernels need revalidating there.
Development
Core geometry and solver maths live in marrow/core/ and never import bpy, which is what makes them testable outside Blender.
# Core suite (pytest, needs a venv on system Python 3.12 with numpy + pytest)
.venv/Scripts/python -m pytest tests/core
# Blender suite (Blender's bundled Python has no pytest, so this is an
# assert-based runner; it exits non-zero on failure)
blender -b --factory-startup --python tests/blender/run_tests.py
Check which binary you invoked. Background mode only has a GPU context from 5.2 on. Point this at 4.5 and every GPU test fails with GPU functions for drawing are not available in background mode, and a windowed 4.5 driven by --python at startup fails each readback with StaleReadError: a RGBA32F upload never became visible. Neither says anything about the code, and on a machine with several Blender versions installed it is an easy hour to lose.
The runner drives one Blender process per test module. Running everything in one process invented failures that were not there: contact modules passed alone and failed one to three a run after the other forty, with the failing set rotating, which is GPU resource pressure from every module holding solvers and textures in its globals for the whole run. With isolation, red means wrong.
| Invocation | What it does |
|---|---|
run_tests.py | Spawn a Blender per module, aggregate, exit 1 on any failure. |
-- --module NAME | The worker: run one module in-process. |
-- --in-process | The old single-process behaviour. Faster, and it lies. |
A numpy reference solver in marrow/core/solver_ref.py is the test oracle: every GPU kernel is diffed against it, because a wrong sign in a compute shader is otherwise indistinguishable from a wrong sign in the constraint derivation.
The addon must use relative imports only. Installed as an extension its package is bl_ext.user_default.marrow, not marrow, so an absolute self-import fails at register time on a user's machine while every test still passes. tests/core/test_packaging.py guards this.
Never pip install into Blender's bundled Python.
Licence
GPL-3.0-or-later.