Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Maillage

Realistic textile assets generator for Physical AI.

CI Python 3.10+ License: MIT

Maillage generates towels, tea towels, napkins, tablecloths, bed sheets, cleaning cloths, bandanas, silk and knitted scarves and flags with real sizes, fabrics, masses and patterns, meshed for cloth simulation and exported to OpenUSD. Every piece is checked in Blender by a drape test: it imports, simulates without exploding, stretches little, does not go through obstacles and comes to rest.

It is built for Physical AI: robots that fold laundry, lay a table or hand over a towel are trained and tested in simulation, and they need many different cloths, not one subdivided square; perception models need synthetic images of crumpled and draped fabric with exact ground truth. Everything can run on CPU. Maillage is the sister project of Granulaire (granular assets) and follows the same approach: distributions instead of one shape, measured outputs, honest limits.

Tea towels draped on the clamped pedestal of the Blender drape test

Waffle-cotton tea towels generated by Maillage (solid, gingham, striped), imported into Blender and draped by the drape test. Render: maillage drop-test output/tea_towel --render.

What it does

  • Generates N distinct pieces of a preset (12 by default): format drawn from real retail sizes, size tolerance, corner rounding, an imperfect cut (wavy edges, slight skew), fabric and pattern.
  • Meshes each piece for cloth solvers: near-equilateral triangles for any outline (rounded corners, round tablecloths), or a structured quad grid for rectangles (the topology of a woven fabric).
  • Draws a procedural pattern (solid, end bands, stripes along or across, gingham, border, flag bands) as a PNG texture that follows the fabric, not the cut.
  • Measures every mesh (area, perimeter, edge lengths, minimum angles) and derives the mass from the fabric's areal density.
  • Exports an OpenUSD scene: meshes with UVs, a textured UsdPreviewSurface material, hem and pin primvars, and the fabric's physical properties as custom attributes.
  • Tests every piece in Blender (maillage drop-test): import, cloth simulation, automatic checks, Cusick drape coefficient, the draped meshes exported back to OpenUSD and an optional Cycles render.

Installation

From PyPI:

pip install "maillage[render]"

From a clone, for development:

python -m venv .venv && source .venv/bin/activate
pip install -e ".[render]"        # add ",dev" for tests and linting

Requires Python 3.10+. For the Blender drape test, either put Blender on the PATH, or install Blender as a Python module (pip install -e ".[blender]", Python 3.13 only, as the bpy wheels exist for one Python version). Tested with Blender 5.2; Blender 4.2 or later should work.

Usage

maillage list
maillage generate --preset tea_towel                     # 12 pieces, triangles
maillage generate --preset tablecloth --count 20 --seed 3
maillage generate --preset bath_towel --topology grid --vertices 2500
maillage generate --preset napkin --fabric silk --no-cut --no-color
maillage generate --preset tablecloth --format "140x180@2,round:150,oval:140x220"
maillage preview output/tea_towel                        # flat sheet (matplotlib)
maillage drop-test output/tea_towel --pieces 4 --render  # Blender drape test
maillage drop-test output/tea_towel --gif                # + animated GIF (Pillow)
maillage drop-test output/flag --wind 4 --render         # flags hang from a pole

python -m maillage ... works too. Without the bpy module or blender on the PATH, maillage drop-test prints the command to run with your Blender:

blender -b -P "$(maillage blender-script)" -- output/tea_towel --render

The script does not import Maillage, so Blender's own Python needs nothing installed. From Python:

from maillage import Variations, generate_scene, write_outputs

towels = generate_scene("tea_towel", count=12, seed=1)
write_outputs(towels, "output")  # -> output/tea_towel/

# 40 cm linen squares with a perfect cut, 2,000 vertices each, quad grid
napkins = generate_scene(
    "napkin",
    count=6,
    fabric="linen",
    topology="grid",
    vertices=2000,
    variations=Variations(cut=False),
)
write_outputs(napkins, "output")

# tablecloths in your own sizes: a 140×180 rectangle twice as often as the rest
tablecloths = generate_scene("tablecloth", count=12, formats="140x180@2, round:150, oval:140x220")
write_outputs(tablecloths, "output")

Presets

