diff --git a/docs/gallery/vertex-weight-limit/index.html b/docs/gallery/vertex-weight-limit/index.html index fd18821..0a47222 100644 --- a/docs/gallery/vertex-weight-limit/index.html +++ b/docs/gallery/vertex-weight-limit/index.html @@ -186,7 +186,7 @@

vertex-weight-limit

A runnable example that rigs a mech arm — flanged bolted pedestal and shoulder fairing, a panel-seamed upper arm ending in clevis cheeks, the elbow hinge pin and knuckle barrel capped with hex bolts through a ribbed flex cuff, a long seam-grooved forearm, wrist cuff and collar, and a three-finger gripper — with deliberately rich five-bone weight bumps in the cuffs, then enforces the game-engine maximum of four bone influences per vertex through the data API, following mesh-editing-and-bmesh and building on the linear-blend-skinning precedent of armature-bend.

Pipeline arc: modeling/LOD in lod-decimate-chain, weighting here, export in gltf-export-roundtrip.

What it witnesses: the skinning constraint every game engine enforces and AI-generated rigging code most often violates silently.

-

1. The limit is a data-API operation, not a context operator. Instead of bpy.ops.object.vertex_group_limit_total, the example reads each vertex's groups, keeps the top four by weight, VertexGroup.removes the rest, and renormalizes the survivors. Dropping without renormalizing leaves sums at 0.986 — a mesh that shrinks toward the origin under load (the check's measured failure, 1.438e-02 off unit sum). 2. The armature modifier is still exactly linear blend skinning after the limit: every depsgraph-evaluated vertex equals Σ wᵢ · (pose.matrix @ bone.matrix_local.inverted()) @ rest, with the weights read back from the mesh's own deform layer (v.groups) — the weights on the mesh are the contract, not the weights you meant to write. Measured lbs_err = 3.0e-07. 3. Pruning must not damage the pose. Evaluated positions before and after the limit are held within 0.05 (measured 3.0e-03), the pedestal mount stays exactly pinned (Root is unposed), and the pre-limit authoring really carries five influences in the boots — otherwise the witness would be vacuous.

+

1. The limit is a data-API operation, not a context operator. Instead of bpy.ops.object.vertex_group_limit_total, the example reads each vertex's groups, keeps the top four by weight, VertexGroup.removes the rest, and renormalizes the survivors. Dropping without renormalizing leaves sums at 0.984 — a mesh that shrinks toward the origin under load (the check's measured failure, 1.616e-02 off unit sum). 2. The armature modifier is still exactly linear blend skinning after the limit: every depsgraph-evaluated vertex equals Σ wᵢ · (pose.matrix @ bone.matrix_local.inverted()) @ rest, with the weights read back from the mesh's own deform layer (v.groups) — the weights on the mesh are the contract, not the weights you meant to write. Measured lbs_err = 2.7e-07. 3. Pruning must not damage the pose. Evaluated positions before and after the limit are held within 0.05 (measured 2.8e-03), the pedestal mount stays exactly pinned (Root is unposed), and the pre-limit authoring really carries five influences in the cuffs — otherwise the witness would be vacuous.

The vertex-group API (v.groups, VertexGroup.add/remove) is stable between Blender 4.5 LTS and 5.1 — the example runs identically on both, which is itself the version witness (measured values match to the digit).

The render shows the pruned arm mid-pose: the flex cuffs carry the teal accent — the five-influence zones the limit prunes glow at the elbow hinge and wrist, sealed by the bright hoop on the elbow cuff — proof that the limited weights still deform as authored.

Run

@@ -240,7 +240,7 @@

Source

SIDES = 32 MAX_INFLUENCES = 4 # the engine constraint being witnessed -BUMP_R = 1.8 # flex-boot blend support; guarantees 5 pre-limit +BUMP_R = 1.8 # flex-cuff blend support; guarantees 5 pre-limit LBS_TOL = 5e-4 # float32 mesh coords vs double pose matrices SUM_TOL = 1e-5 # per-vertex weight sum after renormalize POSE_TOL = 0.05 # pruning must not move the pose beyond this @@ -261,7 +261,7 @@

Source

def flex_weights(z): - """Rich pre-limit weights over all five bones — boots blend broadly.""" + """Rich pre-limit weights over all five bones — cuffs blend broadly.""" w = [bump(z, BONE_CENTERS[b]) for b in BONES] total = sum(w) return [x / total for x in w] @@ -408,7 +408,7 @@

Source

