scenario-maya-deformation
Use when skinning a character in Maya ("bind skin", "fix the weights", weight painting, Skin Tools or ngSkinTools layers), when deformation looks wrong (pinching, candy-wrapper wrist, collapsing elbow or shoulder, volume loss, clothes poking through), when mirroring, copying or c
Install
npx skills add https://github.com/scenario-labs/skills/tree/main/skills/dcc/maya/scenario-maya-deformation
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install scenario-labs-skills@llmmart
git clone https://github.com/scenario-labs/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole scenario-labs/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Maya deformation (character TD)
Expert deformation is decided before the first weight: joint pivots and edge loops set what skinning can do. Weights are then built region by region, judged in motion at extremes with numbers and renders, and every fault is fixed where it originates. Stance: block rigid, diagnose, smooth and shape, correct what weights cannot, and keep everything regenerable as data. If a sibling skill named here is missing from your available skills, ask the user to install it (npx skills add scenario-labs/skills --skill <name>); unattended, proceed from tool schemas and flag the gap.
REQUIRED BACKGROUND: scenario-maya-expert (execution channel, review loop, 2027 version traps).
Module scripts/mx_skin.py: weights IO, region blocking, masks, mirror, transfer, ROM generator, deformation metrics, correctives, Skin Tools wrappers. Status (2026-09-24): its pure-Python half ran offline (test_mx_skin_offline.py, all pass); every Maya call is not yet run in Maya (tests: tests/code/maya-deformation/run_all.sh, via mx_run.py).
import sys; sys.path.insert(0, "<skills>/scenario-maya-deformation/scripts"); import mx_skin as S
Stance (the expert delta)
- Raw blocked weights are a rig test. Assign every vertex 100% to its closest joint and pose: it "should look relatively close to your expected final result"; if not, fix pivots, placement or loops first. Smoothing a rig fault away: "don't do this in production" (Makauskas). Loops running along the wrist or ankle are the modeler's fault; volume loss at a clean elbow is the rig's (antCGi).
- Fill the region, art-direct in the mask. One layer per decision: twist joints get an even fill, the shape of shrug, shoulder and elbow lives in the mask, so re-smoothing twists never breaks the elbow (Jao). "Mask is not an actual influence", it is the layer's alpha (Makauskas). Headless,
S.block_regionscomposites the same stack and writes once. - Judge in motion, one motion per test. Calisthenics: keys every 10 frames, a pose on every second key, back to bind between, never bend and lower together (Makauskas); extreme and "weird" poses expose errors. ML pose data: one joint at a time in about 30 degree steps (Widup).
- Smoothing is not free. Sparse fingers lose volume: smooth the middle loops, then Sharpen (Makauskas). Measured here [added]: smoothing a 110 degree elbow cut stretch from 3.9x to 1.5x but collapsed the inner elbow (edge ratio 0.19). Deep bends need correctives, not more smoothing.
- Skin Tools layers are an authoring state. While layers exist, Maya blocks the classic paint, mirror, copy, hammer, set-max-influences and normalize tools; Delete Skin Layers keeps the evaluated weights and is the handoff step (Help). "It's still a normal Maya skin in the end" (Jao).
- Choose the corrective's space before sculpting. Pre-deformation is "like fixing the original character's mesh" and holds in other poses; post-deformation fixes one posed result, in Transform Space when one joint owns the fix (faster, bounded by that joint's envelope) or Tangent Space when it follows the surface across joints (Help; the cross-joint reading is [added]). Extremes first, then in-betweens (Help). Engines: secondary joints driven by swing-twist pose fractions, each assigned to its LODs (Rudy).
- Read the joint the rig actually moves. Bind joints driven by IK/FK constraints or an offsetParentMatrix blend are posed through their controls. Pose Editor: create the interpolator without neutral poses, add the controller's driving attributes in Driver Settings, then Add Neutral Poses; Euler Twist when a wrist twists about the forearm (Help). Node readers use
dagLocalMatrix, since an OPM drive leaves rotate at 0 [added]. - Face: budget first, joints for rotation. Ask for the joint and shape budget first; a neutral with lips slightly parted made skinning "night and day easier" (Burton). Eyes, lids and jaw stay on joints even in shape faces, and split shapes must sum exactly to their master (antCGi). The ML Deformer learns what you feed it: skin joints in the Control Collector, Delta Mush removed first, pose coverage over network size (Widup).
Establish first
| Input | Changes | Default when the brief is silent |
|---|---|---|
| Target runtime | influence cap, DQ allowed, correctives as shapes or secondary joints, face shapes or joints | film-style in Maya: no cap while authoring, prune 0.01 at the end [added]; game: the engine's or lead's cap (Numbers) |
| Skeleton (scenario-maya-rigging) | regions, twist chains, mirror rules, how bind joints are driven (constraint or OPM) | bind joints only, rotations zero, L_/R_ names, twist joints in every limb section (3 to 5 in the sources) |
| Mesh (scenario-maya-retopology-uv) | bind method, shells | Heat Map on a watertight single shell with joints inside; Geodesic Voxel for multi-piece or non-manifold |
| Deformation brief | stylized squash (two clusters), clothing, face scope, hero vs background (ML Deformer) | body plus clothing as separate skinned meshes; face = jaw, eyes and lids on joints |
Skin Tools API (S.skin_tools_probe()) |
layered or plain path | plain cluster with in-memory layers; real layers only when a person will paint them |
Workflow
- Intake gates.
mx_validate.validate(profile="model"),mx_audit.verdict(r, symmetric=True),S.bind_precheck(mesh, joints, method). GATE: no error lines, joints inside, transform frozen, no non-deformer history, X symmetry; face: lips apart, eye joint at the eyeball center (Burton). - Bind, explicitly.
S.bind(mesh, joints, method, skin_method=0, normalize=1): pass every flag, since cmds and UI defaults differ and the Help contradicts itself onnormalizeWeights. GATE:S.weights_verdict(S.audit_weights(...))clean;S.export_weights_jsonbackup (OpenMaya writes are not undoable). - Block and diagnose.
S.block_regionswith closest-bone regions, no smoothing; key calisthenics (S.rom_schedule,S.key_rom);S.deformation_test;S.pose_snapshotsthenmx_review.reviewheadless and look. GATE: raw poses read close to the goal; faults go back to scenario-maya-rigging (pivots) or scenario-maya-retopology-uv (loops) with numbers and images. - Regions, twists, masks. Order: torso, clavicle and shoulder, upper arm, forearm, hand, thumb, fingers, head and neck, legs, feet (Makauskas). Twist chains as even ramps, masks as ranges along bones; with the API,
S.st_add_region_layerandS.st_flood. Tune mask ranges, not vertices. GATE: audit clean, warnings shrink pose by pose, worst poses looked at. - Mirror and shells.
S.mirror_skin(or the Skin Tools Mirror tab); clothing and accessories take the body under them (S.copy_nearest,S.remap_by_closest, volume smoothing limited to the shell). GATE: mirror error < 0.01, nothing unmatched, no penetration at rest. - Full ROM. Sections at mid-forearm and mid-upper-arm, bone volumes, folds, cross talk, rigid soles, clothing penetration; then combined "weird" poses and animator-style play; a GUI playblast when a session exists. GATE: no
error:, everywarn:fixed or explained,references/critique.mdat 2 or more. - Correctives. Candy wrapper: twist distribution, then DQ or weight-blended skinning. Deep bends: pick pre or post and the target space (stance), check
S.joint_drive(joint)and pose through the controls when it is driven, thenS.solve_pre_correctivefrom a posed sculpt plusS.corrective_driver, or the Pose Editor in the GUI (controller-driven order above; extremes first; Regularization from 0.01). GATE: 0 at rest, 1 at the pose reached through the controls, residual < 1e-3 cm; ROM rerun. - Face, if in scope. Shapes through the split generator (
S.split_targets,S.add_split_targets), or a joint face with a zero rest pose (Burton); squash under tweaks as two clusters (multi=True, UV Pin reading cluster 1, group inverse into bindPreMatrix). GATE: split error < 1e-5, counts within budget, double-transform test passes. - Finalize.
S.delete_skin_layers,S.prune_and_limitto the target cap, ROM rerun, weights JSON (plus layers JSON),mx_validate.validate(profile="rig"), a new saved version.
Numbers
| Value | Relative to | Source |
|---|---|---|
| Keys every 10 frames, pose on every second key | calisthenics timing | Makauskas [00:03:01] |
| 3 (Makauskas) or 5 (Jao) twist joints | per upper arm or forearm | [00:07:57], [00:17:13] |
| Weight sum 1 within 1e-4 | every vertex, per cluster | normalization (Jao, Help) |
| Prune below 0.01 [added] | per weight; the Help cites 0.008 as insignificant | Help, Prune |
| Influence cap 4 [added], 2 on Burton's Unreal mobile | per vertex, games only | Burton [00:51:55] |
| Edge ratio < 0.7 pinch, > 1.3 stretch [added] | posed vs bind edge length | digest thresholds |
| Section area < 0.7, perimeter < 0.8 [added] | mid-limb cross-section vs bind | candy wrapper; LBS at 50/50, 90 deg twist keeps 0.5 area (measured) |
| Volume change > 10% [added] | region around its bone | Makauskas judges volume by eye |
| Mirror difference < 0.01 [added] | weight of a vertex vs its mirror | |
| Regularization 0.01 to 1.0; 3 neutral poses (neutral, swing, twist) | Pose Editor | Help |
| 30 degree steps; 1,500 to 2,000 poses per body; 12 to 20 per arm | ML Deformer pose data | Widup [00:15:50] [00:37:08] |
| 24 face joints | Burton's current-gen game face | [00:35:45] |
Quality gates
Measurable (headless, mx_run): S.weights_verdict clean at the target cap; S.deformation_test without error: (cross talk is impossible under pure skinning); warnings fixed or explained; mirror error < 0.01; penetration increase 0 for clothing; correctives 0 at rest and 1 at the pose through the controls; split error < 1e-5; S.skin_layers_node(sc) == [] before handoff; re-imported JSON reproduces the weights (diff < 1e-4).
Visual: S.pose_snapshots of every ROM pose rendered with mx_review.review (clay and wire, front and three-quarter, same cameras every iteration), or a GUI playblast. Look at shrug and deltoid, elbow crease, wrist twist, fingers, groin and knee while lifting the foot, feet, neck, clothing; faces: mouth corners, cheek squash, lids. Judge with references/critique.md.
Common mistakes
| Mistake | What it looks like | Fix |
|---|---|---|
| Smoothing over a bad pivot | wrist bump, neck lump that returns every pose | move the joint (scenario-maya-rigging), re-block (Makauskas) |
| Edge counts read alone | rigid weights show zero pinch while pieces interpenetrate | read folds, collapsed faces and severity (critique.md) |
Editing maximumInfluences on a live cluster |
custom weights silently reset | S.prune_and_limit (Help: -mi in edit mode wipes weights) |
copySkinWeights with default flags |
closestComponent association, XY mirror plane | pass surfaceAssociation, influenceAssociation, mirrorMode="YZ" |
| Classic tools on a layered cluster | "blocked" errors, or weights overwritten later | S.delete_skin_layers first, or the Skin Tools equivalents |
| Heat Map with end joints outside the mesh | those joints get no weight | move joints inside or use geodesic after the precheck |
| Test animation with combined motions | cannot tell which joint breaks | one motion per segment (rom_schedule) |
| Flood smoothing sparse fingers | sausage fingers, lost knuckles | smooth middle loops, Sharpen (Makauskas) |
| A mesh-level error sculpted as a post corrective | the fix shows near one pose only | pre-deformation (S.solve_pre_corrective): it holds in every pose (Help) |
| Neutral poses created on an IK or constrained joint | the Pose Editor cannot return to neutral or go to a pose | no neutral poses at creation, Driver Settings > Controllers, then Add Neutral Poses (Help) |
Pose reader on .matrix or setAttr on a driven joint |
corrective stuck on or off; pose rejected or snapped back | S.corrective_driver (dagLocalMatrix); pose through the controls |
| Closed lips in the neutral | mirror and wrap transfers fail, lips stick | lips slightly parted (Burton); split upper and lower proxies (Jao) |
| Overlapping split shapes | double motion at the corners | vertex ownership, difference shapes (raise not smile), sum test (antCGi) |
| Delta Mush left under an ML Deformer | the ML learns the smoothing | delete it before training (Widup) |
| Pinned controls reading the final mesh | dependency cycle | UV Pin from skinCluster1.outputGeometry (Maya LC) |
Handoffs
- Receives from scenario-maya-rigging: bind skeleton (joints only, rotations zero, oriented, single root, L_/R_ names or labels), controls for the ROM, twist joints placed, and whether bind joints blend by constraint or offsetParentMatrix;
mx_validate(profile="rig")clean. From scenario-maya-retopology-uv: mesh frozen at the origin, no history, symmetric, loops across every joint, UVs, face with lips apart and eyes in place. - Delivers to scenario-maya-animation: a new rig version with layers deleted, weights and layers JSON, the ROM verdict JSON, the sheet or playblast looked at, what was not verified; referenced, never imported.
- Delivers to scenario-maya-pipeline-scripting (game skins): weights capped and pruned for the engine, bind skeleton only as influences, secondary joint list with LOD assignments, correctives as joints when the engine needs them; audit JSON for its FBX export check.
- Sends back: pivot and placement faults to scenario-maya-rigging, loop faults to scenario-maya-retopology-uv, each with the pose, numbers and image.
Maya 2027 notes
- Skin Tools (ngSkinTools-based) is built in: Skin > Skin Tools; layers only after explicit initialization; Bind Skin's Initialize Skin Layers is off by default; the paint preference defaults to the classic tool.
- Scripting:
from ngSkinTools2 import api(Developer Help, fetched 2026-09-24; not yet imported):init_layers,Layers,assign_from_closest_joint,flood_weights(acts on the component selection),PaintModeSettings,NamedPaintTarget.MASK,export_json,import_json. - Skin > Smooth Skin Weights uses the Skin Tools algorithm and errors on multi-layer clusters; Rigid Bind menus are gone (bind with one influence);
performSkinCluster.melis removed. - Several skinClusters per mesh need
skinCluster(multi=True); tool lists show the newest cluster first. mlDeformercommand (2026),dgaTensionfor stretch analysis (2026.3), Apply Mesh Compare (2026); Bifrost 3.0 ships with 2027; old experimental rigging modules are incompatible.
References
references/procedures.md: code per stage (P1 to P16) with status and test; when executing.references/expert-notes.md: principles by expert, disagreements; when a decision needs its reasoning.references/critique.md: report reading, region checks, 0 to 3 rubric; before calling a stage done.references/gui-paths.md: Skin Tools, Shape Editor, Pose Editor, ML Deformer menus; in the GUI.references/sources.md: sources, credentials, URLs, timestamps; to trace a claim.
Files (skills)
-
references
-
critique.md 12.3 KB
# Critique: how the character TD judges a skin, a corrective or a face Load before calling any deformation stage done, and when reading a `deformation_test` report or a ROM contact sheet. Numbers first, then eyes (scenario-maya-expert protocol); experts judge in motion, at extremes, one motion at a time (Makauskas [00:04:11] [00:10:36]; Widup [00:27:41]). ## 1. Review protocol 1. `S.weights_verdict(S.audit_weights(...))`: no `error:` line. Weights that do not sum to 1 or exceed the cap make every later judgment meaningless. 2. `S.deformation_test(...)` on the calisthenics: read `verdict`, then the per-pose numbers of the worst poses. 3. Look at the same poses: `S.pose_snapshots` + `mx_review.review` headless (clay and wire, front and three-quarter), or a playblast in the GUI. Same cameras for every iteration so before and after compare (digest playblast recipe). 4. Change one thing (a mask range, a twist distribution, a corrective), re-run 2 and 3, keep the change only if the numbers and the images both improve. 5. Last: pose it like an animator, combining extremes ("weird poses", Makauskas [00:16:14]; "in animator shoes" [00:25:02]). ## 2. Reading the numbers (and their traps) | Report item | What it means | Trap | | ---------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cross_talk.count > 0` | vertices with no weight on any moved joint moved | Impossible for pure skinning: a Delta Mush, wrap, ML Deformer or blend shape is leaking, or the wrong joint moved. Error, not warning. | | `pinch` (edge ratio < 0.7) | compression, usually the inside of a bend | Rigid 100% weights show NO pinch: the pieces pass through each other. Always read `folds` and `collapsed_faces` too [added, measured offline]. | | `stretch` (> 1.3) | the outside of a bend, or a hard weight boundary | Smoothing lowers the worst stretch but can multiply pinched edges at deep bends: compare severity (`edge_ratio_min`, `edge_ratio_max`), not only counts. | | `folds` | faces folding onto each other (dihedral >= 120 deg from a flat rest) | Some creases fold by design (finger crease, Jao's fold layer [00:26:09]); judge the region. | | `sections.area_ratio` < 0.7 | candy wrapper at a twist | Linear skinning at 50/50 and 90 deg twist keeps 50% of the area; DQ keeps 100% [measured offline]. A section that is not closed (`error`) means the plane cut other geometry: restrict with `influences`. | | `volume_ratio` off by > 10% | a region lost or gained volume around its bone | Approximate for open regions; trust the sign and the trend between iterations, not the exact value [added]. | | `rigid` > 5% | a rigid part (sole, palm, skull) stretches | Makauskas restores rigidity with a soft Add brush, not with more smoothing [00:23:31]. | | `penetration.increase` | clothing or accessory vertices went inside the body | Fix with body-driven weights on the shell (copy_nearest, remap), then volume smoothing restricted to the shell (Makauskas [00:05:33] [00:21:29]). | | `unused_influences` | joints with no weight | Fine for engine skeletons that must keep the joint (game export) and for helper joints; otherwise remove them (speed, Help). | | heat map: a joint with zero weight | the joint is outside the mesh | The Help says outside joints emit no heat; move the joint inside or weight it by hand. | Thresholds are this toolkit's defaults ([added]); a production can tighten them. Never report a pass on counts alone. ## 3. Region by region: what the experts look at | Region | Look at (visual) | Measure | Usual fix, in order | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Spine and torso | smooth bend and twist falloff, rib cage not dragged by the arm | pinch, stretch, `volume_ratio` per spine joint | joint placement; torso layer smoothing | | Clavicle and shoulder | shrug shape, deltoid mass, armpit, neck not moving too much (Jao [00:14:59]; Makauskas [00:05:52]) | sections across the upper arm; stretch at the armpit; cross talk on the neck | clavicle layer with its own mask (Jao [00:16:06]); correctives for overhead poses (Help PSD) | | Elbow | crease placement, bicep volume, twist not concentrated at the elbow (Makauskas [00:09:44] [00:13:24]) | folds and pinch inside, `volume_ratio` of the upper arm | twist distribution, then an elbow corrective; topology if loops run along the limb (antCGi x07 [00:10:27]) | | Forearm and wrist | candy wrapper, wrist bump (pivot), twist mid-forearm (antCGi x07 [00:11:04]) | forearm mid section area and perimeter | twist joints evenly weighted (Jao), DQ or weight-blended skinning, joint pivot before weights (Makauskas [00:15:39]) | | Hand, thumb, fingers | palm and thumb interaction, finger volume, crease in the crease (Makauskas [00:17:53]; Jao [00:26:09]) | pinch at knuckles, `volume_ratio` per finger | sharper weights, smooth only middle loops (Makauskas [00:18:51]) | | Hip, groin, knee | bend while lifting the foot, groin collapse, thigh pushing the belly (Maya LC [00:02:40]; Widup [00:02:50]) | pinch and folds at the groin, knee section | correctives (Help lists groin and knee), separate wrap drivers on film rigs (Widup) | | Foot | not rubbery, sole rigid (Makauskas [00:23:31]) | `rigid` on sole vertices | soft Add to the foot joint, fewer shared weights | | Neck and head | neck and head junction, accessories follow the neck (Makauskas [00:20:24] [00:21:29]) | cross talk on the head when the arm moves | fix the rig first; necklace-only volume smoothing | | Clothing and accessories | stays glued, no body poke-through (Makauskas [00:06:24]) | `penetration` per pose | shell weights from the body; volume smoothing limited to the shell | | Face | mouth corners without pinch, cheek squash in the smile, lids over the eyeball, sticky lips, texture stretch with real textures (Burton [01:08:45] [00:37:23]; antCGi [00:47:19] [01:10:47]) | split sum error, lip distance at neutral > 0, joint and shape counts | topology and texture fixes before weights (Burton [00:30:17]) | ## 4. Scoring rubric (0 to 3 each) 1. **Weights hygiene:** 0 unnormalized or empty rows; 1 clean sums but tiny weights and over-cap vertices; 2 pruned and capped for the target; 3 plus mirror error < 0.01 and unused influences explained. 2. **Source soundness:** 0 smoothing hides pivot or topology faults; 1 faults known but unreported; 2 blocked weights looked close to the goal before smoothing (Makauskas [00:08:27]); 3 plus each remaining fault attributed to rig, topology or correctives with evidence (identical weights on two topologies, antCGi x07 [00:16:24]). 3. **ROM quality:** 0 no ROM test; 1 a ROM with combined motions; 2 one motion per segment, extremes, verdict read and worst poses fixed; 3 plus weird poses, clothing and animator-style play, no unexplained warning. 4. **Twist and volume:** 0 candy wrapper visible; 1 twist joints present but weighted at the ends; 2 even twist, section area above 0.7 at 90 degrees; 3 plus volume within 10% at elbow, knee and fingers. 5. **Correctives:** 0 none where the ROM needs them; 1 correctives that show at rest, or readers that cannot see a controller-driven joint; 2 pre or post chosen for the right reason (mesh wrong: pre; one posed result: post, Transform Space when one joint owns it, Tangent Space across joints), 0 at rest and 1 at the pose reached through the controls; 3 plus mirrored, extremes first, drivers inspectable (`S.joint_drive` reported), residual reported. 6. **Regenerability:** 0 only in the scene; 1 weights exported; 2 plan or layers exported (JSON), master shapes kept; 3 a rebuild from data reproduces the weights (import diff < 1e-4). 7. **Handoff:** 0 layers still on, source overwritten; 1 layers deleted, no report; 2 `mx_validate(profile="rig")` clean, weights JSON, ROM sheet looked at; 3 plus the list of what was not verified. A stage passes at 2 on every line; a hero asset needs 3 on lines 2 to 5. ## 5. Self-critique questions before reporting - Did I look at a render or playblast of the worst three poses, or only at numbers? - Is any warning explained by a design choice (fold layer, crease) rather than ignored? - Would the result survive a mesh update from the modeler (weights and masks regenerable) and a joint move from the rigger? - Did I change the cap, the skinning method or the deformer order after the last ROM run? Then run it again. - Did I state which checks ran headless, which in the GUI, and which not at all? -
expert-notes.md 26.3 KB
# Expert notes: deformation (skinning, Skin Tools, shapes, correctives, face, ML Deformer) Load when a decision needs the reasoning behind a rule, when experts disagree, or when judging a result the way a named expert would. Timestamps are `[hh:mm:ss]` in the source video; "Help" is the Autodesk Maya 2027 Help (full list in `sources.md`). [added] marks this skill's own additions; [verify] marks names not yet confirmed in Maya 2027. ## 1. Viktoras Makauskas (author of ngSkinTools, now Skin Tools in Maya 2027): full-body layer skinning - **Throw away the default layer.** Initialize layers, discard the layer holding every influence, rebuild region by region, each on its own layer stacked over the torso: torso, shoulders, upper arm, forearm, hand, thumb, fingers, head, legs, feet [00:01:11]. - **Block 100% first.** Select the region's joints, Assign from Closest Joint: every vertex 100% on its closest joint [00:01:41]; Replace brush at intensity 1 for blocking; hold S to sample an influence (ngSkinTools2 shortcut [verify in 2027]) [00:01:55]; Screen projection where strokes must reach back faces [00:02:28]. - **Raw weights are a rig diagnostic.** "When you're posing pay attention at the deformation with raw weights: it should look relatively close to your expected final result; if it looks way off there might be some underlying issues with the rig itself" [00:08:27]. A wrist bump "really needs the joint pivot adjusted" [00:15:39]; smoothing the neck instead of fixing the rig: "don't do this in production" [00:20:33]. - **Calisthenics:** select all controls, key them every 10 frames, a pose on every second key, so the character goes to a pose and back to bind [00:03:01] [00:03:58]. One motion per test: his first forearm test bent and lowered the arm at once and had to be rebuilt [00:10:36]. Very extreme and "weird" poses expose errors and the distribution between joints [00:15:11] [00:16:14]. - **Fill, then mask.** On a limb layer, fill the whole layer with that region's joints, then paint the layer mask; the chapter title calls it "very important" [00:07:57] [00:08:12]. "Mask is not an actual influence in the skin cluster; instead it will define the transparency of the layer" [00:08:46]. He polishes the mask mostly with Smooth, checking every pose [00:09:18]. - **Twist distribution is judged by eye.** Three twist joints in the upper arm and three in the forearm [00:07:57] [00:10:10]; bicep volume loss fixed by more weight on the middle twist joint [00:09:44]; more weight on the first forearm twist for less twisting near the elbow [00:13:24]. He has seen people "take out their calculators and do the math" and does not [00:11:31]. - **Smoothing is not free.** A "pretty strong" smooth pass while scrubbing the calisthenics [00:04:11]; flooding smooth on low-density fingers lost too much volume [00:18:35]; instead smooth the middle loops and use Sharpen, "an opposite of smooth brush" [00:18:51]. The foot became rubbery: a soft-shaped Add brush restores rigidity [00:23:31]. - **Multi-shell geometry:** volume smoothing makes a necklace and the body agree [00:05:33]; it also fixes body geometry poking through the shirt [00:06:24]; deselect the shirt so only the body smooths and the armpit sticks to the shirt [00:07:18]; select only the necklace vertices so the neck weights stay untouched [00:21:29]. - **Debug by toggling layers:** an artifact that appears with one layer enabled belongs to that layer [00:13:54]. - **Done means an animator played with it** [00:25:02]. ## 2. Mitchell Jao (character animator, Blue Sky, Blizzard, Disney): region layers and proxy transfer Captions are machine-translated; quotes are approximate. - **Normalization is the root difficulty of vanilla painting:** removing weight sends it to a joint you did not choose [00:02:25] [00:04:34]. Vanilla discipline: lock everything but the two joints you trade between, add to the one you work on, smooth only between the two unlocked joints [00:05:08] [00:05:41]. - **Bind method barely matters** when everything is repainted; he binds only the joints he wants, max-influence maintenance off [?] [00:09:01] [00:09:33]. Default weights: "Obviously, terrible. We're going to overwrite all of this" [00:10:07]. - **Assign from Closest Joint maps upwards along the chain**, "exactly what you want from FK joints" [00:10:39]; flood Smooth several times [00:11:44]. - **One layer per decision:** a clavicle layer filled 100% everywhere and masked to the arm and shoulder keeps the spine weights consistent; trading weight between chest joints on one layer "gets very dirty" [00:13:19] [00:16:06]. Shrug art-directed with Smooth and Scale, neck kept still [00:14:59]. - **Even twist, directed mask.** Five twist joints per section on his rig [00:17:13]; flood smooth until the twist gradation is even; weights outside the arm do not matter because the mask zeroes them [00:18:20] [00:18:51]; most time goes into the mask [00:20:36]. "You make the twists in the layer and give them even weight, and then, with the mask, you artistically direct the shape of the elbow" [00:22:19]. Softening twists later does not disturb the elbow: "work in a very modular way" [00:25:00]. - **Hard 100% weights can be right** where the rig has one joint per section and moves smoothly itself (eyelids) [00:28:22]. Metacarpals: four joints weighted evenly [00:25:00]. Finger "fold" layer: the finger bends in the crease, not above it [00:26:09]. - **Layers are an authoring convenience:** the result is a normal skinCluster and the tool can leave the scene [00:31:18]; export layers to JSON to keep the layered work [00:31:49]. - **Proxy weights:** loft a surface along lip joints, weight it 100% per joint, copy onto a duplicate of the main mesh (the transfer ignores topology, which is the point), export that layer, import it on the main mesh by vertex ID, then paint the mask there [00:33:09] [00:34:47] [00:36:26]. Split upper and lower lip proxies or the lips stay stuck [00:37:00]. Change the lip joint count, rebuild the proxy, keep the mask work [00:39:49]. ## 3. Maya Learning Channel: Skin Tools quick start (Maya 2027 UI) - Skin Tools works on an already bound mesh [00:00:03]; Initialize Skin Layers [00:00:29]; per-layer influences isolate edits to that layer ("your edits only affect the left leg") [00:00:58]; Assign From Closest Joint for a quick start [00:01:17]. - Screen projection for the rough pass through the limb, Surface to refine [00:01:32]; Replace to paint, Smooth to refine [00:01:32]; mirror via Set Weights > Common Settings > Mirror and Paint > Interactive Mirror [00:02:02]. - Test with the real controls: lift the foot, look for areas that "pinch, collapse, or stretch" [00:02:25] [00:02:40]. Refine with selection-limited Smooth applied repeatedly (grow selection, Set Weights > Smooth > Apply) [00:02:48]. ## 4. Maya Learning Channel (Matt Chan): multiple skinClusters and UV Pin on a face - Several skinClusters keep compound effects "orderly and separate" (squash and stretch under facial tweaks) [00:01:03]; controls drive joints through Offset Parent Matrix [00:01:37]. - Selection order is data: bind and pin in the same order so pin i pairs with control group i [00:01:57] [00:07:36]. The second bind asks to create a second cluster [00:02:37]. - Copy weights cluster to cluster from a reference head (Copy Skin Weights options, Skin Cluster source and target) instead of repainting [00:04:03] [00:05:16]. - **Cycle escape:** the pins must read skinCluster1.outputGeometry (squash only), not the final mesh the controls deform: "the controls need to know how the face looks, but the face needs to know where the controls are" [00:06:27] [00:07:01]. - **Double transform fix:** connect each control group's inverse matrix into the matching bindPreMatrix of skinCluster2 [00:08:22] [00:08:57]. [added] This is exact only when the tweak joint sits at its group's origin at rest; `test_multi_skin.py` measures it (region moves once, not twice). ## 5. Antony Ward (antCGi, games veteran): blend shape face and the lip generator; base mesh deformation testing Blend shape face (RbQM82Ta6zE): - **Joints for rotation, shapes for flesh:** blend shapes are linear, "they don't work well on an eyeball"; eyes, lids and jaw stay on joints [00:01:38] [00:02:13]. Separate the head from the body so shapes only compute head vertices [00:03:12]. - **Additivity:** "blend shapes are additive ... we will get double movement"; corner vertices are excluded from lip shapes that corner shapes own [00:08:58] [00:09:31]. Wide plus smile overshoots: replace the smile with a pure raise [00:52:53]. - **Generator:** one master shape, section heads chained with blend shapes ("using blend shapes to create blend shapes"), complementary per-loop weights (0.8/0.2, then 0.2/0.4/0.7 against 0.8/0.6/0.3) so the sections at 1 equal the master; check each section alone for leaks under the nose and on the chin; keep the generator hidden, never delete it [00:20:32] [00:23:36] [00:30:38] [00:31:59] [00:32:35] [00:38:39]. - Build one side, Mirror Target (direction minus, Object X) or Duplicate plus Flip Target [00:42:14] [00:44:22]. Exaggerate shapes slightly for the animator [00:46:43]; check the engine's shape limit early [01:07:58]. Negative weights sometimes give the opposite shape for free, "this doesn't always work" [01:11:57]. - Soft selection falloff Surface, not Volume, or both lips move [00:07:22]; Edit must be on (red) or edits land on the wrong target [00:04:51]. Base mesh deformation testing (x07USYlvu2o, modeling series): - Test deformation before texturing [00:00:00]; anchor root joint so the body does not follow the shoulder [00:03:34]; Weight Distribution Neighbors "makes painting weights more predictable" [00:04:12]; paint in a deformed pose [00:05:16]. - **Who fixes it:** at a clean elbow "the topology is okay so it would need something extra in the rig" (correctives) [00:10:27]; at wrist and ankle the loops run the wrong way and must be rebuilt [00:11:04] [00:17:37]; "reworking the topology would be much better" than corrective shapes when flow is the cause [00:16:24]. - Delete Non-Deformer History, never History, on a skinned mesh [00:12:41]; unbind with Delete history to return to the default pose [00:13:49]. Face tests with joints because blend shapes break when topology changes [00:19:00]. Studios keep a ROM file of simple to extreme poses [00:21:10]. ## 6. Josh Burton (facial rigger, Midway; Morpheus rig): facial rigging judgment - Two families: blend shape rigs (cinematics, Morpheus) and joint rigs, "typically ... with video game projects ... because game engines seem to prefer that" [00:13:03] [00:13:35]; joints riding on shapes is "more of a film kind of workflow" [01:02:31]. - **Budget first:** "what's our joint budget and what kind of range of motion are you going to get out of this character" [00:35:45]; 24 face joints on his current-gen asset; with few joints, cheek joints beat a centerline lip joint because cheek squash sells the smile [00:36:51] [00:37:23]. - **Lips slightly parted in the base mesh** made skinning "night and day easier"; coincident lip vertices break weight mirroring and wraps [00:26:21] [00:29:33] [01:09:51]. - **Rotation-only joints:** exact eyeball pivot (no translation needed) [00:48:33] [00:49:07]; lower lip and tongue under the jaw for the free ride [00:49:42]; joints oriented along muscle paths, centerline joints world oriented [00:50:09] [00:50:45]. - **Transferable SDK faces:** off-hierarchy joints, a rest pose at zero; snap joints to a new head, reset the rest, reuse every pose: about 1.5 days per new head [00:38:46] [00:39:19] [01:12:19]. - **Influences:** he wrote a tool to find vertices over the cap; Unreal mobile allowed 2 per vertex, and 4 across the centerline was already "nightmarish" [00:51:19] [00:51:55]. - **Asset checks:** no baked shadows at lids, lips, nostrils; brow textures inside plausible bounds; loops that follow the face; mouth cavity, teeth and gum clearance; mouth corners checked before sign-off [00:30:17] [00:31:24] [00:33:03] [00:34:38] [01:08:45]. Mesh edits while rigged: skin a polyUnite of the pieces [00:52:27]. Additive corrective by subtracting the base motion (abSymMesh) [00:18:46]. Keep a reset head [00:13:35]. ## 7. Todd Widup (film rigging supervisor): layered film rig and the ML Deformer (Maya 2025.2) - **Layered hybrid:** a segmented low-res cage with correctives drives a continuous low mesh by proximity wraps, legs and torso as separate wrap drivers so the thigh can push the belly without interference, then mid and high resolution [00:02:17] [00:03:23] [00:04:00]. Keep the control rig light so slowness comes only from deformation [00:05:39]. - **Delete Delta Mush before training** so the ML learns the skinCluster alone [00:07:55]. Control Collector gets the joints that drive the mesh, not animator controls [00:10:08] [00:10:41]. - **Pose coverage is the lever:** about 30 degree steps per axis per joint (range -30 to 90 gives 5 poses), 1,500 to 2,000 random poses for a full character [00:15:50] [00:16:22]; one joint at a time, because cross talk is the typical failure [00:27:41]; 12 to 20 arm poses wanted, a 13 to 15 pose shoulder set generalized well [00:37:08] [00:39:55]. "It is what you feed it" [00:35:58]. - **Settings seen:** first run batch 128 to 256, epochs 1,000 to 2,000, validation 0.2 to 0.4, dropout 0.05, principal shapes 40 at 95% [00:21:25] to [00:24:49]. A bigger network (around 10 layers, up to 4096 neurons [?]) bulged and cross-talked; 5 layers fixed it [00:31:13] [00:32:32]. Final arm: epochs 3,000, validation 0.3, 5 layers, dropout 0.10, principal shapes limit 40 at 97%, only 15 needed [00:42:10] [00:42:44] (batch size and neuron counts are garbled in captions). - Autodesk's test: about 17 fps with the full rig to almost 60 fps with the ML Deformer [00:42:44]. Uses: crowds, background characters, maybe hero with time [00:44:23]. ## 8. Darren Rudy (EA principal technical artist): game PSD with secondary joints in Bifrost - **PSD for games is secondary joints driven by poses** [00:14:18]: driver graph (swing-twist decomposition of a matrix) > pose graph (a driver range remapped to 0..1, e.g. 0 to 4.83, into named poses like arm abduction) > per-region secondary graphs where each secondary joint blends per-pose transforms [00:16:28] [00:18:38] [00:20:47]. "It's never at a pose, it's always at a percentage" [00:21:20]. Artists tune by moving pose locators live, even between poses [00:22:27]. - **Shareable deformation:** a common skeleton in structure and position; secondary joints placed by ratios and raycasts (e.g. 50% between clavicle and arm, then a ray to the trapezius) so they transfer across morphologies [00:06:39] [00:10:22]; blend two authored weight maps by body type [00:11:28]. - **LOD every secondary joint** at design time (Switch and PS5 cannot run the same thing) [00:38:41]. Maya node PSD ran at 4 fps; the Bifrost port at 60 fps [00:37:35] [00:38:07]. Code builds structure, compounds hold logic [00:32:44]. Skinning itself: procedural first, then manual art direction [00:48:26]. ## 9. Autodesk Maya 2027 Help (authority for 2027 behavior) Skinning and Skin Tools (`sources/docs/deformation__skinning-skin-tools-2027.md`): - Heat Map "generally gives better default results"; only influences inside the mesh emit heat, so end joints outside get nothing. Geodesic Voxel handles non-manifold, non-watertight, intersecting, multi-piece meshes, needs every joint inside and outward front faces, treats several meshes as one volume, and must be followed by `geomBind` when created with `bindMethod=3`. - Linear skinning gives the "bow tie" or "candy wrapper" at twisting wrists and elbows; dual quaternion preserves volume; weight blended mixes per joint. - Bind pose blockers: constraints, expressions, keyed IK; Modify > Evaluate Nodes > Ignore All, Go To Bind Pose, Evaluate All. - `skinCluster`: `normalizeWeights` 0 none, 1 interactive, 2 post ("default" on the command page; the Prune page calls Interactive the default mode); editing `maximumInfluences` or `dropoffRate` in edit mode loses custom weights; `heatmapFalloff` 0 smooth (many small weights) to 1 (closest joint dominates); `multi` allows a second cluster; `removeUnusedInfluence` query counts all-zero influences; `dumpInfo` returns a Python dict. - `copySkinWeights`: `surfaceAssociation` defaults to closestComponent on the command page while the UI default is Closest point on surface; `influenceAssociation` defaults to closestJoint; `mirrorMode` defaults to XY; `mirrorInverse` flips the direction (positive to negative by default). - Copy needs overlapping, similar meshes; UV space for different proportions; weight maps are UV based and need non-overlapping UVs. Adding an influence object alters nearby weights: export, add, import, then paint. - **Skin Tools:** scriptable through `ngSkinTools2.api` (Developer Help, "Skin Tools Python API": "meant to be used primarily from the UI. However, it is possible to access some of its functionality from Python"; the C++ plug-in commands are "not intended to be used directly"). Layers exist only after explicit initialization; masks are grayscale alphas; layer groups share one mask (hands, feet); Interactive Mirror, Mirror tab, or Mirror effect applied on write (not visible while painting); Max Influences in the Effects tab is post-process (not visible while painting); Prune from the Skin menu hits all layers, from the Tools tab the active layer only. While layers exist, the classic Paint Skin Weights, Set Max Influences, Hammer, Paste, Move Weights to Influences, Import Weight Maps, Mirror Skin Weights, Substitute Geometry and Normalize are blocked, and Copy Skin Weights is unavailable. Skin > Smooth Skin Weights uses the Skin Tools algorithm (temporary layer, single layer, or an error with several layers). Delete Skin Layers keeps the evaluated weights, "useful when finalizing a rig before handing it off to animation". The paint preference defaults to the classic tool. Blend shapes, PSD, deformers (`sources/docs/deformation__blendshapes-psd-deformers.md`): - Pre-deformation corrective: "like fixing the original character's mesh", holds in other poses. Post-deformation: Tangent Space (local vertex frame, not UVs) or Transform Space (a joint's space, faster, bounded by the joint's envelope). - PSD: problems at shoulder, underarm, knee, groin; build extremes first, then in-betweens, or mark poses Independent; three neutral poses; Regularization "usually somewhere between 0.01 and 1.0" (the same page later suggests "something like 0.001" for wobbliness: start at 0.01 and test); weights far outside 0..1 mean inputs beyond the stored poses or Gaussian falloffs too big (add a pose, or Auto-adjust Gaussian Falloff); Euler Twist for a wrist twisting about the forearm; controller-driven joints need their driving attributes before neutral poses; mirroring needs case-sensitive R_/L_ names. - Delta Mush is a low-pass filter usable for "paint free skinning"; Tension preserves relative edge lengths; Proximity Wrap drivers must line up with targets at bind, ProxNet avoids double deformation; Morph keeps legacy CPU deformers from blocking GPU evaluation. - ML Deformer: anything ranked above it in the deformation stack is not approximated; Target Morph toggles target and ML; training downloads Python modules into `<MAYA_APP_DIR>/mlDeformer/<version>/Lib/site-packages`; Principal Shapes analysis tells how many shapes reach an accuracy. ## 10. From the rigging and modeling digests (upstream facts this skill relies on) - A skinCluster only consumes influence world matrices and bind pre-matrices; the bind pose is data you can reset, never a reason to rebuild joints (Fragapane, rfLEBgOEW1A [01:22:26] [01:35:18]). - Twist joints: first upper-arm and thigh twist at the top stays with the shoulder or hip, twist distributes along the bone (antCGi, fGacyVzJGIU [00:20:47]); twist is judged mid-limb, not at shoulder and wrist (antCGi, x07USYlvu2o [00:08:40]). - Model gate: cm, feet at 0, centered on X, frozen, symmetric pieces combined for weight mirroring, arms raised enough to paint under (antCGi, e74KphYwMww). ## 11. Disagreements and deciding conditions | Topic | Position A | Position B | Decide by | | ----------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Face: shapes or joints | Shapes plus joint eyes and jaw (antCGi); shape rigs for cinematics (Burton) | Joint faces for games (Burton); secondary joints for PSD in engine (Rudy) | Runtime and budget (joints, shapes, influences), need for sculpted detail. Film layers joints on shapes (Burton [01:02:31]). | | Smooth vs 100% weights | Smooth, then shape (Makauskas) | 100% per section where one joint per section and the rig moves smoothly (Jao eyelids, proxies) | Joint density and whether the joints' own motion is smooth. | | Influence cap timing | Off at bind, paint freely (Jao) | Hard 2 to 4 for games (Burton) | Enforce the platform cap at the end: Skin Tools Effects tab or `prune_and_limit`. | | Twist weighting | Per pose by eye (Makauskas) | Even, then leave it and shape the mask (Jao) | Both reject calculators; Jao's split needs a mask to carry the shape. `chain_ramp` gives Jao's even start. | | Delta Mush | "Paint free skinning" and shot fixes (Help) | Delete before ML training (Widup) | Whether the smoothing is part of the look you want learned or baked. | | Pre vs post corrective | Pre fixes the base mesh (Help) | Post fixes one pose (Help) | Is the mesh itself wrong, or must the fix hold in neighboring poses (pre)? Or only the posed result (post)? | | Post target space | Transform Space: faster, corrections stay within the selected joint's envelope (Help) | Tangent Space: the surface's local vertex frame, not UVs (Help) | One joint owns the fix: Transform. The fix crosses joints or must follow the surface: Tangent [added reading]. | | PSD on a driven joint | Pose Editor defaults: neutral poses at creation (Help) | Controller-driven joints: no neutral poses first, controller attributes in Driver Settings, then Add Neutral Poses (Help) | Is the joint moved by IK, constraints or an OPM blend (`S.joint_drive`)? Then the controller order, pose through the controls, and node readers on `dagLocalMatrix` [added]. | | Pose data | PSD: extremes first (Help) | ML: 30 degree steps, 1,500 to 2,000 poses (Widup) | Few poses per interpolator vs a learned global approximation. | | Vanilla vs layers | Lock discipline (Jao) | Layers remove the bookkeeping (Jao, Makauskas) | Layers block classic tools in 2027: script plain clusters, or delete layers before classic operations. | | Topology vs correctives | Rework loops (antCGi wrist, ankle) | Correctives at a clean hinge (antCGi elbow, knee) | Swap topology under identical weights: if it deforms better, it was topology. | ## 12. Measured in this skill's offline tests [added, run 2026-09-24 without Maya] - Linear skinning at 50/50 weights under a 90 degree twist keeps cos 45 = 0.707 of the radius: the mid-forearm section keeps 50% of its area and 71% of its perimeter; dual quaternion keeps 100% (`test_mx_skin_offline.py`, `candy_wrap` note). - 100% closest-bone weights at a 110 degree elbow do not compress any edge: the pieces pass through each other (3.9x stretch outside, folds and collapsed faces inside). Edge ratios alone miss that; folds and collapsed faces catch it. - Smoothing the same elbow lowers the worst stretch (3.9 to 1.5) but collapses the inner elbow (minimum edge ratio 0.19): at deep bends more smoothing is not the fix; a corrective or volume preservation is. ## 13. Gaps (no source in this project) - FACS shape sets and their combination correctives. - Film influence counts and film skinning review criteria from a creature TD. - Hip, groin and buttock specifics (only "lift the foot and watch the bend", groin listed as a PSD area, thigh pushing the belly). - Behavior of the Skin Tools API on a live 2027 install (the Developer Help pages were fetched on 2026-09-24 and name `ngSkinTools2.api`, but nothing has been imported or run yet). -
gui-paths.md 31.6 KB
# GUI paths: menus, hotkeys and tool settings (Maya 2027, for a computer-use agent) Load when operating Maya's interface instead of, or next to, the scripted procedures (weight painting, Shape Editor sculpting, Pose Editor, ML Deformer training). Menu names come from the Maya 2027 Help and the 2027 Skin Tools quick start video; items marked [verify] come from older tutorials. macOS: Ctrl in the Help means Control, Alt means Option (version deltas). Menu set for everything below: Rigging (F3), unless stated. P numbers point to `procedures.md`. ## Bind, unbind, bind pose (P1, P2) | Action | Path | Settings that matter | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bind | Skin > Bind Skin (option box) | Bind to: Selected joints; Bind method: Closest distance, Closest in hierarchy, Heat Map, Geodesic Voxel; Skinning method: Classic Linear, Dual Quaternion, Weight Blended; Normalize Weights: None, Interactive, Post; Weight distribution: Distance, Neighbors (antCGi: Neighbors is more predictable [00:04:12]); Max Influences and Maintain max influences (off while authoring, Jao); Geodesic: Falloff, Resolution, Validate voxel state; **Initialize Skin Layers** (off by default) | | Second cluster on the same mesh | Skin > Bind Skin, answer Yes to the existing-bind warning | (Maya LC multi-skin [00:02:37]) | | Unbind | Skin > Unbind Skin (option box) | Delete history (back to the default pose), Keep history (keep cluster data), Bake history (keeps the current pose); Skin Clusters: Unbind All or pick from the list | | Go to bind pose | Skin > Go To Bind Pose | If blocked: Modify > Evaluate Nodes > Ignore All, Go To Bind Pose, then Evaluate All (Help) | | Clean modeling history, keep the skin | Edit > Delete by Type > Non-Deformer History | never Delete by Type > History on a skinned mesh (antCGi x07 [00:12:41]) | | Deformation order | right-click the mesh > Inputs > All Inputs, middle-drag to reorder; or the Attribute Editor Deformation widget (2025+) | | | Check normals for Geodesic Voxel | viewport Lighting or Shading > Backface Culling | front faces must point out (Help) | | See joints through the mesh | viewport Shading > X-Ray Joints | | ## Skin Tools window (P4, P5, P6, P7) Open: Skin > Skin Tools (then **Initialize Skin Layers**), or Skin > Initialize Skin Layers. After initialization Skin > Paint Skin Weights opens Skin Tools. Preference: Preferences > Animation > Rigging > Paint Skin Weights Tool (Classic by default). | Area | Use | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Skin menu (window menu bar) | pick the active skinCluster when a mesh has several (checkmark; newest first) | | Layers list | right-click > Create Layer (or Layers > Create Layer); double-click to rename; toggle a layer to find the culprit (Makauskas [00:13:54]); reorder: what shows through a mask is what lies below (Jao [00:26:44]); layer groups share one mask (hands, feet) | | Influences list | search field; **Mask** is listed first: select it to paint the layer alpha; Tools > Add Influences isolates joints to the layer | | Tools menu | Assign From Closest Joint (block), Add Influences, Prune Small Skin Weights (active layer only) | | Paint tab | brush modes Replace, Add, Scale, Smooth, Sharpen; projection Surface, Volume (across shells), Screen (through the mesh); shape Hard, Soft, Gaussian; Intensity; Display > Current influence, colored; **Interactive Mirror** | | Set Weights tab | Common Settings > Mirror; Smooth > Apply on a selection (grow with Shift + >, repeat, Maya LC [00:02:48]); influence limit visible while painting | | Mirror tab | axis, Mirror (one-time copy), Influence Mapping editor, Vertex Mapping (symmetry mesh) | | Effects tab | Mirror effect (applied on write, not visible while painting), Prune Small Weights before writing, Max Influences (post-process) | | Joints panel | right-click: Lock Selected, Unlock Selected, Lock Unselected, Unlock Unselected | | Hotkeys in the paint tool | H on-screen help; B + left-drag brush radius; hold Shift to smooth while painting; Ctrl+Z undo; S sample the influence under the cursor (ngSkinTools2, Makauskas [00:01:55]) [verify] | | Export and import layers | Skin Tools export/import of layered data (Help names Export and Import; Jao used Export Layers to JSON and Import Layers with vertex ID transfer) [verify menu location] | | Finish | Skin > Delete Skin Layers (weights kept); Skin > Reset Default Weights deletes layers and re-initializes | Expert order in this window: create a region layer, add its influences, Assign From Closest Joint, Replace intensity 1 to block, pose with the calisthenics animation, Smooth strongly while scrubbing, select Mask, Replace-flood the region, polish the mask with Smooth (Makauskas, Jao, quick start). ## Classic weight tools (only on clusters without layers) | Action | Path | Notes | | -------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paint | Skin > Paint Skin Weights (option box) | Paint Operation Replace, Add, Scale, Smooth; Profile; Opacity and Value 1 with Flood for blocking by selection (antCGi x07 [00:05:49]); Skin Clusters section for multi-cluster meshes; lock all but two influences (Jao [00:05:08]); reflection is disabled, use Mirror Skin Weights | | Copy one vertex's weights | Paint Skin Weights in Select mode, Copy then Paste icons; Option+Control+C / Option+Control+V on macOS | | | Copy between meshes | Skin > Copy Skin Weights (option box) | Surface Association: Closest point on surface (UI default) or UV space; Influence Association: Closest joint, One to one, Label, Name; Skin Cluster Source and Target lists | | Mirror | Skin > Mirror Skin Weights (option box) | Mirror across YZ for characters; direction; mesh centered, pose symmetric | | Prune | Skin > Prune Small Weights (option box) | Prune below; with layers it prunes all layers | | Smooth | Skin > Smooth Skin Weights (option box) | Method: Skin Tools (default) or Legacy; errors on a multi-layer cluster | | Typed weights | Windows > General Editors > Component Editor > Smooth Skins | Skin Cluster and Skin Layer selectors appear with layers; Total shows 2.0 with two clusters | | Weight maps | Skin > Export Weight Maps / Import Weight Maps | UV based, needs non-overlapping UVs; import blocked with layers | | Bake a deformer stack to weights | Skin > Bake Deformers to Skin Weights (2025 added Range of Motion options) [verify menu name] | | ## Shape Editor and correctives (P11, P14) | Action | Path | Settings | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Open | Windows > Animation Editors > Shape Editor, or the Sculpting shelf | | | New deformer | Create > Blend Shape Deformer (option box) | Advanced tab > Deformation Order: Pre-deformation (fix the mesh) or Post-deformation (fix a pose) | | Target | Create > Add Target (option box) | Type: Tangent Space (green icon) or Transform Space (blue icon, Transform Name or Use Selected). Transform when one joint owns the fix (faster, stays in that joint's envelope), Tangent when it spans joints (Help; [added] reading) | | From meshes | Create > Add Selection as Target | the generator's baked sections (antCGi [00:41:05]) | | In-between, combination | Create > Add In-Between Target; Create > Add Combination Target (option box) | Method: Multiplication, Lowest weighting, Smooth | | Edit a target | the target's Edit button (red while editing) | edits land on the wrong target when it is off (antCGi [00:04:51]) | | Target operations | Shapes > Duplicate Target (Ctrl+D), Flip Target, Mirror Target (option box: axis or topology, direction minus, Object X), Rebuild Target, Key, Reset | | | Erase and paint target weights | Erase Target tool in the Shape Editor; Deform > Paint Weights > Blend Shape [verify 2027 path] | white full, black none | | Save shapes | File > Export Shapes / Import Shapes (.shp) | | | Sculpt | Sculpting shelf Grab (Shift smooths), Relax, Inflate (cheek puff), Smooth; Ctrl+Shift+right-click > Symmetry; B + drag radius, B + middle-drag strength | soft selection falloff Surface, not Volume, on lips (antCGi [00:07:22]) | ## Pose Editor (PSD) | Action | Path | Settings | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Open | Windows > Animation Editors > Pose Editor, or Deform > Pose Space Deformation | | | Interpolator | select the joint, Create Pose Interpolator (button adds 3 neutral poses) or Poses > Create Pose Interpolator (option box) | controller-driven joints (IK, constraints, scenario-maya-rigging's OPM blend): answer No to neutral poses, right-click > Show Driver Settings, Controllers > Add (middle-drag Rotate X, Y, Z from the Outliner with Display > Attributes), then Poses > Add Neutral Poses; neutral poses made too early: delete them and redo after the controllers. Pose through the controls. | | Pose | pose the joint, Add Pose (or Poses > Add Pose option box) | Pose Type: Swing, Swing and Twist, Twist; Edit is on; weight 1; sculpt; Edit off | | Tune | View > Advanced | Regularization (start 0.01), Output Smoothing, Interpolation Linear or Gaussian, Allow Negative Weights, Track Rotation and Translation, Gaussian Falloff, Auto-adjust Gaussian Falloff, Independent, Rotation and Translation Falloff; Driver Settings: Twist Axis, Euler Twist | | Check | double-click a pose (Go To Pose); rotate the joint between poses | | | Mirror | Poses > Mirror (option box) | Symmetry Axis Object X or Topology (store seam edge); source string R_, target L_, case sensitive | | Save | File > Export All / Export Selection (.pose plus .shp files) | | | Tumble around the sculpt | Tumble tool settings: Tumble About Tumble pivot, Automatically set tumble pivot | | ## Deformers, pins, face plumbing (P12, P13) | Action | Path | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Delta Mush, Tension, Proximity Wrap, Morph | Deform > Delta Mush / Tension / Proximity Wrap / Morph (Proximity Wrap drivers: Attribute Editor > Manage Drivers) | | UV Pin controls to the surface | select the mesh, then the controls in order, Constrain > UV Pin (option box: Output = New Locator) | | Break the cycle, fix the double transform | Windows > General Editors > Connection Editor: skinCluster1.outputGeometry to uvPin.deformedGeometry; control group Inverse Matrix to skinCluster2.bindPreMatrix[i]; inspect in Windows > Node Editor | | Editable pieces while rigged | Mesh > Combine (polyUnite) of the pieces, skin the combined output (Burton [00:52:27]) | | Bifrost PSD authoring | Windows > Bifrost Graph Editor; Rigging menu set > Bifrost Rigging (2026+) | ## ML Deformer (P15) | Step | Path | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Add | Deform > ML Deformer (Modeling F2, Rigging F3 or Animation F4 menu set) | | Order | Attribute Editor, the shape's tab, Deformation stack: move the ML Deformer above the deformers to approximate (anything above it is not approximated) | | Target | ML Deformer tab, right-click the Target Geometry column > Create Target for Deformers Post ML Deformer; or a separate target mesh (Replace Target with Selected, Widup [00:17:30]) [verify label] | | Controls | Control Collector tab > Add Controls table, right-click Add Selected or Add All (the skinCluster joints, Widup [00:10:41]); Connected Controls Min and Max; pose generation options | | Data and training | Export Training Data (location, start and end frame, name; Delta Mode; smoothing iterations); Train the Model (output name, Preload Data); Training Settings > Advanced > Principal Shapes > Analyze; ML Deformer Job Monitor; wait for "[ML Deformer] Training complete" | | Evaluate | Enabled on; Target Morph toggles target and ML; Envelope; 2026+ Apply Mesh Compare heat map | | Samples | Windows > Content Browser > Examples > Animation > MLDeformer | | Pin the Attribute Editor (Widup [00:09:36]) so selecting joints does not switch it. | ## Review | Action | Path | | ------------------------------------- | -------------------------------------------------------------------------------------------------- | | Playblast a ROM | Windows > Playblast (option box), or `mx_review.playblast` through the scenario-maya-expert bridge | | Wireframe on shaded, default material | viewport Shading menu (the review needs topology visible, antCGi [00:47:19]) | -
procedures.md 23.7 KB
# Procedures: skinning, Skin Tools, ROM tests, correctives, face, ML data (Maya 2027) Load when executing a stage of the workflow. Every block states its status and its test. **Status (2026-09-24): Maya 2027 is not installed. Every block below is NOT YET RUN IN MAYA** except where it says "ran offline" (pure Python in `mx_skin`, `tests/code/maya-deformation/test_mx_skin_offline.py`, python3). Run all tests with `tests/code/maya-deformation/run_all.sh`; each Maya test is an mx_run job (`python3 <skills>/scenario-maya-expert/scripts/mx_run.py tests/code/maya-deformation/<test>.py`). Setup for every block (inside mayapy or through the scenario-maya-expert bridge): ```python import sys sys.path.insert(0, "<skills>/scenario-maya-deformation/scripts") # adds scenario-maya-expert/scripts for mx_audit too import maya.cmds as cmds import mx_skin as S ``` Conventions: weights `W` are a list of `{physical influence index: weight}` per vertex; points are cm, world space; `S.read_weights(sc)["influences"]` gives the index order. --- ## P1. Intake: mesh and skeleton gates before any bind Test: `test_skin_io.py` (precheck part), `test_mx_skin_offline.py` (ray parity and shell volumes ran offline). ```python import mx_validate, mx_audit rep = mx_validate.validate(profile="model", roots=["body_geo"]) # scenario-maya-expert: history, frozen, manifold, UVs aud = mx_audit.audit("body_geo") print(mx_audit.verdict(aud, profile="film", symmetric=True)) # X symmetry for mirroring pre = S.bind_precheck("body_geo", bind_joints, method="heat") # joints inside, normals, non-manifold, driven joints print(pre["problems"]) ``` Decide the method from `pre`: watertight, single shell, joints inside: `heat`. Multi-piece, non-manifold, open: `geodesic`, only if `joints_outside` and `inward_shells` are empty (Help). Everything is repainted anyway (Jao [00:09:01]), so the method only sets the starting point. ## P2. Bind with explicit settings Tests: `test_skin_io.py`, `test_00_probe_deformation.py` (defaults and geomBind headless). ```python b = S.bind("body_geo", bind_joints, method="heat", skin_method=0, normalize=1) # no cap while authoring sc = b["skinCluster"]; print(b["notes"], b["settings"]) # game target, cap known up front and never edited later (editing -mi wipes weights, Help): b = S.bind("body_geo", bind_joints, method="geodesic", max_influences=8, voxel_resolution=256) ``` - Pass `normalizeWeights` explicitly: the command page says post is the default, the UI binds Interactive. - `method="geodesic"` runs `geomBind` after `skinCluster(bindMethod=3)`; if `notes` reports a failure in mayapy, bind in the GUI through the bridge or use heat. - Must be at the bind pose to bind more objects (Help). Return to it: `cmds.dagPose(root_joint, restore=True, bindPose=True)` [verify], tested soft in `test_skin_io.py`. - Do not initialize Skin Tools layers on throwaway test binds (antCGi deformation test): classic tools stay available. ## P3. Read, write, audit, back up weights Status: not yet run in Maya (audit maths ran offline). Test: `test_skin_io.py`. ```python d = S.read_weights(sc) # MFnSkinCluster.getWeights, plug fallback W, names = d["weights"], d["influences"] a = S.audit_weights(W, names, max_influences=4) print(S.weights_verdict(a)) # error: empty rows, bad sums, over budget; warn: tiny weights S.export_weights_json("body_geo", "/abs/out/body_weights_v003.json") # before any OpenMaya write (not undoable) S.write_weights(sc, W) # refuses a cluster with Skin Tools layers ``` Restore or transfer: `S.import_weights_json(path, "body_geo")` (index mapping when topology matches, closest point otherwise; binds to the named joints if the mesh has no cluster). ## P4. Region blocking without a brush (plain skinCluster) Status: not yet run in Maya (closest-bone, ramps, masks, compositing ran offline). Test: `test_rom_metrics.py`, `test_skin_io.py`, `test_mx_skin_offline.py`. Makauskas's order [00:01:11], Jao's fill-then-mask. The plan below is an example for a biped whose joint names come from scenario-maya-rigging; keep one entry per region, bottom to top. ```python plan = [ {"name": "torso", "influences": ["pelvis", "spine_01", "spine_02", "spine_03"], "smooth": {"iterations": 3, "strength": 0.5}}, {"name": "L_shoulder", "influences": ["L_clavicle", "L_upperarm"], "smooth": {"iterations": 2}, "mask": {"along": ["L_clavicle", "L_upperarm", 0.3, 0.8]}}, {"name": "L_upperarm", "chain": ["L_upperarm", "L_upperarm_twist_01", "L_upperarm_twist_02", "L_lowerarm"], "mask": {"along": ["L_clavicle", "L_lowerarm", 0.45, 0.6]}}, {"name": "L_forearm", "chain": ["L_lowerarm", "L_lowerarm_twist_01", "L_lowerarm_twist_02", "L_hand"], "mask": {"along": ["L_upperarm", "L_hand", 0.45, 0.55]}}, {"name": "L_hand", "influences": ["L_hand", "L_thumb_01", "L_index_01", "L_middle_01"], # palm, then fingers "mask": {"along": ["L_lowerarm", "L_hand", 0.85, 1.0]}}, ] res = S.block_regions("body_geo", plan) # composites in memory, writes once, returns an audit print(res["layers"], S.weights_verdict(res["audit"])) ``` - `influences`: 100% to the closest bone segment (Assign from Closest Joint). `chain`: an even twist ramp (Jao). Mask ranges are the art direction; iterate on them, not on per-vertex values [added: parameters above are starting guesses, tune per character]. - Sparse regions (fingers): few smoothing iterations or restrict `smooth.verts` to the middle loops (Makauskas [00:18:35]); then `S.sharpen_weights`. - Must run at the bind pose (the function refuses otherwise). Mirror afterwards (P7) instead of listing the right side. ## P5. Skin Tools layered path (when the API exists) API names match the Maya 2027 Developer Help pages "Skin Tools Python API" (namespace `ngSkinTools2.api`, source in `<maya>/runTime/plug-ins/ngSkinTools/scripts/ngSkinTools2`), fetched 2026-09-24. Test: `test_skin_tools.py` (skips cleanly when no module is found). ```python probe = S.skin_tools_probe() # plug-ins, node types, commands, modules: read it first print(probe["api"], probe["node_types"]) if S.skin_tools_api(): S.st_init_layers(sc) S.st_add_region_layer("body_geo", "torso", ["pelvis", "spine_01", "spine_02", "spine_03"], smooth={"intensity": 0.5, "iterations": 3}) P = S.rest_points("body_geo") segs = S.bone_segments(S.read_weights(sc)["influences"]) # mask as an alpha, computed rather than painted names = S.read_weights(sc)["influences"]; ix = {S.short_name(n): i for i, n in enumerate(names)} mask = S.mask_along(P, segs[ix["L_clavicle"]][0], segs[ix["L_lowerarm"]][0], 0.35, 0.6) S.st_add_region_layer("body_geo", "L_arm", ["L_upperarm", "L_upperarm_twist_01", "L_lowerarm"], mask=mask, smooth={"intensity": 0.5, "iterations": 2}) arm = S.st_add_region_layer("body_geo", "L_arm_fix")["layer_object"] S.st_flood("body_geo", arm, "replace", "mask", 1.0, verts=arm_verts) # Jao: Replace-flood the mask S.st_flood("body_geo", arm, "smooth", None, 0.5, 3, verts=necklace_verts, volume=True) # across shells S.st_export_json("body_geo", "/abs/out/body_layers_v003.json") # keeps the layered work (Jao) print(S.delete_skin_layers(sc)) # before handoff: proves the weights survived, restores if not ``` Raw API, as in the 2027 Help: `from ngSkinTools2 import api`; `layers = api.init_layers(sc)`; `layer = layers.add("torso")`; `api.assign_from_closest_joint(sc, layer, influences=[logical indices])`; `s = api.PaintModeSettings(); s.mode = api.PaintMode.smooth; s.iterations = 3; api.flood_weights(target=layer, settings=s)` (acts on the component selection or the whole mesh); `layer.set_weights(api.NamedPaintTarget.MASK, values)`; `api.export_json(sc, file=path)`; `api.import_json(sc, file=path, vertex_transfer_mode=api.VertexTransferMode.vertexId)`. Without the API: P4 gives the same composite, written once; layers then only matter to a human painting in the GUI. ## P6. Smoothing, sharpening, multi-shell accessories Status: ran offline (pure Python); the write-back is not yet run in Maya. Tests: `test_mx_skin_offline.py`, `test_mirror_transfer.py` (sleeve). ```python d = S.read_weights(sc); W = d["weights"] topo = S.mesh_topology("body_geo") W = S.smooth_weights(W, topo["neighbors"], iterations=2, strength=0.5, verts=elbow_verts, locked=[ix["spine_03"]]) # locks: Jao's two-joint discipline W = S.sharpen_weights(W, topo["neighbors"], strength=0.5, verts=finger_mid_loops) # a necklace or shirt shell in the same mesh: copy the body under it (volume smoothing substitute) W = S.copy_nearest(W, topo["points"], body_verts, necklace_verts, k=3) S.write_weights(sc, W) # separate clothing mesh: remap body weights by closest point, then write to its own cluster Ws, stats = S.remap_by_closest(S.rest_points("body_geo"), W, S.rest_points("shirt_geo"), k=3) ``` Plain Maya smoothing on selected vertices: `cmds.skinCluster(sc, e=True, smoothWeights=0.5, smoothWeightsMaxIterations=2)` (Help; the menu version uses the Skin Tools algorithm in 2027 and errors on multi-layer clusters) [verify behavior of the flag in 2027]. ## P7. Mirror Status: not yet run in Maya (maps ran offline). Test: `test_mirror_transfer.py`. ```python st = S.mirror_skin("body_geo", axis=0, source="+") # +X (character left) onto -X print(st) # unmatched_vertices, unmatched_influences, mirror_error must be 0 / [] / < 0.01 # native alternative: labels plus explicit associations S.label_joints(bind_joints) cmds.copySkinWeights(sourceSkin=sc, destinationSkin=sc, mirrorMode="YZ", mirrorInverse=False, surfaceAssociation="closestPoint", influenceAssociation=["label", "closestJoint"]) ``` Needs a mesh centered on X and a symmetric bind pose (Help). Blocked while Skin Tools layers exist: use its Mirror tab, Interactive Mirror, or delete layers first. ## P8. Copy and transfer (reference head, proxy, updated mesh) Tests: `test_mirror_transfer.py` (copy_skin), `test_skin_io.py` (JSON closest-point import). ```python S.copy_skin("head_ref_geo", "head_geo") # binds if needed, explicit closestPoint + oneToOne/name/closestJoint S.copy_skin("proxy_lips_geo", "head_dup_geo", influence=("oneToOne", "closestJoint")) # Jao's proxy onto a duplicate S.copy_skin("hero_geo", "variant_geo", uv_space=("map1", "map1")) # different proportions # mesh updated by the modeler: carry weights over by closest rest point S.export_weights_json("body_geo", "/abs/out/body_w.json"); S.import_weights_json("/abs/out/body_w.json", "body_v2_geo", k=4) ``` Multi-cluster copies: the UI's Copy Skin Weights Skin Cluster source/target lists; by script pass the cluster names as `sourceSkin` and `destinationSkin`. ## P9. Prune and enforce the engine cap (never through maximumInfluences) Status: not yet run in Maya (maths ran offline). Test: `test_skin_io.py`. ```python rep = S.prune_and_limit("body_geo", max_influences=4, prune=0.01) print(rep["before"]["max_influences_found"], rep["after"]["max_influences_found"], rep["limited_vertices"]) ``` Then re-run P10: capping moves weight, and the centreline and face are where caps hurt most (Burton [00:51:55]). ## P10. Calisthenics ROM and the deformation test (headless) Status: not yet run in Maya (schedules and metrics ran offline). Test: `test_rom_metrics.py`. ```python rom = [("L_arm_fk_ctrl", "rz", 80, "L shoulder up"), ("L_arm_fk_ctrl", "rz", -60, "L shoulder down"), ("L_arm_fk_ctrl", "ry", 70, "L shoulder forward"), ("L_elbow_fk_ctrl", "ry", -140, "L elbow bend"), ("L_wrist_fk_ctrl", "rx", 90, "L forearm twist"), ("L_wrist_fk_ctrl", "rx", -90, "L forearm twist back"), ("L_leg_ik_ctrl", "ty", 45, "L foot lift"), ("spine_02_ctrl", "rx", 35, "spine bend")] # values and axes depend on the rig's controls [added examples]; one motion per segment (Makauskas) sched = S.rom_schedule(rom, start=1, step=10) # pose every 20 frames, rest in between keyed = S.key_rom(sched, relative=True) sections = [{"label": "L forearm mid", "a": "L_lowerarm", "b": "L_hand", "t": 0.5, "influences": ["L_lowerarm", "L_lowerarm_twist_01", "L_lowerarm_twist_02", "L_hand"]}, {"label": "L upper arm mid", "a": "L_upperarm", "b": "L_lowerarm", "t": 0.5}] rep = S.deformation_test("body_geo", keyed["poses"], rest_frame=1, sections=sections, rigid={"L_sole": sole_verts}, accessories=[{"mesh": "shirt_geo", "body": "body_geo"}]) for line in rep["verdict"]: print(line) ``` ML or rig-wide pose data (Widup): `S.rom_from_limits(S.joint_limits(bind_joints), step_deg=30)`. Visual review of the same poses: ```python snap = S.pose_snapshots("body_geo", [p["frame"] for p in keyed["poses"]]) # posed duplicates side by side import mx_review mx_review.review([snap["group"]], "/abs/out/rom_review", views=("front", "threequarter"), modes=("clay", "wire")) cmds.delete(snap["group"]) # GUI session through the bridge: a real playblast of the ROM # Bridge().call("mx_review", "playblast", path="/abs/out/rom", start=1, end=sched["end"], display={"wireframeOnShaded": True}) ``` Or the headless one-liner: `python3 mx_run.py --scene rig.ma mx_skin.py -- test --mesh body_geo --spec rom.json --json out.json` with `rom.json = {"rom": [[node, attr, value, label], ...], "sections": [...], "relative": true}`. ## P11. Correctives: pre, post, pose reader, solve from a sculpt Status: not yet run in Maya (inversion maths and the reader model ran offline). Test: `test_correctives.py`. Decide before sculpting (Maya 2027 Help, pre- and post-skinning correctives): | Question | Choose | Why | | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Is the rest mesh itself wrong where the joint bends, or must the fix hold in neighboring poses? | pre-deformation (`frontOfChain=True`), solved from a posed sculpt (`S.solve_pre_corrective`) | "like fixing the original character's mesh": the skin deforms the delta, so it "looks correct in other poses as well" | | Only this posed result is wrong? | post-deformation | authored "in consideration of how the skin deforms"; what you sculpt at the pose is what you get | | Post, and one joint owns the fix (elbow crease, knee cap)? | Transform Space target, Transform Name = that joint | "generally faster to compute", corrections stay "within the envelope area of the selected joint" | | Post, and the fix spans joints or must follow the surface (armpit, shoulder into chest)? | Tangent Space target | uses the local vertex frame, not UVs [cross-joint reading added] | Controller-driven joints (bind joints moved by IK/FK constraints, pairBlends or scenario-maya-rigging's offsetParentMatrix blend): `S.joint_drive(joint)` returns `free`, `channels` or `opm`. Pose them through their controls, never by `setAttr` on the joint (connected channels refuse it, constrained ones snap back). Node readers read `dagLocalMatrix` (local TRS times OPM), which `S.corrective_driver` does by default: an OPM-driven joint keeps rotate at 0, so a reader on `.matrix` or `rotate` is stuck on or off [added; offline model in `test_mx_skin_offline.py`, Maya check in `test_correctives.py`]. Pose Editor order for such joints (Help, "Use controller-driven joints with pose space deformations"): Create Pose Interpolator, answer No to neutral poses, right-click > Show Driver Settings, add the controller's driving attributes (Rotate X/Y/Z) to Controllers, then Poses > Add Neutral Poses, then add poses as usual; Euler Twist on when a wrist should twist about the forearm axis. Delete neutral poses made too early and redo them after the controllers. ```python pre = cmds.blendShape("body_geo", frontOfChain=True, name="body_preFix_BS")[0] # fixes the mesh, holds in all poses post = cmds.blendShape("body_geo", after=True, name="body_postFix_BS")[0] # fixes one posed result # check the order: nearest-to-output first => [post, skinCluster, pre] [h for h in cmds.listHistory("body_geoShape", pruneDagObjects=True) if cmds.nodeType(h) in ("blendShape", "skinCluster")] # pose the joint (through its control when S.joint_drive says it is driven), duplicate the posed mesh, # sculpt the duplicate (GUI) or move its points by script, then: print(S.joint_drive("L_lowerarm")) # {'kind': 'free' | 'channels' | 'opm', ...} cmds.setAttr("L_lowerarm_fk_ctrl.rz", -120) # driven: pose the control (name from scenario-maya-rigging) # cmds.setAttr("L_lowerarm.ry", -120) # free joint only sol = S.solve_pre_corrective("body_geo", "L_elbow120_sculpt", bs=pre, target_name="L_elbow120") print(sol["residual"], sol["singular"]) # residual < 1e-3 cm drv = S.corrective_driver("L_lowerarm", "%s.weight[%d]" % (pre, sol["index"]), falloff=45.0, rest_time=1) print(drv.get("warning"), drv["plug"], drv["drive"]["kind"]) # 0 at rest; reads dagLocalMatrix ``` - The node reader is swing only; for twist-dependent correctives or many poses per joint use the Pose Editor (GUI, `gui-paths.md`), extremes first, Independent for ad hoc poses, Regularization from 0.01 (Help). - Tangent space post targets by script: `cmds.blendShape(post, e=True, tangentSpace=True, target=("body_geo", 0, "fix_geo", 1.0))` [verify flag], tested soft. Transform Space has no scripted flag in the saved docs: create it in the Shape Editor (Add Target option box > Type Transform Space > Transform Name) until the probe finds one. - Built-in inversion: `cmds.invertShape("body_geo", "sculpt_geo")` [verify], tested soft. - Candy wrapper alternative to correctives: `cmds.setAttr(sc + ".skinningMethod", 2)` (weight blended) and paint DQ blend weights where twist collapses (Help); P10 sections measure it. ## P12. Paint-free skinning: Delta Mush, then bake to plain weights Test: `test_rom_metrics.py` (deltaMush and bakeDeformer, soft). ```python dm = cmds.deltaMush("body_geo", smoothingIterations=10, smoothingStep=0.5)[0] # [verify flags] # bake the smoothed result into a skinCluster on a duplicate bound to the same skeleton cmds.bakeDeformer(srcSkeletonName=root, srcMeshName="body_geo", dstSkeletonName=root, dstMeshName="body_baked_geo", maxInfluences=4) # [verify flags; 2025 added ROM options] ``` Then P3 audit and P10 on the baked mesh. Remove the Delta Mush before ML training if it must not be learned (Widup [00:07:55]). ## P13. Two skinClusters on a face (squash under tweaks) and the double transform fix Test: `test_multi_skin.py`. ```python sq = S.bind("face_geo", ["squash_top_jnt", "squash_bot_jnt"], method="closest", name="squash_SC")["skinCluster"] tw = S.bind("face_geo", tweak_joints, method="closest", multi=True, name="tweak_SC")["skinCluster"] cmds.copySkinWeights(sourceSkin="ref_tweak_SC", destinationSkin=tw, noMirror=True, # cluster to cluster, surfaceAssociation="closestPoint", influenceAssociation=["name", "closestJoint"]) # heads overlapping pin = cmds.createNode("uvPin", name="face_uvPin") cmds.connectAttr(sq + ".outputGeometry[0]", pin + ".deformedGeometry") # pins read the squash stage only names, logical = S.influences(tw) for n, li in zip(names, logical): # each tweak joint at its group origin grp = cmds.listRelatives(n, parent=True, fullPath=True)[0] cmds.connectAttr(grp + ".worldInverseMatrix[0]", "%s.bindPreMatrix[%d]" % (tw, li), force=True) ``` Gate: with all tweak controls at rest, squash moves tweak regions exactly once (the test measures d, not 2d); no cycle warning. `S.skin_clusters("face_geo")` lists the newest cluster first [verify]. ## P14. Lip split generator (antCGi) as arithmetic Status: not yet run in Maya (split maths ran offline). Test: `test_correctives.py` (lips part). ```python base = S.mesh_points("head_geo", world=False) master = S.mesh_points("lips_open_sculpt", world=False) lip = set(lip_verts) # vertices the master moves u = [abs(p[0]) for p in base] # distance from the center line (cm) hw = S.hat_weights(u, [0.0, mouth_half_width * 0.5, mouth_half_width]) # middle, outer, corner [added layout] left, right = S.side_weights(base, axis=0, width=0.3) upper = [1.0 if p[1] >= lip_seam_y else 0.0 for p in base] # or a soft band at the seam sections, names = [], [] for k, part in enumerate(("middle", "outer", "corner")): for side, sw in (("L", left), ("R", right)): for lvl, lw in (("upper", upper), ("lower", [1 - x for x in upper])): sections.append([hw[v][k] * sw[v] * lw[v] if v in lip else 0.0 for v in range(len(base))]) names.append("%s_lip_%s_%s" % (side, lvl, part)) targets = S.split_targets(base, master, sections) assert S.split_error(base, master, targets) < 1e-5 # sections at 1 == master (antCGi's sum test) bs = cmds.blendShape("head_geo", frontOfChain=True, name="head_BS")[0] S.add_split_targets(bs, "head_geo", targets, names) ``` Keep the master target in the node, disabled (antCGi: never delete it) so a master edit regenerates the sections. Corner vertices belong to corner shapes only (exclude them from the lip master). ## P15. ML Deformer: data and evaluation around the GUI training Status: not yet run in Maya; creation and training are GUI (Attribute Editor, Control Collector, Export Training Data, Train). Tests: `test_00_probe_deformation.py` (command and node probe), `test_rom_metrics.py` (rom_from_limits, joint_limits). ```python # 1. remove smoothing you do not want learned (Widup): cmds.delete(delta_mush_node) # 2. pose data: one joint at a time, ~30 degree steps inside the joint limits sched = S.rom_from_limits(S.joint_limits(bind_joints), step_deg=30.0, start=1, step=2) S.key_rom(sched) # then Deform > ML Deformer, Control Collector = skin joints # 3. after training (GUI), evaluate held-out poses against the target, and cross talk for f in held_out_frames: S.set_time(f) print(f, S.mesh_error(S.mesh_points("body_ml_geo"), S.mesh_points("body_target_geo"))) print(S.eval_seconds("body_ml_geo", range(1, 101))) # compare with the ML Deformer disabled ``` Cross talk on the ML result: `S.pose_metrics(rest, posed, topo, W, names, moved=[...])["cross_talk"]` with W from the skinCluster: vertices with no weight on the moved joints must not move. ## P16. Game PSD with secondary joints (architecture only, no code shipped) Rudy's structure: a swing-twist driver per joint, pose fractions remapped between calibrated values to 0..1, and per secondary joint a blend of per-pose transforms, every secondary joint mapped to its LODs. In Maya nodes the first version is `decomposeMatrix` or quaternion nodes for swing-twist, `remapValue` for fractions and `blendMatrix` into the secondary joint's offsetParentMatrix [verify node attributes]; at scale author it in Bifrost from code (Rudy [00:32:44]). Hand the joint list and LOD table to scenario-maya-pipeline-scripting for export. No tested snippet yet: build and test it with scenario-maya-rigging before shipping code. -
sources.md 18.8 KB
# Sources behind scenario-maya-deformation Load when a claim needs its origin, when choosing which source to re-watch, or when checking how current a technique is. Notes per source live in `notes/deformation/` (and the cited rigging and modeling notes); none of the videos were watched by this skill's author beyond their transcripts and notes. ## Videos | Source | Expert and credential | Best for | Best timestamps | Currency | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | [ngSkinTools2: skinning a character body](https://www.youtube.com/watch?v=xAi6cSyAV9M) (ngSkinTools channel, 2022, 26 min) | Viktoras Makauskas, author of ngSkinTools (acquired by Autodesk, shipped as Skin Tools in 2027) | Full-body region layer order, fill then mask, calisthenics, volume smoothing for shells, raw weights as a rig test, sparse-geometry smoothing | [00:01:11] regions, [00:03:01] calisthenics, [00:07:57] fill then mask, [00:08:27] raw weights, [00:08:46] mask is alpha, [00:18:35] fingers, [00:21:29] necklace, [00:23:31] feet | ngSkinTools2 plug-in UI; concepts map 1:1 to 2027 Skin Tools | | [Skinning region layers and proxy mesh transfer](https://www.youtube.com/watch?v=XOmRKZnyIqQ) (2026, 41 min) | Mitchell Jao, character animator (Blue Sky, Blizzard Cinematics, Walt Disney Animation), AnimSchool instructor | Why normalization makes vanilla painting hard, one layer per decision, even twist plus art-directed mask, proxy weights by vertex ID | [00:05:08] lock discipline, [00:16:06] clavicle layer, [00:22:19] twist vs mask, [00:31:18] result is a plain cluster, [00:36:26] proxy import | ngSkinTools2 plug-in; captions machine-translated, quotes approximate | | [Skin Tools quick start, Maya 2027](https://www.youtube.com/watch?v=gJSkEyXkyHE) (Maya Learning Channel, 2026, 4 min) | Autodesk (presenter not named) | Exact 2027 Skin Tools menu and tab names | [00:00:29] initialize, [00:01:32] projection and brushes, [00:02:02] mirror, [00:02:40] what to look for | 2027 UI | | [Facial rigging with multiple skinClusters and UV Pin](https://www.youtube.com/watch?v=yR81lKEbmDg) (Maya Learning Channel, 2023, 10 min) | Matt Chan, Autodesk | Two-cluster face plumbing, the cycle escape, the bindPreMatrix double-transform fix | [00:04:03] cluster-to-cluster copy, [00:07:01] outputGeometry, [00:08:57] bindPreMatrix | Maya 2024 feature, current | | [Face rig with blend shapes and a lip generator](https://www.youtube.com/watch?v=RbQM82Ta6zE) (antCGi, 2020, 75 min) | Antony Ward, games veteran (Atari, EA), author of _Game Character Development with Maya_ | Shape Editor face, additivity and vertex ownership, the non-destructive split generator | [00:01:38] joints for eyes, [00:08:58] additive, [00:20:32] generator, [00:31:59] sum test, [00:52:53] raise not smile, [01:07:58] engine shape limits | Shape Editor workflow unchanged; 2027 adds Add Combination Target | | [Base mesh deformation testing](https://www.youtube.com/watch?v=x07USYlvu2o) (antCGi, 2022, 24 min) | Antony Ward | Modeler's acceptance test, topology vs corrective attribution | [00:10:27] elbow, [00:11:04] wrist loops, [00:12:41] Non-Deformer History, [00:16:24] same weights two topologies, [00:21:10] ROM file | Maya 2023 era; classic painter (no layers) | | [Facial rigging techniques](https://www.youtube.com/watch?v=o_-rxt584GE) (Faceware webinar, 2012, 80 min) | Josh Burton, facial rigger at Midway (Mortal Kombat vs DC Universe), Morpheus rig, CG Monks | Game face judgment: budget first, parted lips, pivots, rest-pose SDK transfer, influence caps, asset checks | [00:13:35] joints for games, [00:35:45] budget, [00:38:46] rest pose, [00:48:33] eye pivot, [00:51:55] influence cap, [01:09:51] parted lips | Tools dated (Python 2, UDK); judgment current | | [Speed up animation workflows with the ML Deformer](https://www.youtube.com/watch?v=1_PoTPm2L1c) (Autodesk M&E, 2024, 45 min) | Todd Widup, film rigging supervisor (LOTR, Star Wars Ep. III, GotG Vol. 3) | Layered film rig, ML Deformer setup, pose coverage rule, settings that worked and failed | [00:02:17] cage and wraps, [00:07:55] delete Delta Mush, [00:10:41] joints as controls, [00:15:50] 30 degree steps, [00:27:41] cross talk, [00:42:10] final settings | Maya 2025.2; 2026 added training options | | [Character deformation and rigging using Bifrost for EA SPORTS](https://www.youtube.com/watch?v=eFH53W7kPZs) (Autodesk GDC webinar, 2026, 54 min) | Darren Rudy, principal technical artist, EA (FC, UFC, NHL, Madden) | Game PSD with secondary joints, swing-twist drivers, pose fractions, ratio placement, LOD planning | [00:14:18] PSD by joints, [00:17:00] swing-twist, [00:18:38] pose normalization, [00:21:20] always a percentage, [00:38:07] 4 to 60 fps, [00:38:41] LOD | Older Bifrost; 2027 ships Bifrost 3.0 | | [Maya joints explained, IK and skinning](https://www.youtube.com/watch?v=rfLEBgOEW1A) (Cult of Rig, 2018) | Raffaele Fragapane, performance technology supervisor | What a skinCluster consumes (matrices), bind pose as data | [01:22:26] [01:35:18] | Concepts timeless | | [Skeleton building against the Unreal mannequin](https://www.youtube.com/watch?v=fGacyVzJGIU), [Model evaluation before rigging](https://www.youtube.com/watch?v=e74KphYwMww) (antCGi, 2021) | Antony Ward | Twist joint placement, model gate for skinning | [00:20:47] (twist), e74KphYwMww whole | UE4 mannequin era | ## Documentation | Source | Best for | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Maya 2027 Help, skinning and Skin Tools, 22 pages saved in `sources/docs/deformation__skinning-skin-tools-2027.md` (entry: [Skin Tools overview](https://help.autodesk.com/cloudhelp/2027/ENU/Maya-CharacterAnimation/files/GUID-15263643-6A32-43E2-86A9-361D339240D8.htm); [cmds.skinCluster](https://help.autodesk.com/cloudhelp/2027/ENU/Maya-Tech-Docs/CommandsPython/skinCluster.html); [cmds.copySkinWeights](https://help.autodesk.com/cloudhelp/2027/ENU/Maya-Tech-Docs/CommandsPython/copySkinWeights.html)) | Bind method behavior and prerequisites, skinning methods, bind pose, normalization, copy and mirror rules, weight maps, multiple clusters, Skin Tools layers, masks, mirror modes, blocked classic tools, Delete Skin Layers, flags and defaults | | Maya 2027 Help, blend shapes, correctives, PSD, deformers, ML Deformer, 21 pages saved in `sources/docs/deformation__blendshapes-psd-deformers.md` (entry: [Shape Editor](https://help.autodesk.com/cloudhelp/2027/ENU/Maya-CharacterAnimation/files/GUID-3C92402C-B24E-4874-AC8D-EADF976A19DC.htm); [Pose Editor](https://help.autodesk.com/cloudhelp/2027/ENU/Maya-CharacterAnimation/files/GUID-2AB6C4C3-75AC-4094-A65C-C232739AFB30.htm); [Create an ML Deformer](https://help.autodesk.com/cloudhelp/2027/ENU/Maya-CharacterAnimation/files/GUID-13051D83-FF77-4A8C-BB4B-D6CA4D9CC82C.htm)) | Pre vs post correctives, tangent vs transform space, controller-driven joints in the Pose Editor, Pose Editor settings with numbers, mirroring rules, Delta Mush, Tension, Proximity Wrap, Morph, ML Deformer order and training steps | | Maya 2027 Developer Help, [Skin Tools Python API](https://help.autodesk.com/cloudhelp/2027/ENU/Maya-DEVHELP/files/Maya-Python-API/Maya_DEVHELP_Maya_Python_API_ngSkinTools2_html.html) and its four pages (Target Info, Layers, Import Export and Transfer, Weight Tools under `.../Maya-Python-API/ngSkinTools2/`), fetched 2026-09-24 (not saved in `sources/docs/` before) | The `ngSkinTools2.api` namespace, `init_layers`, `Layers`, `Layer.set_weights`, `NamedPaintTarget.MASK`, `assign_from_closest_joint`, `flood_weights`, `PaintModeSettings` (incl. `use_volume_neighbours`, `influences_limit`), `export_json` and `import_json` with `VertexTransferMode`, `InfluenceMappingConfig` defaults | | [ngSkinTools2 API docs](https://www.ngskintools.com/documentation/v2/api/) (layers, set_weights, transfer, target_info), fetched 2026-09-24 | The pre-Autodesk version of the same API, including the layer example (mask empty by default, setting influence 1 overrides influence 0) | | `sources/maya-version-deltas.md` section 2.3 (and sections 3, 4) | 2027 traps: layers block classic tools, Smooth Skin Weights algorithm, `skinCluster -multi`, `performSkinCluster.mel` removed, copySkinWeights defaults, `mlDeformer` command (2026), `dgaTension` (2026.3) | | scenario-maya-expert toolkit docstrings (`<skills>/scenario-maya-expert/scripts/`) | `mx_run` (headless jobs), `mx_bridge` (GUI), `mx_audit.resolve_mesh` and `verdict`, `mx_validate` profiles, `mx_review.review` and `playblast` | ## Tests and measurements made for this skill | File | What it proves | | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tests/code/maya-deformation/test_mx_skin_offline.py` | The pure-Python half of `mx_skin` (ran 2026-09-24, python3, all pass): topology, ray parity, volumes, blocking, ramps, masks, compositing, smoothing, mirror, remap, ROM schedules, metrics with known answers (LBS vs DQ candy wrapper, rigid vs smoothed elbow), LBS inversion, split sums | | `archive/tests/maya-deformation/offline-run4/timing_22k.txt` | Pure-Python cost at 22k vertices on python3.14 on this Mac (see the file; mayapy may be slower) | | `tests/code/maya-deformation/test_*.py` (Maya) | Not yet run: probe, skin IO, ROM metrics, mirror and transfer, Skin Tools, correctives, multi-skin | ## Revision history - 2026-09-24, v2: refactor after the M3 blind grade (`tests/grading/M3_grade.md`; v1 kept in `skills/_versions/maya-deformation v1 2026-09-24/`). Taught from `deformation__blendshapes-psd-deformers`: PSD on controller-driven joints (no neutral poses at creation, controller attributes in Driver Settings, then Add Neutral Poses; Euler Twist for wrists; pose through the controls) and the pre vs post, Transform vs Tangent Space choice (stance, workflow stage 7, mistakes, P11 decision table, critique line 5). `corrective_driver` now reads `dagLocalMatrix`, so bind joints driven through offsetParentMatrix fire their correctives; new `joint_drive` and `reader_weight`; offline test 60 of 60 checks passing; Maya tests extended, not yet run.
-
-
scripts
-
mx_skin.py 112.3 KB
""" mx_skin: skinning, weight maths and deformation tests for the scenario-maya-deformation skill (Maya 2027). Import: import sys; sys.path.insert(0, "<skills>/scenario-maya-deformation/scripts"); import mx_skin as S (it adds ../../scenario-maya-expert/scripts to sys.path for mx_audit.resolve_mesh) Data conventions W list, one dict per vertex {influence index: weight}; the index is the PHYSICAL index in the skinCluster's influence list (MFnSkinCluster.influenceObjects order), not the logical .matrix[] index. read_weights() returns both lists. points list of (x, y, z) in centimeters (OpenMaya internal unit), world space unless stated. matrix 16 floats, row-major, Maya row-vector convention: p' = p * M, translation in [12:15]. Headless recipes (through mx_run): python3 <skills>/scenario-maya-expert/scripts/mx_run.py --scene rig.ma <skills>/scenario-maya-deformation/scripts/mx_skin.py \ -- audit --mesh body_geo --max-influences 4 --json /abs/out/weights_audit.json ... mx_skin.py -- export --mesh body_geo --path /abs/out/body_weights.json ... mx_skin.py -- test --mesh body_geo --spec /abs/rom_spec.json --json /abs/out/rom_report.json ... mx_skin.py -- probe (Skin Tools module, plug-in and node names in this Maya) Main entry points weights read_weights, write_weights, audit_weights, limit_influences, prune_rows, export/import_weights_json authoring bind, bind_precheck, block_regions, closest_bone_block, chain_ramp, mask_along, composite_layers, smooth_weights, sharpen_weights, volume_neighbors, copy_nearest, mirror_skin, copy_skin skin tools skin_tools_probe, skin_tools_api, skin_layers_node, st_add_region_layer, st_flood, delete_skin_layers testing rom_schedule, rom_from_limits, key_rom, deformation_test, pose_metrics, deformation_verdict, cross_section, bone_volume, cross_talk, pose_snapshots, penetration shapes corrective_driver, reader_weight, joint_drive, solve_pre_corrective, split_targets, add_split_targets, invert_deltas Revision 2026-09-24 (refactor after the M3 blind grade): corrective_driver reads dagLocalMatrix so constraint-driven and offsetParentMatrix-driven bind joints both fire their correctives; joint_drive reports what moves a joint (pose it through its controls; Pose Editor controller-driven workflow); reader_weight models the reader offline. """ __version__ = "0.1" # Maya Expert Skills v0.1 (2026-09-24) import json import math import os import re import sys import time HERE = os.path.dirname(os.path.abspath(__file__)) _MX_EXPERT = os.path.normpath(os.path.join(HERE, "..", "..", "scenario-maya-expert", "scripts")) if os.path.isdir(_MX_EXPERT) and _MX_EXPERT not in sys.path: sys.path.append(_MX_EXPERT) FORMAT = "mx_skin_weights" FORMAT_VERSION = 1 # Thresholds. Sources: see references/expert-notes.md. [added] = this toolkit's default, not an # expert number; override per project. THRESHOLDS = { "sum_tol": 1e-4, # per-vertex weight sum (normalization, Jao 00:02:25; Maya 2027 Help) "prune": 0.01, # [added]; the Help cites 0.008 as a typical insignificant weight "pinch": 0.7, # edge length ratio below = compression [added] "stretch": 1.3, # edge length ratio above = stretch [added] "collapse_area": 0.3, # face area ratio below = collapsed face [added] "fold_deg": 120.0, # dihedral angle in pose that counts as a fold [added] "fold_rest_max_deg": 60.0, # ...only where the rest dihedral was below this [added] "section_area": 0.7, # cross-section area ratio below = candy wrapper [added] "section_perimeter": 0.8, # cross-section perimeter ratio below [added] "volume": 0.10, # relative bone-volume change [added] (Makauskas judges volume by eye) "rigid": 0.05, # rigid regions (sole, palm): edge change [added] "mirror": 0.01, # weight difference between mirrored vertices [added] "cross_talk": 1e-3, # cm moved by vertices with zero weight on every moved joint [added] "penetration": 0.05, # cm inside the body surface [added] } # =============================================================================== vector and matrix def _sub(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) def _add(a, b): return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) def _mul(a, s): return (a[0] * s, a[1] * s, a[2] * s) def _dot(a, b): return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] def _cross(a, b): return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) def _len(a): return math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]) def _dist(a, b): return _len(_sub(a, b)) def _unit(a): n = _len(a) return (a[0] / n, a[1] / n, a[2] / n) if n > 1e-12 else (0.0, 0.0, 0.0) def _lerp(a, b, t): return (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t) def _clamp(x, lo=0.0, hi=1.0): return lo if x < lo else hi if x > hi else x def smoothstep(e0, e1, x): """0 below e0, 1 above e1, cubic ease in between (e0 > e1 inverts).""" if e1 == e0: return 0.0 if x < e0 else 1.0 t = _clamp((x - e0) / (e1 - e0)) return t * t * (3.0 - 2.0 * t) def _angle_deg(a, b): ua, ub = _unit(a), _unit(b) return math.degrees(math.acos(_clamp(_dot(ua, ub), -1.0, 1.0))) IDENTITY = (1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0) def mat_mul(a, b): """a * b for 16-float row-major matrices (apply a, then b, in Maya's row-vector convention).""" out = [0.0] * 16 for r in range(4): for c in range(4): out[4 * r + c] = (a[4 * r] * b[c] + a[4 * r + 1] * b[4 + c] + a[4 * r + 2] * b[8 + c] + a[4 * r + 3] * b[12 + c]) return tuple(out) def xform_point(p, m): x, y, z = p return (x * m[0] + y * m[4] + z * m[8] + m[12], x * m[1] + y * m[5] + z * m[9] + m[13], x * m[2] + y * m[6] + z * m[10] + m[14]) def xform_vector(v, m): x, y, z = v return (x * m[0] + y * m[4] + z * m[8], x * m[1] + y * m[5] + z * m[9], x * m[2] + y * m[6] + z * m[10]) def mat_inverse(m): """General 4x4 inverse (Gauss-Jordan with partial pivoting). Raises on a singular matrix.""" a = [list(m[4 * r:4 * r + 4]) + [1.0 if r == c else 0.0 for c in range(4)] for r in range(4)] for col in range(4): piv = max(range(col, 4), key=lambda r: abs(a[r][col])) if abs(a[piv][col]) < 1e-14: raise ValueError("singular matrix") a[col], a[piv] = a[piv], a[col] d = a[col][col] a[col] = [x / d for x in a[col]] for r in range(4): if r != col and a[r][col] != 0.0: f = a[r][col] a[r] = [x - f * y for x, y in zip(a[r], a[col])] return tuple(a[r][4 + c] for r in range(4) for c in range(4)) def mat_rotation(axis, degrees, pivot=(0.0, 0.0, 0.0)): """Rotation about `axis` through `pivot`, row-vector convention (p' = p * M).""" x, y, z = _unit(axis) t = math.radians(degrees) c, s, C = math.cos(t), math.sin(t), 1.0 - math.cos(t) # column-convention Rodrigues matrix R (v' = R v); row convention is its transpose R = [[c + x * x * C, x * y * C - z * s, x * z * C + y * s], [y * x * C + z * s, c + y * y * C, y * z * C - x * s], [z * x * C - y * s, z * y * C + x * s, c + z * z * C]] m = [R[0][0], R[1][0], R[2][0], 0.0, R[0][1], R[1][1], R[2][1], 0.0, R[0][2], R[1][2], R[2][2], 0.0, 0.0, 0.0, 0.0, 1.0] rp = xform_vector(pivot, m) m[12], m[13], m[14] = pivot[0] - rp[0], pivot[1] - rp[1], pivot[2] - rp[2] return tuple(m) def mat_translation(t): return (1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, t[0], t[1], t[2], 1.0) def _mat3_inverse(m): """Inverse of the upper 3x3 of a 16-float matrix, as 9 floats row-major; None if singular.""" a, b, c = m[0], m[1], m[2] d, e, f = m[4], m[5], m[6] g, h, i = m[8], m[9], m[10] det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g) if abs(det) < 1e-10: return None, det k = 1.0 / det return ((e * i - f * h) * k, (c * h - b * i) * k, (b * f - c * e) * k, (f * g - d * i) * k, (a * i - c * g) * k, (c * d - a * f) * k, (d * h - e * g) * k, (b * g - a * h) * k, (a * e - b * d) * k), det # =============================================================================== topology and grid def build_topology(points, faces): """Edges, edge-to-face and face-to-edge maps, fan triangles, vertex neighbours, shells. points: list of xyz; faces: list of vertex-index lists (Maya winding).""" edge_index, edges, edge_faces, face_edges, tris = {}, [], [], [], [] for fi, f in enumerate(faces): k = len(f) fe = [] for i in range(k): a, b = f[i], f[(i + 1) % k] key = (a, b) if a < b else (b, a) e = edge_index.get(key) if e is None: e = len(edges) edge_index[key] = e edges.append(key) edge_faces.append([]) edge_faces[e].append(fi) fe.append(e) face_edges.append(fe) for i in range(1, k - 1): tris.append((f[0], f[i], f[i + 1])) n = len(points) nb = [set() for _ in range(n)] for a, b in edges: nb[a].add(b) nb[b].add(a) parent = list(range(n)) def find(x): while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x for a, b in edges: ra, rb = find(a), find(b) if ra != rb: parent[ra] = rb roots, shells = {}, [] for v in range(n): r = find(v) shells.append(roots.setdefault(r, len(roots))) return {"points": [tuple(p) for p in points], "faces": [list(f) for f in faces], "edges": edges, "edge_faces": edge_faces, "face_edges": face_edges, "tris": tris, "neighbors": [sorted(s) for s in nb], "shells": shells, "shell_count": len(roots)} def face_vector_areas(points, faces): """Newell vector area per face (direction = normal by winding, length = area).""" out = [] for f in faces: nx = ny = nz = 0.0 k = len(f) for i in range(k): p, q = points[f[i]], points[f[(i + 1) % k]] nx += (p[1] - q[1]) * (p[2] + q[2]) ny += (p[2] - q[2]) * (p[0] + q[0]) nz += (p[0] - q[0]) * (p[1] + q[1]) out.append((0.5 * nx, 0.5 * ny, 0.5 * nz)) return out def bbox(points): xs, ys, zs = zip(*points) return (min(xs), min(ys), min(zs)), (max(xs), max(ys), max(zs)) class Grid(object): """Uniform hash grid for nearest and radius queries on a point subset.""" def __init__(self, points, ids=None, cell=None): self.points = points ids = list(range(len(points))) if ids is None else list(ids) if not ids: raise ValueError("Grid needs at least one point") if cell is None: lo, hi = bbox([points[i] for i in ids]) diag = _dist(lo, hi) or 1.0 cell = max(diag / max(1.0, len(ids) ** (1.0 / 3.0)), 1e-6) self.cell = float(cell) self.cells = {} for i in ids: self.cells.setdefault(self._key(points[i]), []).append(i) keys = list(self.cells) self.kmin = tuple(min(k[d] for k in keys) for d in range(3)) self.kmax = tuple(max(k[d] for k in keys) for d in range(3)) def _key(self, p): c = self.cell return (int(math.floor(p[0] / c)), int(math.floor(p[1] / c)), int(math.floor(p[2] / c))) def within(self, p, r): kx, ky, kz = self._key(p) n = int(math.ceil(r / self.cell)) r2 = r * r out = [] for dx in range(-n, n + 1): for dy in range(-n, n + 1): for dz in range(-n, n + 1): for i in self.cells.get((kx + dx, ky + dy, kz + dz), ()): q = self.points[i] d2 = (q[0] - p[0]) ** 2 + (q[1] - p[1]) ** 2 + (q[2] - p[2]) ** 2 if d2 <= r2: out.append(i) return out def nearest(self, p, k=1): """[(distance, id)] of the k nearest points.""" key = self._key(p) max_r = max(max(abs(key[d] - self.kmin[d]), abs(key[d] - self.kmax[d])) for d in range(3)) best = [] R = 0 while R <= max_r: for dx in range(-R, R + 1): for dy in range(-R, R + 1): for dz in range(-R, R + 1): if max(abs(dx), abs(dy), abs(dz)) != R: continue for i in self.cells.get((key[0] + dx, key[1] + dy, key[2] + dz), ()): q = self.points[i] best.append(((q[0] - p[0]) ** 2 + (q[1] - p[1]) ** 2 + (q[2] - p[2]) ** 2, i)) if len(best) >= k: best.sort() if best[k - 1][0] <= (R * self.cell) ** 2: break R += 1 best.sort() return [(math.sqrt(d2), i) for d2, i in best[:k]] # =============================================================================== weight rows def normalize_rows(W, eps=1e-12): out = [] for row in W: s = sum(w for w in row.values() if w > 0.0) out.append({i: w / s for i, w in row.items() if w > 0.0} if s > eps else {}) return out def prune_rows(W, threshold=THRESHOLDS["prune"]): """Drop weights below threshold (the largest weight of a vertex always stays), renormalize.""" out = [] for row in W: if not row: out.append({}) continue top = max(row, key=row.get) kept = {i: w for i, w in row.items() if w >= threshold or i == top} s = sum(kept.values()) out.append({i: w / s for i, w in kept.items()} if s > 0 else {}) return out def limit_influences(W, max_influences, prune=0.0): """Keep the `max_influences` largest weights per vertex (and drop those under `prune`), renormalize. This is the safe way to enforce an engine cap: editing skinCluster maximumInfluences in edit mode discards custom weights (Maya 2027 Help, skinCluster -mi). Returns (W, changed_vertex_count).""" out, changed = [], 0 for row in W: items = sorted(row.items(), key=lambda kv: -kv[1]) keep = [(i, w) for i, w in items[:max_influences] if w >= prune or i == (items[0][0] if items else None)] if len(keep) != len(items): changed += 1 s = sum(w for _, w in keep) out.append({i: w / s for i, w in keep} if s > 0 else {}) return out, changed def dominant(W): """Index of the largest weight per vertex (-1 for an empty row).""" return [max(r, key=r.get) if r else -1 for r in W] def rows_to_dense(W, k): flat = [0.0] * (len(W) * k) for v, row in enumerate(W): base = v * k for i, w in row.items(): flat[base + i] = w return flat def dense_to_rows(flat, k, eps=0.0): n = len(flat) // k if k else 0 return [{i: flat[v * k + i] for i in range(k) if flat[v * k + i] > eps} for v in range(n)] def max_row_difference(Wa, Wb): """Largest absolute per-influence difference between two weight lists (same vertex order).""" m, where = 0.0, -1 for v, (a, b) in enumerate(zip(Wa, Wb)): for i in set(a) | set(b): d = abs(a.get(i, 0.0) - b.get(i, 0.0)) if d > m: m, where = d, v return m, where def audit_weights(W, names=None, max_influences=None, tol=THRESHOLDS["sum_tol"], tiny=None, max_listed=20): """Objective weight checks: sums, influence counts, tiny and negative weights, unused influences.""" k = len(names) if names else (max((max(r) for r in W if r), default=-1) + 1) tiny = THRESHOLDS["prune"] if tiny is None else tiny hist, over, bad, zero, neg = {}, [], [], [], [] tiny_count, max_dev, totals = 0, 0.0, [0.0] * k for v, row in enumerate(W): nz = [w for w in row.values() if w > 0.0] hist[len(nz)] = hist.get(len(nz), 0) + 1 s = sum(row.values()) if not row or s <= 1e-12: zero.append(v) elif abs(s - 1.0) > tol: bad.append(v) max_dev = max(max_dev, abs(s - 1.0) if row else 1.0) if max_influences and len(nz) > max_influences: over.append(v) tiny_count += sum(1 for w in nz if w < tiny) if any(w < 0.0 for w in row.values()): neg.append(v) for i, w in row.items(): if 0 <= i < k: totals[i] += w unused = [i for i in range(k) if totals[i] <= 0.0] label = (lambda i: names[i]) if names else (lambda i: i) return {"vertices": len(W), "influences": k, "max_influences_found": max(hist) if hist else 0, "influence_count_histogram": {str(c): n for c, n in sorted(hist.items())}, "over_budget": len(over), "over_budget_sample": over[:max_listed], "budget": max_influences, "bad_sum": len(bad), "bad_sum_sample": bad[:max_listed], "max_sum_deviation": max_dev, "zero_rows": len(zero), "zero_rows_sample": zero[:max_listed], "negative": len(neg), "tiny_weights": tiny_count, "tiny_threshold": tiny, "unused_influences": [label(i) for i in unused]} def weights_verdict(a): """error / warn / info lines from audit_weights().""" out = [] if a["zero_rows"]: out.append("error: %d vertices have no weight at all (sample %s)" % (a["zero_rows"], a["zero_rows_sample"][:5])) if a["bad_sum"]: out.append("error: %d vertices do not sum to 1 (max deviation %.2g)" % (a["bad_sum"], a["max_sum_deviation"])) if a["negative"]: out.append("error: %d vertices carry negative weights" % a["negative"]) if a["budget"] and a["over_budget"]: out.append("error: %d vertices exceed %d influences (max found %d)" % ( a["over_budget"], a["budget"], a["max_influences_found"])) if a["tiny_weights"]: out.append("warn: %d weights below %.3g (prune before handoff)" % (a["tiny_weights"], a["tiny_threshold"])) if a["unused_influences"]: out.append("info: %d influences carry no weight: %s" % (len(a["unused_influences"]), a["unused_influences"][:10])) return out # =============================================================================== blocking and ramps def point_segment(p, a, b): """(distance, t) from p to segment a-b; t in 0..1 along it (b None: a point).""" if b is None: return _dist(p, a), 0.0 ab = _sub(b, a) l2 = _dot(ab, ab) t = 0.0 if l2 < 1e-18 else _clamp(_dot(_sub(p, a), ab) / l2) return _dist(p, _add(a, _mul(ab, t))), t def closest_bone_block(points, bones, verts=None): """'Assign from Closest Joint' in plain Python: each vertex gets 1.0 on the influence whose bone segment (joint to child joint; end joints are points) is nearest (Makauskas 00:01:41; Jao 00:10:39 on why segments map 'upwards' along an FK chain). bones: [(influence index, start, end or None)]. Rows outside `verts` are empty dicts.""" out = [dict() for _ in range(len(points))] vs = range(len(points)) if verts is None else verts for v in vs: p = points[v] best, bi = None, None for idx, a, b in bones: d = point_segment(p, a, b)[0] if best is None or d < best: best, bi = d, idx if bi is not None: out[v] = {bi: 1.0} return out def chain_ramp(points, chain, verts=None, ease=False): """Even twist distribution: partition of unity along a polyline of joints (1.0 at each joint's position, linear or smoothstep between neighbours, clamped at the ends). chain: [(influence index, position)] ordered along the limb. Jao weights twist joints evenly and keeps the shape in the mask (XOmRKZnyIqQ 00:20:36, 00:22:19); the functional form is [added].""" if len(chain) < 2: raise ValueError("chain_ramp needs at least two joints") s = [0.0] for k in range(1, len(chain)): s.append(s[-1] + _dist(chain[k - 1][1], chain[k][1])) out = [dict() for _ in range(len(points))] vs = range(len(points)) if verts is None else verts for v in vs: p = points[v] best = None for k in range(len(chain) - 1): d, t = point_segment(p, chain[k][1], chain[k + 1][1]) if best is None or d < best[0]: best = (d, k, t) _, k, u = best if k == 0 and u <= 0.0: out[v] = {chain[0][0]: 1.0} continue if k == len(chain) - 2 and u >= 1.0: out[v] = {chain[-1][0]: 1.0} continue if ease: u = smoothstep(0.0, 1.0, u) row = {} if 1.0 - u > 0.0: row[chain[k][0]] = row.get(chain[k][0], 0.0) + (1.0 - u) if u > 0.0: row[chain[k + 1][0]] = row.get(chain[k + 1][0], 0.0) + u out[v] = row return out # =============================================================================== masks and layers def mask_along(points, a, b, t0, t1, verts=None): """Layer mask from the parameter along a -> b: 0 below t0, 1 above t1 (smoothstep). Example: shoulder layer mask = mask_along(P, clavicle, upperarm, 0.3, 0.7) [added]. Values outside `verts` are 0.""" ab = _sub(b, a) l2 = _dot(ab, ab) or 1e-18 out = [0.0] * len(points) vs = range(len(points)) if verts is None else verts for v in vs: out[v] = smoothstep(t0, t1, _dot(_sub(points[v], a), ab) / l2) return out def mask_sphere(points, center, r_in, r_out): """1 inside r_in, 0 beyond r_out, smoothstep between.""" return [1.0 - smoothstep(r_in, r_out, _dist(p, center)) for p in points] def mask_from_verts(n, verts, value=1.0): out = [0.0] * n for v in verts: out[v] = value return out def smooth_scalar(values, neighbors, iterations=1, strength=0.5, verts=None): """Laplacian smoothing of a per-vertex scalar (mask polishing, Jao and Makauskas polish masks mostly with Smooth).""" cur = list(values) vs = range(len(cur)) if verts is None else list(verts) for _ in range(iterations): new = list(cur) for v in vs: nb = neighbors[v] if nb: new[v] = (1.0 - strength) * cur[v] + strength * sum(cur[u] for u in nb) / len(nb) cur = new return cur def composite_layers(n, layers): """Skin Tools style layer stack evaluated in memory, bottom to top: W = a * W_layer + (1 - a) * W_below with a = mask * opacity; the mask is an alpha, not an influence (Makauskas 00:08:46; Maya 2027 Help, layer masks). Empty rows are transparent. layers: [{"weights": W, "mask": list or None, "opacity": 1.0, "enabled": True, "name": ...}]. This mirrors the documented behaviour; it is not Autodesk's exact algorithm [added].""" acc = [dict() for _ in range(n)] for L in layers: if not L.get("enabled", True): continue Wl, mask, op = L["weights"], L.get("mask"), float(L.get("opacity", 1.0)) for v in range(n): row = Wl[v] if not row: continue a = op * (mask[v] if mask is not None else 1.0) if a <= 0.0: continue s = sum(row.values()) if s <= 0.0: continue below = acc[v] if not below or a >= 1.0: # opaque, or nothing underneath to show through acc[v] = {i: w / s for i, w in row.items()} continue new = {i: (1.0 - a) * w for i, w in below.items()} for i, w in row.items(): new[i] = new.get(i, 0.0) + a * w / s acc[v] = new return normalize_rows(acc) def blend_weight_maps(Wa, Wb, t): """(1 - t) Wa + t Wb, normalized: Rudy's blendable skin weights between two body types (eFH53W7kPZs 00:11:28).""" out = [] for a, b in zip(Wa, Wb): row = {i: (1.0 - t) * w for i, w in a.items()} for i, w in b.items(): row[i] = row.get(i, 0.0) + t * w out.append(row) return normalize_rows(out) # =============================================================================== smoothing def _renorm_with_locks(row, orig, locked): lock_sum = sum(orig.get(i, 0.0) for i in locked) free = {i: w for i, w in row.items() if i not in locked and w > 0.0} fs = sum(free.values()) target = max(0.0, 1.0 - lock_sum) if fs <= 0.0: free = {i: w for i, w in orig.items() if i not in locked} fs = sum(free.values()) out = {i: orig[i] for i in locked if orig.get(i, 0.0) > 0.0} if fs > 0.0: for i, w in free.items(): out[i] = w * target / fs return out def smooth_weights(W, neighbors, iterations=1, strength=0.5, verts=None, locked=(), max_influences=None): """Average each vertex's weights with its neighbours (Jacobi iterations), renormalize. locked: influence indices whose weights never change (Jao's vanilla discipline, 00:05:08). Makauskas: strong smoothing on dense regions, sparing on sparse ones (fingers lose volume, 00:18:35); restrict with `verts` and use fewer iterations there.""" locked = set(locked) cur = [dict(r) for r in W] vs = range(len(cur)) if verts is None else sorted(set(verts)) for _ in range(max(0, int(iterations))): new = list(cur) for v in vs: nb = neighbors[v] if not nb: continue avg = {} for u in nb: for i, w in cur[u].items(): avg[i] = avg.get(i, 0.0) + w inv = 1.0 / len(nb) row = cur[v] mixed = {} for i in set(row) | set(avg): x = (1.0 - strength) * row.get(i, 0.0) + strength * avg.get(i, 0.0) * inv if x > 1e-9: mixed[i] = x if locked: mixed = _renorm_with_locks(mixed, row, locked) else: s = sum(mixed.values()) mixed = {i: w / s for i, w in mixed.items()} if s > 0 else dict(row) if max_influences: mixed = limit_influences([mixed], max_influences)[0][0] new[v] = mixed cur = new return cur def sharpen_weights(W, neighbors, strength=0.5, iterations=1, verts=None): """Inverse of smoothing (w + s * (w - average)), clamped and renormalized: Skin Tools' Sharpen, 'an opposite of smooth brush' (Makauskas 00:18:51).""" cur = [dict(r) for r in W] vs = range(len(cur)) if verts is None else sorted(set(verts)) for _ in range(max(0, int(iterations))): new = list(cur) for v in vs: nb = neighbors[v] if not nb: continue avg = {} for u in nb: for i, w in cur[u].items(): avg[i] = avg.get(i, 0.0) + w / len(nb) row = cur[v] out = {} for i in set(row) | set(avg): x = row.get(i, 0.0) + strength * (row.get(i, 0.0) - avg.get(i, 0.0)) if x > 1e-9: out[i] = x s = sum(out.values()) new[v] = {i: w / s for i, w in out.items()} if s > 0 else dict(row) cur = new return cur def volume_neighbors(points, radius, verts=None, candidates=None): """Neighbour lists by 3D proximity (across shells): the 'Volume' brush projection that lets a necklace or shirt agree with the body (Makauskas 00:05:33; Maya 2027 Help, brush projection). Pass the result to smooth_weights(neighbors=...). candidates: vertices allowed as neighbours.""" grid = Grid(points, ids=candidates, cell=radius) vs = range(len(points)) if verts is None else verts out = [[] for _ in range(len(points))] for v in vs: out[v] = [u for u in grid.within(points[v], radius) if u != v] return out def copy_nearest(W, points, src_verts, dst_verts, k=1): """Rows of dst_verts copied from the nearest src_verts (inverse-distance blend for k > 1): an accessory or clothing shell follows the body under it (Makauskas volume smoothing on the necklace, 00:21:29, as a deterministic transfer [added]).""" grid = Grid(points, ids=src_verts) out = [dict(r) for r in W] for v in dst_verts: hits = grid.nearest(points[v], k) if k == 1 or hits[0][0] < 1e-9: out[v] = dict(W[hits[0][1]]) continue row = {} for d, u in hits: wt = 1.0 / d for i, w in W[u].items(): row[i] = row.get(i, 0.0) + wt * w s = sum(row.values()) out[v] = {i: w / s for i, w in row.items()} return out # =============================================================================== mirror and remap SIDE_RULES = ( ("prefix", "L_", "R_"), ("prefix", "l_", "r_"), ("prefix", "Left", "Right"), ("prefix", "left", "right"), ("suffix", "_L", "_R"), ("suffix", "_l", "_r"), ("suffix", "_lf", "_rt"), ("infix", "_L_", "_R_"), ("infix", "_l_", "_r_"), ("infix", "Left", "Right"), ("infix", "left", "right"), ) def short_name(n): return n.rsplit("|", 1)[-1].rsplit(":", 1)[-1] def mirror_name(name, rules=SIDE_RULES): """Opposite-side short name, or None for a centre name. Pose Editor mirroring also needs exact, case-sensitive side substrings (Maya 2027 Help, Mirror pose interpolators).""" s = short_name(name) for kind, a, b in rules: for x, y in ((a, b), (b, a)): if kind == "prefix" and s.startswith(x): return y + s[len(x):] if kind == "suffix" and s.endswith(x): return s[:-len(x)] + y if kind == "infix" and x in s: return s.replace(x, y, 1) return None def mirror_influence_map(names, rules=SIDE_RULES): """(map, unmatched): map[i] = index of the opposite influence (itself for centre ones).""" by_short = {} for i, n in enumerate(names): by_short.setdefault(short_name(n), i) out, unmatched = [], [] for i, n in enumerate(names): m = mirror_name(n, rules) if m is None: out.append(i) elif m in by_short: out.append(by_short[m]) else: out.append(i) unmatched.append(n) return out, unmatched def mirror_vertex_map(points, axis=0, tol=0.01): """(map, unmatched_count): map[v] = vertex nearest to v mirrored across the plane axis = 0, or -1 when none lies within tol (cm) [added default]. Mirroring needs a mesh centred on the axis in a symmetric pose (Maya 2027 Help, Mirror smooth skin weights).""" grid = Grid(points, cell=max(float(tol), 1e-6) * 2.0) # partners only matter within tol out, miss = [], 0 for p in points: q = list(p) q[axis] = -q[axis] q = tuple(q) hits = grid.within(q, tol) if hits: out.append(min(hits, key=lambda u: _dist(points[u], q))) else: out.append(-1) miss += 1 return out, miss def _remap_row(row, imap): out = {} for i, w in row.items(): j = imap[i] out[j] = out.get(j, 0.0) + w return out def mirror_weights(W, vmap, imap, points, axis=0, source="+", center_tol=1e-3): """Copy the source side onto the other side with influences swapped by imap; centre vertices (|coord| <= center_tol) get the average of their row and its mirror [added]. source '+' copies +X to -X (the character's left to right for a Y-up, +Z-facing character, like copySkinWeights without mirrorInverse). Returns (W, stats).""" out = [dict(r) for r in W] copied = centre = missing = 0 for v, p in enumerate(points): x = p[axis] u = vmap[v] if abs(x) <= center_tol: src = W[u] if u >= 0 else W[v] row = dict(W[v]) for i, w in _remap_row(src, imap).items(): row[i] = row.get(i, 0.0) + w out[v] = normalize_rows([row])[0] centre += 1 elif (x < 0.0) == (source == "+"): if u < 0: missing += 1 continue out[v] = _remap_row(W[u], imap) copied += 1 return out, {"copied": copied, "centre": centre, "unmatched": missing} def mirror_error(W, vmap, imap, verts=None): """(max difference, vertex) between each vertex's row and its partner's mirrored row.""" m, where = 0.0, -1 vs = range(len(W)) if verts is None else verts for v in vs: u = vmap[v] if u < 0: continue a, b = W[v], _remap_row(W[u], imap) for i in set(a) | set(b): d = abs(a.get(i, 0.0) - b.get(i, 0.0)) if d > m: m, where = d, v return m, where def remap_by_closest(src_points, src_W, dst_points, k=1): """Weights for dst points from the nearest source points (k > 1: inverse-distance blend). For meshes that overlap in space, as Copy Skin Weights expects (Maya 2027 Help).""" grid = Grid(src_points) out, dmax, dsum = [], 0.0, 0.0 for p in dst_points: hits = grid.nearest(p, k) dmax = max(dmax, hits[0][0]) dsum += hits[0][0] if k == 1 or hits[0][0] < 1e-9: out.append(dict(src_W[hits[0][1]])) continue row = {} for d, u in hits: for i, w in src_W[u].items(): row[i] = row.get(i, 0.0) + w / d s = sum(row.values()) out.append({i: w / s for i, w in row.items()}) return out, {"max_distance": dmax, "mean_distance": dsum / max(1, len(dst_points))} # =============================================================================== ROM schedules def rom_schedule(segments, start=1, step=10): """Calisthenics timing (Makauskas 00:03:01): keys every `step` frames, a pose on every second key, back to rest in between, one motion per segment (00:10:36). segments: [{"label": str, "channels": {"node.attr": value}}] or (node, attr, value[, label]). Values are absolute unless key_rom(relative=True). Returns the schedule dict (pure data).""" norm = [] for s in segments: if isinstance(s, dict): norm.append({"label": s.get("label") or ",".join("%s=%g" % kv for kv in s["channels"].items()), "channels": dict(s["channels"])}) else: node, attr, value = s[0], s[1], s[2] plug = "%s.%s" % (node, attr) norm.append({"label": s[3] if len(s) > 3 else "%s=%g" % (plug, value), "channels": {plug: value}}) plugs = [] for s in norm: for p in s["channels"]: if p not in plugs: plugs.append(p) keys = {(p, start): None for p in plugs} poses, f = [], start for s in norm: pose_f, rest_f = f + step, f + 2 * step for p, val in s["channels"].items(): keys.setdefault((p, f), None) keys[(p, pose_f)] = val keys[(p, rest_f)] = None poses.append({"label": s["label"], "frame": pose_f, "channels": dict(s["channels"])}) f = rest_f key_list = sorted(((p, fr, v) for (p, fr), v in keys.items()), key=lambda k: (k[1], k[0])) rest_frames = sorted({start} | {p["frame"] + step for p in poses}) return {"start": start, "end": f, "step": step, "plugs": plugs, "keys": key_list, "poses": poses, "rest_frames": rest_frames} def rom_steps(lo, hi, step_deg=30.0): """Angles from 0 toward each limit in `step_deg` increments, limits included, 0 excluded (Widup: range -30..90 gives -30, 30, 60, 90 plus the neutral, 1_PoTPm2L1c 00:15:50).""" vals = [] a = step_deg while a < hi - 1e-6: vals.append(a) a += step_deg if hi > 1e-6: vals.append(hi) a = -step_deg while a > lo + 1e-6: vals.append(a) a -= step_deg if lo < -1e-6: vals.append(lo) return sorted(set(round(v, 6) for v in vals)) def rom_from_limits(joints, step_deg=30.0, default_range=(-90.0, 90.0), axes=("rx", "ry", "rz"), start=1, step=10): """One-joint-at-a-time pose data in ~30 degree steps (Widup, ML Deformer training, 00:15:50, 00:27:41). joints: [{"node": name, "limits": {"rx": (min, max)}}]; axes without limits use default_range [added] and are listed in the schedule's "defaulted" field.""" segs, defaulted = [], [] for j in joints: lim = j.get("limits", {}) for ax in axes: if ax in lim and lim[ax] is None: continue lo, hi = lim.get(ax, default_range) if ax not in lim: defaulted.append("%s.%s" % (j["node"], ax)) for val in rom_steps(lo, hi, step_deg): segs.append({"label": "%s.%s=%g" % (j["node"], ax, val), "channels": {"%s.%s" % (j["node"], ax): val}}) sched = rom_schedule(segs, start, step) sched["defaulted"] = defaulted return sched # =============================================================================== deformation metrics def edge_ratios(P0, P1, edges): out = [] for a, b in edges: l0 = _dist(P0[a], P0[b]) out.append(_dist(P1[a], P1[b]) / l0 if l0 > 1e-12 else 1.0) return out def face_area_ratios(P0, P1, faces): A0 = face_vector_areas(P0, faces) A1 = face_vector_areas(P1, faces) return [(_len(b) / _len(a)) if _len(a) > 1e-12 else 1.0 for a, b in zip(A0, A1)] def fold_edges(P0, P1, topo, fold_deg=None, rest_max_deg=None): """Interior edges whose two faces fold onto each other in the pose (dihedral >= fold_deg) while they were fairly flat at rest (<= rest_max_deg): collapsed creases at the inner elbow or knee.""" fold_deg = THRESHOLDS["fold_deg"] if fold_deg is None else fold_deg rest_max_deg = THRESHOLDS["fold_rest_max_deg"] if rest_max_deg is None else rest_max_deg n0 = face_vector_areas(P0, topo["faces"]) n1 = face_vector_areas(P1, topo["faces"]) out = [] for e, fs in enumerate(topo["edge_faces"]): if len(fs) != 2: continue a, b = fs if _len(n0[a]) < 1e-12 or _len(n0[b]) < 1e-12 or _len(n1[a]) < 1e-12 or _len(n1[b]) < 1e-12: continue if _angle_deg(n0[a], n0[b]) <= rest_max_deg and _angle_deg(n1[a], n1[b]) >= fold_deg: out.append(e) return out def cross_section(points, topo, plane_point, plane_normal, verts=None): """Section of the mesh by a plane: the loop of edge crossings nearest to plane_point, ordered through shared faces. Returns {"points", "closed", "area", "perimeter", "centroid"} or None. Used for the candy-wrapper test: the section at mid-forearm keeps its area under a 90 degree twist with dual quaternion skinning and collapses with linear skinning (Maya 2027 Help, smooth skinning methods) [added: metric]. verts restricts crossings to edges touching those vertices.""" n = _unit(plane_normal) scale = max(1e-6, _len(_sub(*bbox(points)))) c = _add(plane_point, _mul(n, 1.37e-7 * scale)) # avoid vertices exactly on the plane d = [_dot(_sub(p, c), n) for p in points] allowed = None if verts is None else set(verts) hit = {} for e, (a, b) in enumerate(topo["edges"]): if (d[a] > 0.0) == (d[b] > 0.0): continue if allowed is not None and a not in allowed and b not in allowed: continue t = d[a] / (d[a] - d[b]) hit[e] = _lerp(points[a], points[b], t) if len(hit) < 3: return None adj = {e: set() for e in hit} for fi, fe in enumerate(topo["face_edges"]): hs = [e for e in fe if e in hit] for i in range(len(hs)): for j in range(i + 1, len(hs)): adj[hs[i]].add(hs[j]) adj[hs[j]].add(hs[i]) seen, comps = set(), [] for e in hit: if e in seen: continue stack, comp = [e], [] seen.add(e) while stack: x = stack.pop() comp.append(x) for y in adj[x]: if y not in seen: seen.add(y) stack.append(y) comps.append(comp) def centroid(comp): s = (0.0, 0.0, 0.0) for e in comp: s = _add(s, hit[e]) return _mul(s, 1.0 / len(comp)) comps = [cp for cp in comps if len(cp) >= 3] if not comps: return None comp = min(comps, key=lambda cp: _dist(centroid(cp), plane_point)) cset = set(comp) start = next((e for e in comp if len(adj[e] & cset) == 1), comp[0]) order, prev, cur = [start], None, start while True: nxt = [y for y in adj[cur] if y in cset and y != prev and y not in order] if not nxt: break prev, cur = cur, nxt[0] order.append(cur) closed = len(order) == len(comp) and start in adj[order[-1]] and len(order) > 2 pts = [hit[e] for e in order] cen = centroid(order) area_vec = (0.0, 0.0, 0.0) per = 0.0 m = len(pts) for i in range(m if closed else m - 1): p, q = pts[i], pts[(i + 1) % m] area_vec = _add(area_vec, _cross(_sub(p, cen), _sub(q, cen))) per += _dist(p, q) return {"points": m, "closed": bool(closed), "area": abs(_dot(area_vec, n)) * 0.5 if closed else 0.0, "perimeter": per, "centroid": cen} def signed_volume(points, tris, verts=None, origin=(0.0, 0.0, 0.0)): """Signed volume by the divergence theorem (positive for a closed shell with outward normals).""" allowed = None if verts is None else set(verts) V = 0.0 for i, j, k in tris: if allowed is not None and not (i in allowed and j in allowed and k in allowed): continue a, b, c = _sub(points[i], origin), _sub(points[j], origin), _sub(points[k], origin) V += _dot(a, _cross(b, c)) / 6.0 return V def bone_volume(points, tris, a, b, verts=None): """Volume swept around a bone: tetrahedra from each triangle to the closest point of segment a-b. Compare rest and pose for one region (bicep, forearm) [added approximation for open regions].""" allowed = None if verts is None else set(verts) V = 0.0 for i, j, k in tris: if allowed is not None and not (i in allowed and j in allowed and k in allowed): continue pi, pj, pk = points[i], points[j], points[k] cen = ((pi[0] + pj[0] + pk[0]) / 3.0, (pi[1] + pj[1] + pk[1]) / 3.0, (pi[2] + pj[2] + pk[2]) / 3.0) ab = _sub(b, a) l2 = _dot(ab, ab) t = 0.0 if l2 < 1e-18 else _clamp(_dot(_sub(cen, a), ab) / l2) o = _add(a, _mul(ab, t)) V += _dot(_sub(pi, o), _cross(_sub(pj, o), _sub(pk, o))) / 6.0 return V def shell_volumes(points, tris, shells): """Signed volume per shell about its own centroid; negative means the normals point inward (Geodesic Voxel needs outward front faces, Maya 2027 Help).""" groups = {} for v, s in enumerate(shells): groups.setdefault(s, []).append(v) out = {} for s, vs in groups.items(): cen = _mul(_add((0.0, 0.0, 0.0), tuple(sum(points[v][d] for v in vs) for d in range(3))), 1.0 / len(vs)) out[s] = signed_volume(points, tris, verts=vs, origin=cen) return out def displacements(P0, P1): return [_dist(a, b) for a, b in zip(P0, P1)] def cross_talk(P0, P1, W, moved, tol=None): """Vertices with zero weight on every moved influence that still moved: impossible for pure skinning, so it flags a deformer above or below the skin (ML Deformer cross talk, Widup 00:27:41; a Delta Mush or wrap leaking) [added: exact metric].""" tol = THRESHOLDS["cross_talk"] if tol is None else tol moved = set(moved) bad, mx = [], 0.0 for v, row in enumerate(W): if sum(w for i, w in row.items() if i in moved) > 1e-9: continue d = _dist(P0[v], P1[v]) if d > tol: bad.append(v) mx = max(mx, d) return {"count": len(bad), "max": mx, "sample": bad[:20]} def rigid_error(P0, P1, edges, verts): """Largest |edge ratio - 1| among edges inside `verts` (foot sole, palm: Makauskas restores rigidity where anatomy is rigid, 00:23:31).""" vs = set(verts) m = 0.0 for a, b in edges: if a in vs and b in vs: l0 = _dist(P0[a], P0[b]) if l0 > 1e-12: m = max(m, abs(_dist(P1[a], P1[b]) / l0 - 1.0)) return m def mesh_error(Pa, Pb): """Mean and max distance between two point lists (ML output against its target, held-out poses).""" ds = displacements(Pa, Pb) i = max(range(len(ds)), key=ds.__getitem__) if ds else -1 return {"mean": sum(ds) / max(1, len(ds)), "max": ds[i] if ds else 0.0, "argmax": i} def _by_region(items, region_of, names): out = {} for x in items: r = region_of(x) key = names[r] if names and 0 <= r < len(names) else str(r) out[key] = out.get(key, 0) + 1 return dict(sorted(out.items(), key=lambda kv: -kv[1])) def pose_metrics(P0, P1, topo, W=None, names=None, thresholds=None, bones0=None, bones1=None, moved=None, sections=None, rigid=None, min_region_tris=8): """Every measurable deformation check for one pose against rest. bones0/bones1: {influence index: (joint position, child position or None)} at rest and pose; sections: [{"label", "rest": (point, normal), "pose": (point, normal), "verts": optional}]; rigid: {label: [vertex ids]}. Regions are the dominant influence of each vertex.""" thr = dict(THRESHOLDS) thr.update(thresholds or {}) dom = dominant(W) if W else [-1] * len(P0) edges = topo["edges"] er = edge_ratios(P0, P1, edges) pinch = [e for e, r in enumerate(er) if r < thr["pinch"]] stretch = [e for e, r in enumerate(er) if r > thr["stretch"]] ar = face_area_ratios(P0, P1, topo["faces"]) collapsed = [f for f, r in enumerate(ar) if r < thr["collapse_area"]] folds = fold_edges(P0, P1, topo, thr["fold_deg"], thr["fold_rest_max_deg"]) disp = displacements(P0, P1) rep = { "edge_ratio_min": min(er) if er else 1.0, "edge_ratio_max": max(er) if er else 1.0, "pinch": {"count": len(pinch), "by_region": _by_region(pinch, lambda e: dom[edges[e][0]], names)}, "stretch": {"count": len(stretch), "by_region": _by_region(stretch, lambda e: dom[edges[e][0]], names)}, "collapsed_faces": {"count": len(collapsed), "by_region": _by_region(collapsed, lambda f: dom[topo["faces"][f][0]], names)}, "folds": {"count": len(folds), "by_region": _by_region(folds, lambda e: dom[edges[e][0]], names)}, "max_displacement": max(disp) if disp else 0.0, } if W is not None and moved is not None: rep["cross_talk"] = cross_talk(P0, P1, W, moved, thr["cross_talk"]) if bones0 and bones1 and W is not None: vols = {} region_verts = {} for v, r in enumerate(dom): region_verts.setdefault(r, []).append(v) for i in (moved if moved is not None else bones0.keys()): if i not in bones0 or i not in bones1 or i not in region_verts: continue vs = set(region_verts[i]) ntri = sum(1 for t in topo["tris"] if t[0] in vs and t[1] in vs and t[2] in vs) if ntri < min_region_tris: continue a0, b0 = bones0[i] a1, b1 = bones1[i] v0 = bone_volume(P0, topo["tris"], a0, b0 if b0 is not None else a0, vs) v1 = bone_volume(P1, topo["tris"], a1, b1 if b1 is not None else a1, vs) if abs(v0) > 1e-9: vols[names[i] if names else str(i)] = v1 / v0 rep["volume_ratio"] = vols if sections: out = [] for s in sections: r0 = cross_section(P0, topo, s["rest"][0], s["rest"][1], s.get("verts")) r1 = cross_section(P1, topo, s["pose"][0], s["pose"][1], s.get("verts")) item = {"label": s.get("label", "section")} if r0 and r1 and r0["closed"] and r1["closed"] and r0["area"] > 0 and r0["perimeter"] > 0: item.update(area_ratio=r1["area"] / r0["area"], perimeter_ratio=r1["perimeter"] / r0["perimeter"], rest_area=r0["area"], pose_area=r1["area"]) else: item["error"] = "section not found or not closed (rest %s, pose %s)" % (bool(r0), bool(r1)) out.append(item) rep["sections"] = out if rigid: rep["rigid"] = {label: rigid_error(P0, P1, edges, vs) for label, vs in rigid.items()} return rep def deformation_verdict(report, thresholds=None): """error / warn / info lines for deformation_test() (or a list of pose_metrics dicts).""" thr = dict(THRESHOLDS) thr.update(thresholds or {}) out = [] if "weights" in report: out.extend(weights_verdict(report["weights"])) for p in report.get("poses", []): tag = "pose %r f%s" % (p.get("label"), p.get("frame")) m = p.get("metrics", p) ct = m.get("cross_talk") if ct and ct["count"]: out.append("error: %s: %d vertices with no weight on the moved joints moved (max %.3g cm): a deformer " "outside the skin, or the wrong joint moved" % (tag, ct["count"], ct["max"])) for s in m.get("sections", []): if "area_ratio" in s and (s["area_ratio"] < thr["section_area"] or s["perimeter_ratio"] < thr["section_perimeter"]): out.append("warn: %s: section %s keeps %.0f%% area, %.0f%% perimeter (candy wrapper: twist joints, " "DQ blend or a corrective)" % (tag, s["label"], 100 * s["area_ratio"], 100 * s["perimeter_ratio"])) elif "error" in s: out.append("info: %s: section %s: %s" % (tag, s["label"], s["error"])) for region, r in (m.get("volume_ratio") or {}).items(): if abs(r - 1.0) > thr["volume"]: out.append("warn: %s: %s volume %+.0f%%" % (tag, region, 100 * (r - 1.0))) for key, label in (("pinch", "edges compressed below %.2f" % thr["pinch"]), ("stretch", "edges stretched above %.2f" % thr["stretch"]), ("collapsed_faces", "faces below %.0f%% area" % (100 * thr["collapse_area"])), ("folds", "folded edges")): c = m.get(key, {}).get("count", 0) if c: top = list(m[key]["by_region"].items())[:3] out.append("warn: %s: %d %s (%s)" % (tag, c, label, ", ".join("%s %d" % kv for kv in top))) for label, e in (m.get("rigid") or {}).items(): if e > thr["rigid"]: out.append("warn: %s: rigid region %s changes %.0f%%" % (tag, label, 100 * e)) for acc in m.get("penetration", []): if acc.get("increase", 0) > 0: out.append("warn: %s: %d vertices of %s inside %s" % (tag, acc["increase"], acc["mesh"], acc["body"])) return out # =============================================================================== blend shape splits def hat_weights(values, centers): """Partition of unity over sorted 1D centers (piecewise linear, clamped at the ends): lip sections that sum back to the master shape (antCGi's 0.8/0.2, 0.7/0.3 hand-typed loops, RbQM82Ta6zE 00:23:36, as a function [added]). Returns one list of len(centers) weights per value.""" order = sorted(range(len(centers)), key=lambda k: centers[k]) cs = [centers[k] for k in order] out = [] for x in values: w = [0.0] * len(centers) if x <= cs[0]: w[order[0]] = 1.0 elif x >= cs[-1]: w[order[-1]] = 1.0 else: for k in range(len(cs) - 1): if cs[k] <= x <= cs[k + 1]: u = (x - cs[k]) / (cs[k + 1] - cs[k]) if cs[k + 1] > cs[k] else 0.0 w[order[k]] += 1.0 - u w[order[k + 1]] += u break out.append(w) return out def side_weights(points, axis=0, width=0.5): """(left, right) complementary weights with a smooth band of +-width around the centre line.""" left = [smoothstep(-width, width, p[axis]) for p in points] return left, [1.0 - x for x in left] def split_targets(base, master, sections): """Section target point lists base + (master - base) * w_s. sections: per-vertex weight lists that must sum to 1 where the master moves; then the sections at weight 1 recreate the master exactly (antCGi's generator, 00:31:59).""" out = [] for w in sections: out.append([_add(b, _mul(_sub(m, b), w[v])) for v, (b, m) in enumerate(zip(base, master))]) return out def split_error(base, master, targets): """Max |sum of section deltas - master delta| (antCGi's visual sum test made exact).""" m = 0.0 for v in range(len(base)): s = (0.0, 0.0, 0.0) for t in targets: s = _add(s, _sub(t[v], base[v])) m = max(m, _dist(s, _sub(master[v], base[v]))) return m # =============================================================================== linear skinning maths def skin_matrices(bind_pre, world): """Per influence: bindPreMatrix * worldMatrix (the matrix a skinCluster applies to a rest point).""" return [mat_mul(b, m) for b, m in zip(bind_pre, world)] def lbs_points(rest, W, skin_mats): """Linear blend skinning in Python (reference and tests). Assumes an identity geometry matrix.""" out = [] for p, row in zip(rest, W): x = y = z = 0.0 for i, w in row.items(): q = xform_point(p, skin_mats[i]) x += w * q[0] y += w * q[1] z += w * q[2] out.append((x, y, z)) return out def invert_deltas(W, skin_mats, deltas, verts=None): """Rest-space deltas that reproduce posed-space deltas under linear skinning: d_rest = d_pose * L^-1 with L the blended 3x3 skinning matrix of the vertex. This is what a pre-deformation corrective needs (it 'fixes the original mesh', Maya 2027 Help). Returns (deltas, singular_vertices).""" out = [(0.0, 0.0, 0.0)] * len(deltas) singular = [] vs = range(len(deltas)) if verts is None else verts for v in vs: d = deltas[v] if d == (0.0, 0.0, 0.0): continue m = [0.0] * 16 for i, w in W[v].items(): s = skin_mats[i] for k in (0, 1, 2, 4, 5, 6, 8, 9, 10): m[k] += w * s[k] inv, det = _mat3_inverse(m) if inv is None: singular.append(v) continue out[v] = (d[0] * inv[0] + d[1] * inv[3] + d[2] * inv[6], d[0] * inv[1] + d[1] * inv[4] + d[2] * inv[7], d[0] * inv[2] + d[1] * inv[5] + d[2] * inv[8]) return out, singular # =============================================================================== joints inside (pure) def _ray_hits(points, tris, p, axis): u, v = [d for d in (0, 1, 2) if d != axis] pu, pv = p[u] + 1.234567e-5, p[v] + 2.345678e-5 # jitter off edges [added] hits = 0 for a, b, c in tris: A, B, C = points[a], points[b], points[c] if (pu < A[u] and pu < B[u] and pu < C[u]) or (pu > A[u] and pu > B[u] and pu > C[u]): continue if (pv < A[v] and pv < B[v] and pv < C[v]) or (pv > A[v] and pv > B[v] and pv > C[v]): continue den = (B[u] - A[u]) * (C[v] - A[v]) - (C[u] - A[u]) * (B[v] - A[v]) if abs(den) < 1e-18: continue l1 = ((B[u] - pu) * (C[v] - pv) - (C[u] - pu) * (B[v] - pv)) / den l2 = ((C[u] - pu) * (A[v] - pv) - (A[u] - pu) * (C[v] - pv)) / den l3 = 1.0 - l1 - l2 if l1 < 0.0 or l2 < 0.0 or l3 < 0.0: continue if l1 * A[axis] + l2 * B[axis] + l3 * C[axis] > p[axis]: hits += 1 return hits def point_inside(points, tris, p): """Ray-parity test along +X, +Y and +Z, majority vote (robust to one grazing ray) [added].""" return sum(_ray_hits(points, tris, p, ax) % 2 for ax in (0, 1, 2)) >= 2 def joints_inside(points, tris, joint_positions): """{joint: inside?}. Heat Map gives no weight to joints outside the mesh; Geodesic Voxel needs every joint inside (Maya 2027 Help, Bind methods; Geodesic Voxel binding).""" return {j: point_inside(points, tris, p) for j, p in joint_positions.items()} # =============================================================================== Maya glue def _cmds(): import maya.cmds as cmds return cmds def _om(): import maya.api.OpenMaya as om return om def _oma(): import maya.api.OpenMayaAnim as oma return oma class LayersPresentError(RuntimeError): pass def resolve_mesh(name): """(shape, transform) long names; from scenario-maya-expert's mx_audit (shared toolkit).""" import mx_audit return mx_audit.resolve_mesh(name) def _sel(name): om = _om() s = om.MSelectionList() s.add(name) return s def _dag(name): return _sel(name).getDagPath(0) def _mobj(name): return _sel(name).getDependNode(0) def mesh_points(mesh, world=True): """Current (deformed) points of the visible shape, cm.""" om = _om() shape = resolve_mesh(mesh)[0] pts = om.MFnMesh(_dag(shape)).getPoints(om.MSpace.kWorld if world else om.MSpace.kObject) return [(p.x, p.y, p.z) for p in pts] def mesh_topology(mesh, world=True): """build_topology() of the visible shape at the current time, plus "shape" and "transform".""" om = _om() shape, xf = resolve_mesh(mesh) fn = om.MFnMesh(_dag(shape)) counts, connects = fn.getVertices() counts, connects = list(counts), list(connects) faces, k = [], 0 for c in counts: faces.append(connects[k:k + c]) k += c topo = build_topology(mesh_points(shape, world), faces) topo["shape"], topo["transform"] = shape, xf return topo def skin_clusters(mesh): """skinCluster nodes deforming the mesh, nearest to the output first (the most recently added cluster is listed first in Maya's own tools, Maya 2027 Help) [verify order].""" cmds = _cmds() shape = resolve_mesh(mesh)[0] hist = cmds.listHistory(shape, pruneDagObjects=True) or [] return [h for h in hist if cmds.nodeType(h) == "skinCluster"] def skin_cluster(mesh, index=0): scs = skin_clusters(mesh) if not scs: raise ValueError("%s has no skinCluster" % mesh) return scs[index] def _skin_fn(sc): return _oma().MFnSkinCluster(_mobj(sc)) def _skin_path(fn, shape=None): try: return fn.getPathAtIndex(0) except Exception: if shape is None: raise return _dag(shape) def influences(sc): """(names, logical indices) in physical order.""" fn = _skin_fn(sc) paths = fn.influenceObjects() return [p.partialPathName() for p in paths], [int(fn.indexForInfluenceObject(p)) for p in paths] def skin_settings(sc): cmds = _cmds() out = {} for a in ("skinningMethod", "normalizeWeights", "maxInfluences", "maintainMaxInfluences", "weightDistribution", "envelope"): try: out[a] = cmds.getAttr("%s.%s" % (sc, a)) except Exc
-
-
SKILL.md 19.1 KB
--- name: scenario-maya-deformation description: 'Use when skinning a character in Maya ("bind skin", "fix the weights", weight painting, Skin Tools or ngSkinTools layers), when deformation looks wrong (pinching, candy-wrapper wrist, collapsing elbow or shoulder, volume loss, clothes poking through), when mirroring, copying or capping skin weights for Unreal or Unity, building blend shapes, corrective shapes or pose space deformation (Pose Editor on IK-driven joints), a face rig with shapes or joints, multiple skinClusters, Delta Mush or the ML Deformer, or running a range-of-motion deformation test.' license: MIT --- # Maya deformation (character TD) Expert deformation is decided before the first weight: joint pivots and edge loops set what skinning can do. Weights are then built region by region, judged in motion at extremes with numbers and renders, and every fault is fixed where it originates. Stance: block rigid, diagnose, smooth and shape, correct what weights cannot, and keep everything regenerable as data. If a sibling skill named here is missing from your available skills, ask the user to install it (`npx skills add scenario-labs/skills --skill <name>`); unattended, proceed from tool schemas and flag the gap. **REQUIRED BACKGROUND:** scenario-maya-expert (execution channel, review loop, 2027 version traps). Module [`scripts/mx_skin.py`](scripts/mx_skin.py): weights IO, region blocking, masks, mirror, transfer, ROM generator, deformation metrics, correctives, Skin Tools wrappers. **Status (2026-09-24):** its pure-Python half ran offline (`test_mx_skin_offline.py`, all pass); every Maya call is **not yet run in Maya** (tests: `tests/code/maya-deformation/run_all.sh`, via `mx_run.py`). `import sys; sys.path.insert(0, "<skills>/scenario-maya-deformation/scripts"); import mx_skin as S` ## Stance (the expert delta) - **Raw blocked weights are a rig test.** Assign every vertex 100% to its closest joint and pose: it "should look relatively close to your expected final result"; if not, fix pivots, placement or loops first. Smoothing a rig fault away: "don't do this in production" (Makauskas). Loops running along the wrist or ankle are the modeler's fault; volume loss at a clean elbow is the rig's (antCGi). - **Fill the region, art-direct in the mask.** One layer per decision: twist joints get an even fill, the shape of shrug, shoulder and elbow lives in the mask, so re-smoothing twists never breaks the elbow (Jao). "Mask is not an actual influence", it is the layer's alpha (Makauskas). Headless, `S.block_regions` composites the same stack and writes once. - **Judge in motion, one motion per test.** Calisthenics: keys every 10 frames, a pose on every second key, back to bind between, never bend and lower together (Makauskas); extreme and "weird" poses expose errors. ML pose data: one joint at a time in about 30 degree steps (Widup). - **Smoothing is not free.** Sparse fingers lose volume: smooth the middle loops, then Sharpen (Makauskas). Measured here [added]: smoothing a 110 degree elbow cut stretch from 3.9x to 1.5x but collapsed the inner elbow (edge ratio 0.19). Deep bends need correctives, not more smoothing. - **Skin Tools layers are an authoring state.** While layers exist, Maya blocks the classic paint, mirror, copy, hammer, set-max-influences and normalize tools; Delete Skin Layers keeps the evaluated weights and is the handoff step (Help). "It's still a normal Maya skin in the end" (Jao). - **Choose the corrective's space before sculpting.** Pre-deformation is "like fixing the original character's mesh" and holds in other poses; post-deformation fixes one posed result, in Transform Space when one joint owns the fix (faster, bounded by that joint's envelope) or Tangent Space when it follows the surface across joints (Help; the cross-joint reading is [added]). Extremes first, then in-betweens (Help). Engines: secondary joints driven by swing-twist pose fractions, each assigned to its LODs (Rudy). - **Read the joint the rig actually moves.** Bind joints driven by IK/FK constraints or an offsetParentMatrix blend are posed through their controls. Pose Editor: create the interpolator without neutral poses, add the controller's driving attributes in Driver Settings, then Add Neutral Poses; Euler Twist when a wrist twists about the forearm (Help). Node readers use `dagLocalMatrix`, since an OPM drive leaves rotate at 0 [added]. - **Face: budget first, joints for rotation.** Ask for the joint and shape budget first; a neutral with lips slightly parted made skinning "night and day easier" (Burton). Eyes, lids and jaw stay on joints even in shape faces, and split shapes must sum exactly to their master (antCGi). The ML Deformer learns what you feed it: skin joints in the Control Collector, Delta Mush removed first, pose coverage over network size (Widup). ## Establish first | Input | Changes | Default when the brief is silent | | --------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Target runtime | influence cap, DQ allowed, correctives as shapes or secondary joints, face shapes or joints | film-style in Maya: no cap while authoring, prune 0.01 at the end [added]; game: the engine's or lead's cap (Numbers) | | Skeleton (scenario-maya-rigging) | regions, twist chains, mirror rules, how bind joints are driven (constraint or OPM) | bind joints only, rotations zero, L_/R_ names, twist joints in every limb section (3 to 5 in the sources) | | Mesh (scenario-maya-retopology-uv) | bind method, shells | Heat Map on a watertight single shell with joints inside; Geodesic Voxel for multi-piece or non-manifold | | Deformation brief | stylized squash (two clusters), clothing, face scope, hero vs background (ML Deformer) | body plus clothing as separate skinned meshes; face = jaw, eyes and lids on joints | | Skin Tools API (`S.skin_tools_probe()`) | layered or plain path | plain cluster with in-memory layers; real layers only when a person will paint them | ## Workflow 1. **Intake gates.** `mx_validate.validate(profile="model")`, `mx_audit.verdict(r, symmetric=True)`, `S.bind_precheck(mesh, joints, method)`. GATE: no error lines, joints inside, transform frozen, no non-deformer history, X symmetry; face: lips apart, eye joint at the eyeball center (Burton). 2. **Bind, explicitly.** `S.bind(mesh, joints, method, skin_method=0, normalize=1)`: pass every flag, since cmds and UI defaults differ and the Help contradicts itself on `normalizeWeights`. GATE: `S.weights_verdict(S.audit_weights(...))` clean; `S.export_weights_json` backup (OpenMaya writes are not undoable). 3. **Block and diagnose.** `S.block_regions` with closest-bone regions, no smoothing; key calisthenics (`S.rom_schedule`, `S.key_rom`); `S.deformation_test`; `S.pose_snapshots` then `mx_review.review` headless and look. GATE: raw poses read close to the goal; faults go back to scenario-maya-rigging (pivots) or scenario-maya-retopology-uv (loops) with numbers and images. 4. **Regions, twists, masks.** Order: torso, clavicle and shoulder, upper arm, forearm, hand, thumb, fingers, head and neck, legs, feet (Makauskas). Twist chains as even ramps, masks as ranges along bones; with the API, `S.st_add_region_layer` and `S.st_flood`. Tune mask ranges, not vertices. GATE: audit clean, warnings shrink pose by pose, worst poses looked at. 5. **Mirror and shells.** `S.mirror_skin` (or the Skin Tools Mirror tab); clothing and accessories take the body under them (`S.copy_nearest`, `S.remap_by_closest`, volume smoothing limited to the shell). GATE: mirror error < 0.01, nothing unmatched, no penetration at rest. 6. **Full ROM.** Sections at mid-forearm and mid-upper-arm, bone volumes, folds, cross talk, rigid soles, clothing penetration; then combined "weird" poses and animator-style play; a GUI playblast when a session exists. GATE: no `error:`, every `warn:` fixed or explained, [`references/critique.md`](references/critique.md) at 2 or more. 7. **Correctives.** Candy wrapper: twist distribution, then DQ or weight-blended skinning. Deep bends: pick pre or post and the target space (stance), check `S.joint_drive(joint)` and pose through the controls when it is driven, then `S.solve_pre_corrective` from a posed sculpt plus `S.corrective_driver`, or the Pose Editor in the GUI (controller-driven order above; extremes first; Regularization from 0.01). GATE: 0 at rest, 1 at the pose reached through the controls, residual < 1e-3 cm; ROM rerun. 8. **Face, if in scope.** Shapes through the split generator (`S.split_targets`, `S.add_split_targets`), or a joint face with a zero rest pose (Burton); squash under tweaks as two clusters (`multi=True`, UV Pin reading cluster 1, group inverse into bindPreMatrix). GATE: split error < 1e-5, counts within budget, double-transform test passes. 9. **Finalize.** `S.delete_skin_layers`, `S.prune_and_limit` to the target cap, ROM rerun, weights JSON (plus layers JSON), `mx_validate.validate(profile="rig")`, a new saved version. ## Numbers | Value | Relative to | Source | | ------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------- | | Keys every 10 frames, pose on every second key | calisthenics timing | Makauskas [00:03:01] | | 3 (Makauskas) or 5 (Jao) twist joints | per upper arm or forearm | [00:07:57], [00:17:13] | | Weight sum 1 within 1e-4 | every vertex, per cluster | normalization (Jao, Help) | | Prune below 0.01 [added] | per weight; the Help cites 0.008 as insignificant | Help, Prune | | Influence cap 4 [added], 2 on Burton's Unreal mobile | per vertex, games only | Burton [00:51:55] | | Edge ratio < 0.7 pinch, > 1.3 stretch [added] | posed vs bind edge length | digest thresholds | | Section area < 0.7, perimeter < 0.8 [added] | mid-limb cross-section vs bind | candy wrapper; LBS at 50/50, 90 deg twist keeps 0.5 area (measured) | | Volume change > 10% [added] | region around its bone | Makauskas judges volume by eye | | Mirror difference < 0.01 [added] | weight of a vertex vs its mirror | | | Regularization 0.01 to 1.0; 3 neutral poses (neutral, swing, twist) | Pose Editor | Help | | 30 degree steps; 1,500 to 2,000 poses per body; 12 to 20 per arm | ML Deformer pose data | Widup [00:15:50] [00:37:08] | | 24 face joints | Burton's current-gen game face | [00:35:45] | ## Quality gates Measurable (headless, `mx_run`): `S.weights_verdict` clean at the target cap; `S.deformation_test` without `error:` (cross talk is impossible under pure skinning); warnings fixed or explained; mirror error < 0.01; `penetration` increase 0 for clothing; correctives 0 at rest and 1 at the pose through the controls; split error < 1e-5; `S.skin_layers_node(sc) == []` before handoff; re-imported JSON reproduces the weights (diff < 1e-4). Visual: `S.pose_snapshots` of every ROM pose rendered with `mx_review.review` (clay and wire, front and three-quarter, same cameras every iteration), or a GUI playblast. Look at shrug and deltoid, elbow crease, wrist twist, fingers, groin and knee while lifting the foot, feet, neck, clothing; faces: mouth corners, cheek squash, lids. Judge with `references/critique.md`. ## Common mistakes | Mistake | What it looks like | Fix | | ------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Smoothing over a bad pivot | wrist bump, neck lump that returns every pose | move the joint (scenario-maya-rigging), re-block (Makauskas) | | Edge counts read alone | rigid weights show zero pinch while pieces interpenetrate | read folds, collapsed faces and severity (critique.md) | | Editing `maximumInfluences` on a live cluster | custom weights silently reset | `S.prune_and_limit` (Help: `-mi` in edit mode wipes weights) | | `copySkinWeights` with default flags | closestComponent association, XY mirror plane | pass `surfaceAssociation`, `influenceAssociation`, `mirrorMode="YZ"` | | Classic tools on a layered cluster | "blocked" errors, or weights overwritten later | `S.delete_skin_layers` first, or the Skin Tools equivalents | | Heat Map with end joints outside the mesh | those joints get no weight | move joints inside or use geodesic after the precheck | | Test animation with combined motions | cannot tell which joint breaks | one motion per segment (`rom_schedule`) | | Flood smoothing sparse fingers | sausage fingers, lost knuckles | smooth middle loops, Sharpen (Makauskas) | | A mesh-level error sculpted as a post corrective | the fix shows near one pose only | pre-deformation (`S.solve_pre_corrective`): it holds in every pose (Help) | | Neutral poses created on an IK or constrained joint | the Pose Editor cannot return to neutral or go to a pose | no neutral poses at creation, Driver Settings > Controllers, then Add Neutral Poses (Help) | | Pose reader on `.matrix` or `setAttr` on a driven joint | corrective stuck on or off; pose rejected or snapped back | `S.corrective_driver` (dagLocalMatrix); pose through the controls | | Closed lips in the neutral | mirror and wrap transfers fail, lips stick | lips slightly parted (Burton); split upper and lower proxies (Jao) | | Overlapping split shapes | double motion at the corners | vertex ownership, difference shapes (raise not smile), sum test (antCGi) | | Delta Mush left under an ML Deformer | the ML learns the smoothing | delete it before training (Widup) | | Pinned controls reading the final mesh | dependency cycle | UV Pin from skinCluster1.outputGeometry (Maya LC) | ## Handoffs - **Receives from scenario-maya-rigging:** bind skeleton (joints only, rotations zero, oriented, single root, L_/R_ names or labels), controls for the ROM, twist joints placed, and whether bind joints blend by constraint or offsetParentMatrix; `mx_validate(profile="rig")` clean. From scenario-maya-retopology-uv: mesh frozen at the origin, no history, symmetric, loops across every joint, UVs, face with lips apart and eyes in place. - **Delivers to scenario-maya-animation:** a new rig version with layers deleted, weights and layers JSON, the ROM verdict JSON, the sheet or playblast looked at, what was not verified; referenced, never imported. - **Delivers to scenario-maya-pipeline-scripting (game skins):** weights capped and pruned for the engine, bind skeleton only as influences, secondary joint list with LOD assignments, correctives as joints when the engine needs them; audit JSON for its FBX export check. - **Sends back:** pivot and placement faults to scenario-maya-rigging, loop faults to scenario-maya-retopology-uv, each with the pose, numbers and image. ## Maya 2027 notes - Skin Tools (ngSkinTools-based) is built in: Skin > Skin Tools; layers only after explicit initialization; Bind Skin's Initialize Skin Layers is off by default; the paint preference defaults to the classic tool. - Scripting: `from ngSkinTools2 import api` (Developer Help, fetched 2026-09-24; not yet imported): `init_layers`, `Layers`, `assign_from_closest_joint`, `flood_weights` (acts on the component selection), `PaintModeSettings`, `NamedPaintTarget.MASK`, `export_json`, `import_json`. - Skin > Smooth Skin Weights uses the Skin Tools algorithm and errors on multi-layer clusters; Rigid Bind menus are gone (bind with one influence); `performSkinCluster.mel` is removed. - Several skinClusters per mesh need `skinCluster(multi=True)`; tool lists show the newest cluster first. - `mlDeformer` command (2026), `dgaTension` for stretch analysis (2026.3), Apply Mesh Compare (2026); Bifrost 3.0 ships with 2027; old experimental rigging modules are incompatible. ## References - [`references/procedures.md`](references/procedures.md): code per stage (P1 to P16) with status and test; when executing. - [`references/expert-notes.md`](references/expert-notes.md): principles by expert, disagreements; when a decision needs its reasoning. - `references/critique.md`: report reading, region checks, 0 to 3 rubric; before calling a stage done. - [`references/gui-paths.md`](references/gui-paths.md): Skin Tools, Shape Editor, Pose Editor, ML Deformer menus; in the GUI. - [`references/sources.md`](references/sources.md): sources, credentials, URLs, timestamps; to trace a claim.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.