Category Preset Formats (cm) Fabrics
Bath bath_towel 70×140, 90×150 cotton terry
hand_towel 50×100, 40×60 cotton terry
washcloth 30×30 cotton terry
Kitchen tea_towel 50×70, 45×65 waffle cotton, linen, plain cotton
Table napkin 40×40, 50×50 linen, plain cotton
tablecloth 140×180, 140×240, round ⌀150, oval 140×200 plain cotton, linen
Bedding bed_sheet 180×290, 240×300, 270×310 percale, linen
Cleaning microfiber_cloth 40×40, 30×30 microfiber
Accessories bandana 55×55 plain cotton
silk_scarf 90×90, 50×160 silk
scarf 25×160, 30×180, 20×140 (knitted winter scarf) wool knit
Outdoor flag 100×150, 60×90 (hung by the hoist) polyester knit

The 12 presets side by side, one piece each, draped in Blender

One piece of each preset (seed 2), simulated 5 s by the Blender drape test and rendered with Cycles; each image is framed on its own piece, so scales differ (30 cm washcloth to 3 m sheet). Same colours for the three terry presets: they share a palette and, with one seed, the same colour draw.

Sizes are common retail formats; values are realistic orders of magnitude, not measurements of a specific product. Flags use generic two or three band designs with random colours on unequal, asymmetric bands, so that real national flags (equal or symmetric bands) are unlikely.

Fabrics

Fabric Areal density Thickness Friction
cotton_terry 450 g/m² 4 mm 0.8
cotton_waffle 220 g/m² 1.5 mm 0.6
cotton_plain 140 g/m² 0.3 mm 0.5
cotton_percale 120 g/m² 0.25 mm 0.5
linen 180 g/m² 0.35 mm 0.55
denim 400 g/m² 0.7 mm 0.6
fleece 250 g/m² 3 mm 0.8
microfiber 300 g/m² 2 mm 0.9
polyester_knit 115 g/m² 0.2 mm 0.4
silk 60 g/m² 0.1 mm 0.3
wool_knit 350 g/m² 3 mm 0.7

--fabric gives every piece the same fabric, whatever the preset's mix.

Formats

Each preset draws from its own retail formats (table above). --format (Python: formats=) replaces them with your sizes and shapes, in cm, as a comma-separated list:

Format Shape
50x70 (or rect:50x70, 50×70) rectangle, 50 × 70 cm
oval:140x200 (or ellipse:140x200) ellipse, 140 × 200 cm
round:150 circle, 150 cm in diameter
50x70@3 any of the above with a weight (here 3; 1 when there is no @)

Weights. Each piece draws one format at random, with a probability proportional to its weight: weight / sum of the weights. With --format "140x180@2,round:150,oval:140x220" the weights are 2, 1 and 1 (sum 4), so each tablecloth has a 50 % chance of being 140×180, 25 % of being round and 25 % of being oval: about 6, 3 and 3 out of 12 on average, the exact counts depending on the seed. Only the ratios matter (@2, @1, @1 is the same as @50, @25, @25), and without any @ all formats are equally likely. With --no-size, nothing is drawn: every piece takes the first format of the list, at its exact size. The presets' own formats use the same weights (a bath_towel is 70×140 with weight 0.7, 90×150 with weight 0.3).

The longer side is always the length, so 70x50 is the same as 50x70. The preset's size tolerance, cut, corners, fabric and patterns still apply. In Python, a format can also be a dict ({"width": 50, "length": 70, "shape": "rect", "weight": 1}) or a tuple ((50, 70), (150, 150, "round")). With --topology grid, round and oval formats are skipped (at least one rectangle is needed). summary.json counts the pieces of each format drawn.

Variations

Each aspect has its own random stream: turning one off leaves the others unchanged (same shapes with plain colours, same colours with a perfect cut…).

Option (CLI / Python Variations) On (default) Off
--no-size / size=False format drawn, size within the preset's tolerance first format, exact size
--no-cut / cut=False wavy edges (0.3–6 mm) and skew (up to 1.5°) perfect cut
--no-corners / corners=False corner radius drawn middle of the range
--no-color / color=False pattern and colours drawn first pattern, first colours
--fabric NAME / fabric="linen" (Variations(fabric=False): first fabric) fabric drawn from the preset's mix one fabric for every piece

Mesh

Option Default Effect
--topology tri ✓ near-equilateral triangles (Delaunay on a jittered hexagonal lattice, outline sampled at the same spacing); any outline; minimum angles above ~20°
--topology grid structured quads stretched over the four cut edges (Coons patch); rectangles only, sharp corners; round formats are skipped
--vertices N 1500 target vertices per piece, whatever its size
--count N, --seed S 12, 1 number of pieces, random seed; same options and seed give the same pieces