def assign_weights(obj, part_of): """Author the rich pre-limit weights: hard single-bone on armor, broad - five-bone bumps in the flex boots.""" + five-bone bumps in the flex cuffs.""" groups = {b: obj.vertex_groups.new(name=b) for b in BONES} if len(part_of) != len(obj.data.vertices): raise RuntimeError(f"part tag count {len(part_of)} != vert count " @@ -466,7 +466,7 @@

Source

def check(obj, arm, groups, pose_before): me = obj.data - # pre-limit witness: the flex boots really carry five influences + # pre-limit witness: the flex cuffs really carry five influences pre_max = max(len(v.groups) for v in me.vertices) if pre_max != 5: print(f"ERROR: pre-limit max influences {pre_max} != 5 — the rich " diff --git a/examples/vertex-weight-limit/README.md b/examples/vertex-weight-limit/README.md index 4eea9d4..0e5d23a 100644 --- a/examples/vertex-weight-limit/README.md +++ b/examples/vertex-weight-limit/README.md @@ -21,18 +21,18 @@ AI-generated rigging code most often violates silently. `bpy.ops.object.vertex_group_limit_total`, the example reads each vertex's groups, keeps the top four by weight, `VertexGroup.remove`s the rest, and renormalizes the survivors. Dropping without renormalizing leaves sums at - 0.986 — a mesh that shrinks toward the origin under load (the check's - measured failure, 1.438e-02 off unit sum). + 0.984 — a mesh that shrinks toward the origin under load (the check's + measured failure, 1.616e-02 off unit sum). 2. **The armature modifier is still exactly linear blend skinning** after the limit: every depsgraph-evaluated vertex equals `Σ wᵢ · (pose.matrix @ bone.matrix_local.inverted()) @ rest`, with the weights **read back from the mesh's own deform layer** (`v.groups`) — the weights on the mesh are the contract, not the weights you meant to write. - Measured `lbs_err = 3.0e-07`. + Measured `lbs_err = 2.7e-07`. 3. **Pruning must not damage the pose.** Evaluated positions before and after - the limit are held within 0.05 (measured 3.0e-03), the pedestal mount stays + the limit are held within 0.05 (measured 2.8e-03), the pedestal mount stays exactly pinned (Root is unposed), and the pre-limit authoring really carries - five influences in the boots — otherwise the witness would be vacuous. + five influences in the cuffs — otherwise the witness would be vacuous. The vertex-group API (`v.groups`, `VertexGroup.add`/`remove`) is stable between Blender 4.5 LTS and 5.1 — the example runs identically on both, which is itself diff --git a/examples/vertex-weight-limit/vertex_weight_limit.py b/examples/vertex-weight-limit/vertex_weight_limit.py index c544898..a1ad562 100644 --- a/examples/vertex-weight-limit/vertex_weight_limit.py +++ b/examples/vertex-weight-limit/vertex_weight_limit.py @@ -34,7 +34,7 @@ SIDES = 32 MAX_INFLUENCES = 4 # the engine constraint being witnessed -BUMP_R = 1.8 # flex-boot blend support; guarantees 5 pre-limit +BUMP_R = 1.8 # flex-cuff blend support; guarantees 5 pre-limit LBS_TOL = 5e-4 # float32 mesh coords vs double pose matrices SUM_TOL = 1e-5 # per-vertex weight sum after renormalize POSE_TOL = 0.05 # pruning must not move the pose beyond this @@ -55,7 +55,7 @@ def bump(z, center, radius=BUMP_R): def flex_weights(z): - """Rich pre-limit weights over all five bones — boots blend broadly.""" + """Rich pre-limit weights over all five bones — cuffs blend broadly.""" w = [bump(z, BONE_CENTERS[b]) for b in BONES] total = sum(w) return [x / total for x in w] @@ -202,7 +202,7 @@ def tag(rings_verts, bone): def assign_weights(obj, part_of): """Author the rich pre-limit weights: hard single-bone on armor, broad - five-bone bumps in the flex boots.""" + five-bone bumps in the flex cuffs.""" groups = {b: obj.vertex_groups.new(name=b) for b in BONES} if len(part_of) != len(obj.data.vertices): raise RuntimeError(f"part tag count {len(part_of)} != vert count " @@ -260,7 +260,7 @@ def eval_positions(obj): def check(obj, arm, groups, pose_before): me = obj.data - # pre-limit witness: the flex boots really carry five influences + # pre-limit witness: the flex cuffs really carry five influences pre_max = max(len(v.groups) for v in me.vertices) if pre_max != 5: print(f"ERROR: pre-limit max influences {pre_max} != 5 — the rich "