Flat preview of nine tea towels, with their mesh

maillage preview output/tea_towel on a grid-topology set: the texture is drawn over each piece and the mesh shown on top; the wavy grid lines are the imperfect cut, spread across the piece.

Outputs (per preset, under output/<preset>/)

File Content
maillage_<preset>.usdc OpenUSD scene (Blender: File > Import > USD)
textures/<piece>.png pattern texture of each piece
pieces.csv per piece: fabric, shape, pattern, size, corner radius, mass, mesh metrics
summary.json counts by fabric, pattern, shape and format; mass and mesh statistics
sheet.png flat preview (maillage preview)
drop_test/ drop_test.json, draped.usdc, drop_test.png, drop_test.gif and frames/ (maillage drop-test)

OpenUSD layout

Z up, metres, kilograms. /World is the default prim.

/World
  /Looks/<piece>      UsdPreviewSurface + UsdUVTexture (textures/<piece>.png)
  /Cloths/<piece>     UsdGeom.Mesh, flat on the ground, laid out in rows
  /Ground             collision ground (UsdPhysics.CollisionAPI)

Each cloth mesh has subdivisionScheme = none, is double sided, and carries:

Property Content
primvars:st texture coordinates (vertex)
primvars:hem 1 on the hemmed band along the edges, 0 elsewhere (vertex)
primvars:pin 1 on pinned vertices: the hoist corners of a flag (vertex)
primvars:displayColor mean colour of the texture
maillage:fabric, maillage:pattern, maillage:shape what the piece is
maillage:arealDensity, maillage:mass, maillage:thickness, maillage:friction physical properties (SI units)
maillage:nominalSize, maillage:size, maillage:cornerRadius retail and actual size (m)
maillage:hemBendingFactor how much stiffer the hem is in bending
maillage:blender:* stiffness for Blender's cloth solver, for 1 cm edges (see Blender values)

OpenUSD has no standard schema for cloth simulation (UsdPhysics covers rigid bodies and joints), so these properties are custom attributes. Blender's USD importer reads the geometry, UVs, material, texture and the hem and pin primvars (as mesh attributes); the drape test reads the maillage:* attributes with the OpenUSD bindings shipped with Blender.

Blender drape test

maillage drop-test imports the scene into Blender and, for each tested piece:

  1. sets up Blender's cloth solver from the maillage:* attributes: stiffness rescaled to the mesh and the piece's real mass, stiffer bending on the hem, collisions and self-collisions;
  2. drape: lays the piece flat on a round pedestal (diameter 60 % of the shorter side), its centre clamped as between the two discs of a Cusick drape tester, and lets the rest fall; hang: a flag hangs from its two hoist corners on a pole, optionally in the wind;
  3. simulates 5 s (120 frames at 24 fps);
  4. checks the result and writes drop_test.json:
Check Pass when
finite no invalid coordinate
stretch 99 % of the edges stretched by less than 10 %, none by more than 60 %
penetration less than 0.5 % of the vertices inside the pedestal or under the floor
at rest (drape) fastest vertex below 10 cm/s at the end

It also measures the drape coefficient of each draped piece, as in the Cusick test: (projected area − pedestal area) / (flat area − pedestal area), from 1 (stays flat) to 0 (hangs straight down). The draped meshes are exported to draped.usdc with their maillage:* attributes copied back: ready-to-use draped assets.

Results

Indicative results of maillage drop-test on three pieces of each preset (seed 1, ~1,500 vertices per piece, 120 frames, Blender 5.2):

Preset Mass (g) Vertices Scenario Drape coefficient Stretch (p99) Result
bath_towel 429–450 1563–1582 drape 0.22–0.23 3.1–3.7 % 3/3 pass
hand_towel 219–229 1563–1585 drape 0.22–0.26 2.6–3.6 % 3/3 pass
washcloth 39–41 1562–1572 drape 0.40–0.49 4.2–5.1 % 3/3 pass
tea_towel 75–78 1549–1570 drape 0.28 2.8–2.9 % 3/3 pass
napkin 28–29 1559–1578 drape 0.36–0.38 2.2–2.6 % 3/3 pass
tablecloth 356–470 1564–1572 drape 0.20–0.25 3.1–4.0 % 3/3 pass
bed_sheet 636–863 1553–1573 drape 0.28–0.37 3.0–3.1 % 3/3 pass
microfiber_cloth 47–49 1554–1575 drape 0.37–0.41 2.2–2.4 % 3/3 pass
bandana 42–43 1558–1576 drape 0.30–0.33 2.5–3.1 % 3/3 pass
silk_scarf 48–49 1560–1579 drape 0.25–0.27 3.1–3.4 % 3/3 pass
scarf 134–189 1608–1621 drape 0.10–0.13 2.6–2.8 % 3/3 pass
flag 170–174 1551–1567 hang — 1.9–2.4 % 3/3 pass

No piece went through the pedestal or the floor. The drape coefficient depends on the shape of the piece as much as on the fabric (a long towel hangs further past a pedestal sized on its short side), so it compares pieces of one preset, not presets with each other. To compare fabrics, the same 40 cm square was draped in each (maillage generate --preset napkin --no-size --no-cut --fabric silk,polyester_knit,cotton_plain,linen,cotton_terry,denim):

Fabric silk polyester knit plain cotton linen cotton terry denim
Mass of the square 10 g 18 g 22 g 29 g 72 g 64 g
Drape coefficient 0.37 0.35 0.42 0.40 0.38 0.42

In this version the fabrics differ in mass, thickness, friction and look much more than in drape: the drape coefficients stay within 0.35–0.42, where real silk and denim are far apart (see the roadmap).

Four 40 cm squares (silk, plain cotton, linen, denim) falling onto their pedestals

Silk, plain cotton, linen and denim squares falling onto the clamped pedestals, slowed down twice: maillage generate --preset napkin --count 4 --fabric silk,cotton_plain,linen,denim then maillage drop-test output/napkin --frames 40 --gif --gif-step 1.

Two flags hanging from their poles, fluttering in the wind

Flags hung by their hoist corners in the wind: maillage drop-test output/flag --wind 4 --gif.

Blender values

Blender's cloth stiffness acts per edge and is not divided by the vertex mass, so the same numbers make a fabric stiffer or softer depending on the mesh resolution. Maillage stores the stiffness for a reference mesh with 1 cm edges and the real mass, and the drape test rescales it: tension and shear with the edge length, bending unchanged, air damping with the area per vertex. The solver itself runs with Blender's usual 0.3 kg vertex mass and every stiffness and damping multiplied by the same factor, which leaves the motion unchanged and keeps Blender's solver in the range its tolerances are tuned for.

Tension gives about 3 % stretch when a piece drapes; bending is smaller for softer fabrics, and kept low for all of them (see the roadmap). The values are not calibrated against measured fabric properties.

Method in brief

  1. Outline. A rectangle with rounded corners or an ellipse, parametrised by arc length. The cut adds a smooth normal displacement (sum of a few sines along the perimeter: long gentle deviations plus a small wobble) and a slight shear.
  2. Mesh. tri: the outline is sampled at the target edge length (sharp corners kept, roundings below the resolution merged into one vertex), the inside filled with a jittered hexagonal lattice kept at least half an edge away from the outline, then a Delaunay triangulation clipped to the outline. grid: transfinite (Coons) interpolation of the four cut edges.
  3. Fabric and mass. The fabric is drawn from the preset's mix; the mass is the measured area times the areal density.
  4. Hem and pins. Vertices within the hem width of the outline get hem = 1; for flags, vertices near the two hoist corners get pin = 1.
  5. Pattern. Parameters drawn per piece (colours from the preset's palettes, periods, band widths), rendered on the nominal rectangle with a faint weave noise; texture coordinates follow the fabric, so a skewed or wavy cut shows as a pattern not quite parallel to the edge, as on real pieces.

Limits

  • Blender only, so far. The drape test validates the assets in Blender's cloth solver. Nothing is exported yet for NVIDIA OmniPhysics deformables (Isaac Sim) or MuJoCo flex; the geometry and physical attributes are there, the engine-specific schemas are not.
  • Fabrics drape alike. Bending stiffness is kept low for every fabric (see the roadmap), and no value is calibrated against measurements (no Kawabata or Cusick data behind them).
  • Flat pieces only. No garments (sewn panels), no holes or grommets (a flag is pinned at its hoist corners instead), no folded or crumpled starting states, no wet cloth.
  • Textures are procedural. Patterns are clean and regular; no prints, logos, stains or wear.

Roadmap

  1. Stiff fabrics that drape like real ones. Blender's angular bending model (the only one that works on triangle meshes) becomes unstable when the bending stiffness is high relative to the vertex mass, as with stiff fabrics on fine meshes: instead of settling, the cloth keeps vibrating, and adding damping makes it worse. Every fabric's bending stiffness is therefore kept below this limit (about 0.015 × areal density for 1 cm edges, measured), so a 40 cm square drapes with a coefficient of 0.35–0.42 whether it is silk or denim. Next steps: more solver steps or a coarser simulation mesh for stiff fabrics, quad-grid meshes with Blender's linear bending model, and other engines.
  2. Pre-draped and crumpled states as OpenUSD variants (the drape test already exports draped meshes), and folded states for laundry-folding tasks.
  3. Engine-neutral physical stiffness (tensile and bending rigidity in SI units) with per-engine mappings, starting with OmniPhysics surface deformables.
  4. Calibration against published fabric measurements.
  5. Simple garments from sewing patterns.

Development

The project follows GitHub Flow:

  1. main is always releasable and protected: changes reach it only through pull requests, nobody pushes to it directly.
  2. Every change starts on a short-lived branch from main, named after its type: feat/<name>, fix/<name>, docs/<name>, ci/<name>, …
  3. Push the branch and open a pull request into main; CI runs on it.
  4. Merge when CI is green, then delete the branch.

Releasing is a change like any other: on a chore/release-X.Y.Z branch, bump version in pyproject.toml and move the Unreleased entries of CHANGELOG.md under the new version; merge the pull request, then tag vX.Y.Z on main (or create the release on the GitHub web page with that new tag). The release workflow builds the packages and attaches them.

A versioned pre-push hook refuses direct pushes to main. Enable it once per clone with git config core.hooksPath .githooks (emergency override: ALLOW_MAIN_PUSH=1 git push).

Continuous integration

GitHub Actions runs these workflows (.github/workflows/):

Workflow When What
ci.yml every push to main, every pull request ruff check and ruff format --check; pytest on Python 3.10–3.13 (Linux) and 3.12 (macOS, Windows); a command-line smoke test; the Blender drape test of every preset with the bpy module (Python 3.13, Linux), reports kept as a run artifact; build the wheel and sdist, twine check, install the wheel in a clean environment.
release.yml a pushed tag v* checks the tag matches the version in pyproject.toml, tests, builds, then creates the GitHub release with the packages attached and the notes taken from CHANGELOG.md. Versions with a, b or rc are marked as pre-releases.
Dependabot monthly pull requests to update the GitHub Actions and the Python dependencies.

Ruff is pinned in CI so that a new ruff release cannot fail the build on unchanged code; update the pin and run ruff format . together.

License

MIT © 2026 Daniel Régis Sarmento Caon

The logo is set in IBM Plex Sans (SIL Open Font License 1.1), outlined into paths, so it displays without the font installed.


Maillage is part of Primvaria, physics-ready variability for Physical AI, in OpenUSD: pip install primvaria installs the whole suite.

Metadata

Release files for maillage 0.1.0b2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for maillage 0.1.0b2
File Size Uploaded Provenance Actions
maillage-0.1.0b2.tar.gz 59.4 kB With provenance Details for maillage-0.1.0b2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for maillage 0.1.0b2
File Interpreter ABI Platform Actions
maillage-0.1.0b2-py3-none-any.whl Python 3 none any Details for maillage-0.1.0b2-py3-none-any.whl

Total release size: 108.0 kB

Release files / maillage-0.1.0b2.tar.gz

Download URL maillage-0.1.0b2.tar.gz
Size 59.4 kB
Tags Source
SHA-256 checksum
How to use checksums
e39fbf299012f21efe9912cadaf80fb0f271b59879c9ad0d575940833853f227
BLAKE2b-256 checksum
How to use checksums
282fd382d2635ce38e73d821e4ecdaf823e1872415126ddcb61325c329759c90
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Release files / maillage-0.1.0b2-py3-none-any.whl

Download URL maillage-0.1.0b2-py3-none-any.whl
Size 48.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0803c7c0bac43348ccb584fe093f284c7b81790274ad6be310beea2562af6293
BLAKE2b-256 checksum
How to use checksums
47abfdcf0ef1baeef7666c6bc0c16003f5c15e04234d92c6d0884c976b155da9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Security for maillage 0.1.0b2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for maillage-0.1.0b2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page