Claude Skill

scenario-blender-retopology

Use when turning a dense sculpt, scan or AI-generated mesh into clean topology in Blender, for animation, subdivision or a game low poly with a triangle budget. Also when choosing between QuadriFlow or voxel remesh and manual retopo, placing loops around eyes, mouth and joints, d

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download scenario-labs-skills-skills_dcc_blender_scenario-blender-retopology-dc592af.zip · 81 KB
Part of scenario-labs/skills — 97 skills

Install

skills CLI npx skills add https://github.com/scenario-labs/skills/tree/main/skills/dcc/blender/scenario-blender-retopology
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install scenario-labs-skills@llmmart
Git 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

Blender retopology

Expert retopology is a plan executed with the fewest loops that capture the forms and let the mesh deform: loops close around eyes, mouth and joints, poles sit where nothing moves, and the subdivided surface, not the cage, matches the sculpt. An agent without a mouse can build every piece of this from data, but it reaches expert quality only on the parts it plans explicitly. Automatic remeshing is a finish for static assets and a start for everything else. 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-blender-expert (execution channel, review loop, 5.2 API changes). Toolkit: scripts/bx_retopo.py (import sys; sys.path.append("<skills>/scenario-blender-retopology/scripts"); import bx_retopo as R), all coordinates in the sculpt's local space.

Stance (the expert delta)

  • Map first, as data. Kaspar draws six layers on the sculpt before any vertex: edge flow, primary creases, secondary (expression-only) creases, articulation, expression bounds, landmarks (BCON22 [00:07:55], shown in frames 00:07:53 to 00:14:38). The agent writes the same map as coordinates and loop counts (landmarks from renders plus R.surface_point), then builds.
  • Loops are the budget, not vertices. Articulation minimum (Kaspar after Brian Tindall): lids, lips and brows as curves with 3 controls and 2 handle loops each, plus 2 corner loops on eyes; upper count = lower count (Dikko, Kaspar, Kenny); 3 loops per joint; a crease is 1 loop on it plus 2 hugging it. Elsewhere "as much as it needs to be" (Kaspar Live #3 [01:56:33]).
  • Poles: valence 3 or 5, where nothing moves, never on a crease loop, a shell rim or the face center line (Kaspar Live #7 [00:11:41], Live #3 [00:36:54]; Dikko [00:04:49]). Lampel's reason: pinching is a density jump in the subdivided result, harmless on flat still areas (BCON24 [00:21:18]). [added] An agent's poles come from ring-count reductions (4 per unit) and from cut borders that staircase across an auto base (about half their vertices): budget both.
  • Loops close on themselves. A spiral around eyes or lips means rebuild (Dikko); a spiral that terminates in a still area is tolerated (Dikko body [00:09:34], Kaspar nose).
  • Judge the subdivided surface. A snapped cage shrinks inside when subdivided; a Shrinkwrap offset fixes convex areas and worsens cavities. Fit the cage so its subdivided surface matches (Duha BCON23 [00:08:58]): R.fit_subdiv. Kaspar's Multires Apply Base does the same job but overshoots (measured below).
  • Game low poly = silhouette plus animation loops (SpeedChar): face about 3x body density, triangles wherever nothing bends, twisted quads cut by hand so baker and engine agree, low poly slightly proud of the high; every hard edge is a UV seam (On Mars 3D).
  • Auto-remesh fits static, scanned, background or bake-only assets (Lampel [00:09:05], Grant Abbitt, Duha, Jamie Dunbar). For deforming characters it lacks the judgment calls: on the sheep, QuadriFlow at expert density gave no rings around eyes or mouth and tore a thin ear.

Establish first

Input Why it changes the plan Default when silent
Purpose film cage renders at subdivision 2 (Kaspar); game ships triangles and bakes deforming character: film-style all-quad cage
Budget film: no vertex budget, minimum loops (Kaspar); game: triangle budget film stylized head 1,650 to 4,100 faces, body 9,400 to 16,500 (Blender Studio meshes); game character < 10,000 tris
Deformation face rig, joints, how far the mouth travels (expression bounds) face + limbs deform
Camera detail and density follow the closest view (SpeedChar) close-up face
Style stylized: creases in topology; realistic: generic base + displacement (Kaspar) stylized
Symmetry R.topology_report(sculpt)["sym_local_pct"], local space Mirror only above ~98 % (the sheep is 21 %, posed)
Separate parts eyes, teeth, hair clumps, clothing (from the body duplicate) separate objects

Workflow

  1. Inspect (procedures P1): holes, manifoldness, scale, rotation, symmetry. GATE: all four written down.

  2. Map: read the sculptor's face sets first (R.face_set_map, R.face_set_review, P1b: lip line, eye masks, ears, hooves); landmarks with coordinates, loop counts per feature from the Numbers table, crease list (check expression shape keys), per-part density. GATE: map exists before any geometry.

  3. Choose a method per part:

    Part Method Evidence
    Eyes, mouth, brows (deforming) carve_rings(corners=, count=(lo, hi)): upper = lower by construction, geodesic rings, the border's count kept when inside the range; pair odd boundaries first (parity_strip) v2: eyes 24 (12/12), mouth 30 (15/15) on every clean ring; forcing Kaspar's 18/24 cost 40 more poles
    Nasolabial, face frame, neck (map lines) carve_band along enclosing_contour of face sets (P7): one clean loop, poles pushed onto the base v2: nasolabial +18 poles; face frame opt-in, +64 poles on the sheep
    Limbs, ears, tail, fingers, horns limb_profile (geodesic contours, root and joints found) + socket_tube(count=) (P3b, P8); straight parts: tube from sections v2: all 7 parts incl. the 1 m curled tail, 3 rings per joint
    Skull, torso, smooth masses QuadriFlow start acceptable, else grid patches little deformation
    Static prop, scan, bake-only QuadriFlow whole object (R.quadriflow) Lampel, Grant Abbitt
    Realistic human head shrinkwrap a clean generic base mesh, then fit (Kaspar BCON22 [00:20:16]) not scripted here
  4. Build islands separately and roughly (Kaspar: "don't focus on tweaking all the points"). Stack via R.setup_cage (Mirror clip > Shrinkwrap Target Normal Project > Subsurf hidden in edit mode). GATE: topology gate below passes per island.

  5. Relax and freeze hand-built layouts: R.relax, R.freeze. Not after QuadriFlow (worsened coverage 0.44 % to 0.55 %).

  6. Connect by counts: R.boundary_loops, then R.bridge / R.weld_loops on equal counts; reduce the larger side first (Kaspar: wrist 10 vs hand 16 was "six too many"). GATE: no triangles created.

  7. Volume: film R.fit_subdiv (gentle defaults); game R.push_outside then R.triangulate_twisted and a Triangulate modifier on export.

  8. Review and hand off: gates, two render sheets, handoff hygiene (P13). Report what the agent did not plan.

What an agent reaches (measured on the sheep; tests/code/blender-retopology/sheep_results.json and compare/metrics.json)

Strategy Faces Subdiv-2 mean error Inside Eye / mouth rings Closed-loop edges Looks
Kaspar's own retopo 2,596 0.24 % 47 % 5,5 / 2 29.9 % clean, reads unsubdivided
QuadriFlow, area-matched budget 4,372 0.14 % 87 % 0 / 0 1.6 % faceted, torn ear, uniform density
+ gentle fit_subdiv 4,372 0.045 % 53 % 0 / 0 1.6 % smooth masses, lumpy hooves
+ Multires Apply Base instead 4,372 0.11 % 15 % 0 / 0 1.6 % overshoot
Hybrid: eye rings + ear tubes + gentle fit 4,544 0.042 % 52 % 3,3 / 0 5.9 % first skill version
E1 run with the skill (fresh agent: face sets, warp, 4 eye rings, 5 lip rings, limb tubes) 5,778 0.040 % 51 % 4,4 / 5 (46 to 48 verts) 19.4 % planned face, rippled lids, lumps at leg joins
v2 tools, final (test_sheep_v2.py: face sets, counts as ranges, corner split, geodesic rings and tubes, nasolabial loop) 4,980 0.028 % 52 % 4,4 clean 24-vert rings, 12/12 / 5 x 30, 15/15 33.9 % best agent result: poles 5.7 % (Kaspar 5.2 %), none on clean rings, no 6-poles; nose and upper-lip patch irregular (human pass)

Numbers flatter the agent: an aggressive fit scored 0.043 % and looked lumpier than Kaspar's cage. What stays human-level only: articulation counts with handle loops, crease loops placed from expression shapes, pole routing and loop budgets out of the face. Say so in the report.

Numbers

Item Value Source
Render subdivision (film) 2 levels; viewport 1 Kaspar Live #6 [00:21:53]; BCON22 frame 00:26:38
Joint 3 loops over the bending part Kaspar Live #5 [01:37:48]
Limb start ring 8 verts (legs 8 then 12); Dikko arms 8 min, 10 to 12 typical, legs 18 Kaspar Live #4 [00:06:54]; Dikko [00:13:47]
Eyelid 2 loops per lid crease + ~4 across; 10/10 spans example Kaspar Live #1 [02:11:22]; Dikko [00:04:16]
Studio mesh 99.9 %+ quads, 0 to 4 valence 6+ (report and justify each), poles ~1.8 % head, ~5 % body, edge CV 0.33 to 0.8 calibration (bx_audit on Blender Studio files)
Auto-remesh hint (not a gate) edge CV ~0.17 to 0.27 on an unedited uniform remesh, near 0 % closed loops calibration, sheep
Game head 2,000 to 3,000 tris, hands ~1,000 both, body 5,000 to 6,000 SpeedChar ep23 [00:09:58], ep24 [00:50:34]
Apply Base levels 2 body, 3 head, hair, clothes Kaspar Live #5 [02:15:03]

Quality gates

Code (thresholds [added], calibrated on Kaspar's sheep: 0.24 % mean, 47 % inside, coverage p95 0.82 %):

retopo_gate = R.topology_report(retopo)
fit_gate = R.fidelity(retopo, sculpt, levels=2)
gate = {"six_poles_to_justify": retopo_gate["valence_6plus"], "no_rim_poles": retopo_gate["rim_poles"] == 0,
        "quads": retopo_gate["non_quads_visible"] == 0 or retopo_gate["quads_pct"] >= 99.0,
        "subd_fit": fit_gate["mean_pct"] <= 0.3 and 40 <= fit_gate["inside_pct"] <= 60,
        "coverage": fit_gate["cov_p95_pct"] <= 1.0, "loops": R.loop_stats(retopo)}
print(gate)

Edge-length CV is never pass/fail (studio meshes read 0.33 to 0.8); 6-poles are reported and justified, not banned (studio 0 to 4). Also: crease_poles == 0, center_poles == 0 on mirrored faces, R.loops_around(retopo, eye, normal, max_radius) returns at least 3 closed rings per eye and mouth, ring counts equal before every merge, triangles only in hidden or rigid areas, budget met (R.tri_count). Visual (bx_review, same views as the sculpt): cage in wire (rings close, no wandering loops, poles in still areas) and subdivided matcap (lumps, pinching at poles, creases held), then the cage alone: it must read without subdivision (Kaspar Finale [01:58:41]). Score with references/critique.md.

Common mistakes

Mistake Looks like Fix
Carving around QuadriFlow's closed eye slits every eye ring odd (47, 45), no all-quad fix R.parity_strip between the two odd boundaries BEFORE carving
Counts inherited from a dense cut, or forced far below it eye rings 46 to 48; or pole density 9.5 % count=(lo, hi): each reduction unit costs 4 poles; ranges took the sheep from 9.5 to 5.7 %
3D-nearest lookups where surfaces touch a band or socket cut bites an ear lying on the cheek geodesic fields, curve_verts, oriented nearest_vertex (built into bands and tubes)
Tight rings for an edge-flow loop a ridge after subdivision band spacing = edge length; tight only for creases
Shipping a snapped cage subdivided surface inside the sculpt (87 %) R.fit_subdiv
Shrinkwrap offset for volume cavities worse fit, offset 0
Raw quadriflow_remesh on a small dense mesh CANCELED, "needs to be manifold" R.quadriflow (scale trick, voxel fallback)
QuadriFlow symmetry left at its default True wrong half on a posed mesh symmetry=False unless symmetric
Relaxing an auto-remesh tips and ears shrink relax hand-built layouts only
Mark Sharp / Seam as reminders shading breaks (4.1+), unwrap cuts retopo_crease attribute (P5)
Grid fill on an odd loop FINISHED, 0 faces count first; R.grid_fill raises
Connecting parts early every new loop runs everywhere islands, count, merge last
Trusting mean error lumpy fit passes matcap review, gentle fit
Re-shrinkwrap after Apply Base volume lost again never (Kaspar Live #6 [00:20:08])
Game quads left non-planar lines in the normal map R.triangulate_twisted, Triangulate on export

Blender 5.2 notes

  • Hidden Wire is the Retopology overlay (overlay.show_retopology, offset 0.01 m since 4.5); "Project Individual Elements" is snap_elements_individual = {'FACE_PROJECT'}. GUI only.
  • Slide Relax is the Relax Slide brush asset (strokes need a window, bx_gui); headless use R.relax. F2, LoopTools, BSurfaces are extensions; mesh.circularize, space_edge_loops_evenly, flatten are built in; mesh.select_by_pole_count exists (4.4).
  • The BCON22 build's Shrinkwrap showed Smooth Factor 0.05 / Repeat 1 (frame 00:22:53); 5.2.1's Shrinkwrap has no such option.
  • sharp_edge is always honored since 4.1; Auto Smooth is gone (Smooth by Angle modifier).
  • Apply Base = object.multires_base_apply(apply_heuristic=True); QuadriFlow lost its shortcut and has the traps above.

References

  • references/procedures.md: tested code, P0 to P14 (inspect, face sets P1b, stack, tubes, geodesic limbs P3b, freeze/relax, annotations, QuadriFlow, map-line loops and rings, sockets, volume, checks, game, expression test, handoff, AI-mesh finishing). Load before writing any code.
  • references/expert-notes.md: principles by expert with timestamps, face and body loop maps, clothing, game rules, the sheep experiment in full. Load when planning a face, body, clothing or game mesh.
  • references/critique.md: the rubric to judge your own retopo. Load at every review.
  • references/sources.md: all 24 source videos with best timestamps.
Files (skills)
  • references
    • critique.md 15.2 KB
      # Retopology critique rubric
      
      How the experts judge a retopo, turned into checks an agent runs on itself. Run the code checks first, then render the two review sheets and walk the visual list in order. Score each section 0 (fails), 1 (works with visible problems), 2 (clean), and report every 0 or 1 with its cause. Kaspar's own summary of what matters: "is it clean, is it all quads, does it subdivide well, do you capture all of your forms and details, is it easy to rig ... vertex count has very little to do with that" (Snow Live #4 [00:58:52]).
      
      ## 0. Setup for the review
      
      ```text
      t   = R.topology_report(retopo, crease_attr="retopo_crease", hidden_group="hidden_ok")
      f   = R.fidelity(retopo, sculpt, levels=2, target_mask=<region you meant to cover>)
      ls  = R.loop_stats(retopo)
      eye = R.loops_around(retopo, eye_center, eye_normal, max_radius=2.6 * eye_radius, corners=(inner, outer))
      sheets: cage wire (front, right, threequarter, low) and the same views with a temporary
      Subsurf level 2 in matcap; the sculpt in matcap from the same views for comparison.
      ```
      
      Full, tested code: `procedures.md` P10.
      
      ## 1. Fit for purpose (budget and brief)
      
      | Check                                            | Pass                                                                                                                                                                                                                                         | Source                                               |
      | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
      | Face or triangle count vs the stated budget      | within budget; film: "the lowest necessary amount of loops"                                                                                                                                                                                  | Kaspar Live #4 [00:58:19]; SpeedChar ep23 [00:09:58] |
      | Density follows detail and deformation, not area | face denser than body (game ~3x), flat or hidden areas sparse; uniform density fails a character. Edge-length CV is a hint only, never pass/fail: an unedited uniform remesh reads about 0.17 to 0.27, studio meshes 0.33 (hand base) to 0.8 | SpeedChar ep23 [00:34:34]; calibration               |
      | Not over-dense                                   | "overly dense retopology" is the most common RetopoFlow problem                                                                                                                                                                              | Lampel [00:36:21]                                    |
      | Surface details in maps, not loops               | no loops chasing small bumps, pores, muscle striations on an animation body                                                                                                                                                                  | Jamie Dunbar [00:17:15]; Dikko body [00:32:28]       |
      
      ## 2. Flow (wire sheet)
      
      | Check                                   | Pass                                                                                                          | Source                                                |
      | --------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
      | Rings around each eye and the mouth     | at least 3 closed rings each (`loops_around`), concentric, no spiral                                          | Dikko [00:04:49]; Kaspar BCON22 [00:08:29]            |
      | Lid and lip parity                      | upper count = lower count (`corners=` in `loops_around`)                                                      | Dikko [00:04:16]; Kaspar BCON22 [00:46:37]            |
      | Articulation (film face rig)            | lids, lips, brows: 3 main loops with 2 handle loops each, 2 eye corner loops                                  | Kaspar BCON22 [00:12:25]                              |
      | Face frame, nasolabial loop, cheek grid | a loop framing jaw and forehead; one loop from over the nose around the chin; cheek a plain grid              | Dikko [00:02:34] [00:03:42]; Kaspar BCON22 [00:09:01] |
      | Joints                                  | 3 loops over each bending part (elbow, knee, wrist, finger joints)                                            | Kaspar Live #4 [00:08:01], Live #5 [01:37:48]         |
      | Body core (full characters)             | chest loop over the shoulder cap and hip loop exist; limb loops separated from torso loops                    | Dikko body [00:01:51] [00:26:03]                      |
      | Loops close or end in still areas       | `loop_stats` spiral suspects not rising after an edit; no loop wrapping a limb                                | Kaspar Live #7 [01:23:34]                             |
      | Limb midline straight                   | ring start vertices line up down the limb, no twist                                                           | Dikko body [01:28:03]                                 |
      | Loops out of the face are budgeted      | loops leaving the face end in the framing loop, the chin frame, a nostril or a hole, not in the neck and ears | Kaspar Live #2 [00:03:03] [00:41:44]                  |
      
      ## 3. Poles
      
      | Check                                                       | Pass                                                                                                                                                                                                                                                                         | Source                                                          |
      | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
      | Valence                                                     | only 3 and 5; `valence_6plus == 0`; `valence2 == 0` in the final mesh                                                                                                                                                                                                        | Kaspar Live #7 [00:11:41]                                       |
      | Placement                                                   | in low-movement areas (back of hand, between the collarbones, cheek), not near mouth corners' travel                                                                                                                                                                         | Kaspar BCON22 [00:43:49]; Live #5 [00:58:19]                    |
      | Never on a crease loop, a shell rim or the face center line | `crease_poles == 0`, `rim_poles == 0`, `center_poles == 0`                                                                                                                                                                                                                   | Kaspar Live #1 [00:36:27], Live #3 [00:36:54]; Dikko [00:04:49] |
      | No clusters                                                 | `adjacent_e5` low; no two diamonds touching (they make 6+ poles); report and justify every 6-pole (studio meshes carry 0 to 4)                                                                                                                                               | Kaspar Live #2 [00:53:13], Finale [01:04:16]; calibration       |
      | Pole budget and placement [added]                           | pole density near the studio 5 %; none on lid/lip rings or crease loops (clean rings of `carve_rings` carry 0 by construction; check `poles_on_clean_rings` as in `test_sheep_v2.py`); reduction units (4 poles each) and staircase cut borders are the agent's pole sources | sheep v2: 9.5 % forced counts, 5.7 % with count ranges          |
      | Map lines exist as loops [added]                            | every planned line (nasolabial, face frame, crease) has a real closed edge loop: `R.find_loop_near(obj, curve, tol)`; a ring carrying poles is not a loop (loop walks stop at poles)                                                                                         | sheep v2                                                        |
      | Flow loops do not read as creases [added]                   | band rings about one edge apart; three tight rings make a ridge after subdivision (Kaspar: the closer they hug, the sharper the crease)                                                                                                                                      | sheep v2, 3.5 mm rings on 12 mm faces                           |
      | Density                                                     | pole count / faces near the studio range (1.8 % head, ~5 % body)                                                                                                                                                                                                             | calibration                                                     |
      
      ## 4. Shape (subdivided matcap sheet vs sculpt)
      
      | Check                            | Pass                                                                                                                                                                                     | Source                        |
      | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
      | Subdivided surface on the sculpt | `mean_pct` <= 0.3 %, `inside_pct` 40 to 60 % [added thresholds, Kaspar's sheep: 0.24 %, 47 %]                                                                                            | Duha [00:12:24]               |
      | Coverage                         | `cov_p95_pct` <= 1 % over the region you meant to build (missing limbs and bridged-over cavities show here)                                                                              | [added]                       |
      | Creases held                     | eyes, lips, nasolabial, chin, brow, ear helix, knuckles crisp after subdivision; crease loops centered in the crease                                                                     | Kaspar Live #1 [02:13:08]     |
      | No lumps or pinching             | no ripples around poles on curved areas; no lumps on hooves, fingers, mouth. Numbers do not catch this: an aggressively fitted cage scored 0.043 % and looked worse than Kaspar's 0.24 % | Lampel [00:21:18]; sheep test |
      | Volume                           | no shrunken limbs (`inside_pct` > 55 is shrink), no overshoot (< 40)                                                                                                                     | Kaspar Live #6 [00:14:27]     |
      | Reads unsubdivided               | the cage alone still shows the character; this is what riggers and animators see                                                                                                         | Kaspar Finale [01:58:41]      |
      | Silhouette (game)                | low-poly silhouette matches the high from the game camera; round parts not faceted                                                                                                       | SpeedChar ep24 [00:48:51]     |
      
      ## 5. Hygiene
      
      | Check                    | Pass                                                                                                                                              | Source                                                  |
      | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
      | Face types               | `non_quads_visible == 0` (tris and n-gons only in the `hidden_ok` group); film: all quads                                                         | Kaspar Finale [00:12:53]                                |
      | Manifold, normals, loose | non-manifold 0 except intended holes; flipped 0; loose 0; duplicates 0                                                                            | Lampel [00:36:54]                                       |
      | Self-intersections       | no worse than the input's own contacts (Kaspar's sheep cage: 177 face pairs, where ears touch the head)                                           | calibration                                             |
      | Mirror seam              | every center vertex at x = 0, no open edges on the seam after applying Mirror                                                                     | CG Boost [00:13:01]                                     |
      | Handoff                  | modifiers applied except Subdivision, transforms clean, `GEO-` names, a material per object, annotation attributes removed, reused parts stripped | Kaspar Finale [01:56:19], Live #6 [01:37:02]            |
      | Separate parts           | clothing derived from the body topology; no intersections even where the body is hidden; inner band kept at openings                              | Kaspar Live #6 [01:44:44], Finale [01:47:58] [01:49:42] |
      
      ## 6. Game-specific
      
      | Check                    | Pass                                                                                          | Source                                       |
      | ------------------------ | --------------------------------------------------------------------------------------------- | -------------------------------------------- |
      | Every edge has a job     | silhouette, shadow, material split or UV seam, else dissolve                                  | Lampel [00:34:32]                            |
      | Triangulation controlled | no twisted quads left (`triangulate_twisted`), Triangulate modifier applied on export         | SpeedChar ep35 [00:10:18]; Lampel [00:12:45] |
      | Deforming zones          | quads and no triangles at knuckles, elbows, knees, mouth, eyes; triangles fine on rigid parts | SpeedChar ep24 [00:00:32], ep26 [00:10:28]   |
      | Proud of the high        | low poly slightly outside everywhere (`push_outside`)                                         | SpeedChar ep35 [00:21:30]                    |
      | Hard edges               | every hard edge is a UV seam                                                                  | On Mars 3D [00:08:04]                        |
      | Cavities                 | deep concavities (ear bowl) have geometry, not only the normal map                            | SpeedChar ep25 [00:39:02]                    |
      
      ## 7. Honesty of the report
      
      State the method per part (planned, auto, carved, socketed tube), the numbers from sections 1 to 5, the two sheets, and what was not done to expert level (articulation handles, expression creases, pole routing). An auto-remesh sold as "animation-ready" fails this section regardless of its numbers.
      
    • expert-notes.md 35.4 KB
      # Expert notes: retopology
      
      Principles and judgment by expert, each with its source video and timestamp (video ids in `sources.md`). `[added]` marks my own additions or measurements; everything else is the expert's. Snow = Julien Kaspar's "Snow - Stylized Character Retopology Live" series (#1 sXhIz-5g3Gc, #2 tRqCeWZLqQo, #3 7xMRyBHeDBo, #4 KWNcQZQkyjc, #5 8ljpBjgIST0, #6 8UqpNwkTEXA, #7 EBet5sIZcKY, Finale FnTOVcPaWV8).
      
      ## 1. Julien Kaspar (Blender Studio): film retopology
      
      ### 1.1 The requirement map (BCON22, OLwtul3ngB8)
      
      - Retopology is a puzzle with many valid answers; once requirements are known "it's hard to really go wrong" [00:04:37]. "Good" means the sculpt's forms read in both the subdivided and low version, loops are easy to select for riggers, and the mesh is light [00:03:31].
      - Do expression tests before retopology (sculpted on temporary topology) to learn where creases form and how far features move [00:06:14].
      - The map is drawn as Grease Pencil layers named, in his file, `edge flow`, `creases primary`, `creases secondary`, `articulation`, `expression bounds`, `landmarks` (layer panel visible in every frame from 00:00:45). What the frames show:
        - **Edge flow** (yellow, 00:07:53): a ring around each eye, a ring around the nose, rings around the mouth, the nasolabial loop running from over the nose around the mouth to the chin, a face frame from the forehead down the jaw, a ring around the ear.
        - **Primary creases** (cyan, 00:09:23): the lid creases around both eyes, the nose wing, the lip outline. Modeled into topology so they stay crisp [00:09:01].
        - **Secondary creases**: only in expressions; each needs one loop on it plus two proximity loops the rig can squeeze [00:10:11].
        - **Articulation** (purple, 00:10:53 and 00:11:38): short strokes across each lid (start, middle, end), across the lips, across each brow, with small boxes on the brow marking the control points. Tindall's model: each feature is a curve with 3 controls and 2 handles per control; eyes add 2 corner loops splitting top from bottom [00:11:52] [00:12:25].
        - **Expression bounds** (red, 00:13:08): one large loop from under the nose over both cheeks around the chin: how far the mouth corners must travel. Poles and loop direction inside it limit that travel [00:13:32].
        - **Landmarks** (magenta, 00:13:53): the eyebrow shapes outlined on the forehead; areas where a separate object (brows, horns, an ear tag) must move with the skin get matching topology [00:14:05].
      - Style calibration (frames 00:14:38 to 00:20:38): Snow's retopo under all six layers; Rex (Sprite Fright) with a huge mouth ring, bounds covering the whole lower face and poles only in the cheek, no secondary creases because 2D creases were added by another system [00:17:26]; Anar (Heist, realistic) with rings and radial articulation strokes but a generic, fairly even grid, no creases, no landmarks [00:18:35]; a generic sculpting base mesh with even quads that can simply be shrinkwrapped onto a realistic sculpt [00:20:16].
      - Rain's head colored by face sets per patch (00:05:17): purple eye masks, pink nose and mouth, orange nasolabial area, green cheeks, blue skull: patches are the unit of design. Seams (red) mark patch borders and articulation loops, sharps (cyan) mark crease edges, as reminders (frames 00:00:45, 00:04:32, 00:15:23).
      - Snow uses about half the polygons of Rain for the same requirements because its patches are tailored to its chin, ears and boxy forehead [00:15:11].
      - **Live demo on the sheep character this skill is tested on** (retopo_demo.blend): stack Mirror (X, Clipping, Merge 0.001 m, frame 00:27:23), Shrinkwrap (Target Normal Project, Snap Mode Above Surface, Offset 0.001 m, plus "Smooth Factor 0.050, Repeat 1", frame 00:22:53, an option absent from 5.2.1), Subdivision (Catmull-Clark, viewport 1, render 2, Optimal Display on, 00:26:38), Solidify last (Simple, negative thickness, Offset -1, Rim Fill, 00:34:08) only to hide back faces. He starts with an isolated eye ring with radial spokes and marks seams on it (00:28:08 to 00:29:38), then the mouth and nose island (00:31:08), keeps islands separate [00:36:28], relaxes in Sculpt Mode with the Topology-type brush (00:39:23), and adds crease loops terminated by diamonds on the brow (00:42:23 to 00:43:08).
      - Build rules from the demo: start with eyes and mouth [00:27:00]; keep top/bottom counts locked with a temporary 2-pole (select two verts, J, subdivide the new edge) [00:28:40]; do not push vertices early, jagged is fine [00:32:45]; proximity loops come later [00:35:12]; spiraling into the nose is acceptable [00:37:05]; treat Tindall counts as a minimum [00:37:37]; contain crease loops locally: bevel the crease edge, add a center loop, merge and collapse into a diamond [00:44:59]; poles where there is little movement, never on a crease [00:43:49]; cheeks one big square patch [00:48:18]; retopologize eyes in their most common state (he leans half-open) and hand over open/closed shape keys [00:47:10]; "even if you go a bit too high or a bit too low ... it doesn't quite matter" as long as key features are captured [00:49:56].
      
      ### 1.2 Setup and maintenance (Snow)
      
      - Retopologize the most final sculpt (the expression-test head) [#1 00:06:26].
      - Stack: Mirror (clip, on cage) > Shrinkwrap Target Normal Project, On Surface (he used Project before, "but it requires more setup") > Subdivision in object mode only [#1 00:11:32] [00:12:09]. Apply location and rotation so the mirror plane is world X [#1 00:29:49] (the agent equivalent: give the cage the sculpt's matrix [added]).
      - "Duplicate and apply" the Shrinkwrap regularly so real vertices sit on the surface; also before quitting [#1 01:31:00] [02:15:58]; twitching in Sculpt Mode means it is time [#5 00:13:09].
      - One retopo object over two sculpts: two Shrinkwraps restricted by vertex groups [#5 00:56:06]. A "shrinkwrap" vertex group masks the inner mouth, lids and nostrils at 0; every new skin vertex must get weight 1 or it stops wrapping [#2 02:02:06] [#3 00:16:26]. A copy with the wrong wrap method makes vertices jitter [#3 00:19:30].
      - Relax across the mirror seam: duplicate and apply the Mirror, keep a live Mirror with Bisect, mesh X symmetry on [#2 00:30:36].
      - Clothing shells snap to their own inner side: pre-offset outward, then Project, both directions, Above Surface, Limit [#7 00:03:56] [00:48:52].
      - Vanilla Blender only; F2 the one add-on [#1 00:16:11].
      
      ### 1.3 Face (Snow #1, #2)
      
      - Plan order: edge flow, articulation counts, creases (look at the expression shape keys for secondary creases), then poles [#1 00:12:49] [00:33:07].
      - Articulation for mouth and eyes: a middle, left, right and corner loop, each flanked by two handle loops; "enough loops to pull off any shape you want" [#1 00:18:34] [00:19:41]. [added arithmetic: about 9 radial edges per lip between corners.]
      - A 2-pole at the mouth corner turns the upper-lip loop back into the lower lip so every added loop circles the mouth; ripped open later for the inner mouth [#1 00:22:38] [#2 01:55:24].
      - Block the eye as if closed (temporarily filled) so lid loops stay symmetric [#1 00:27:00]. Eyelids: 2 loops per lid crease plus about 4 across the lid so the closed eye keeps volume; lids are thick, extrude inward [#1 02:10:46] [02:11:22].
      - Crease = center loop + two framing loops; "the closer they hug this edge the sharper the crease" [#1 00:34:37] [02:13:08]. A brow crease only needs cross-hatching loops [#1 01:15:06].
      - Poles never on creases, on flat surface, framed by loops; 6-edge pole "horrendous", split into two 5-poles [#1 00:36:27] [00:51:45].
      - Loop sinks: send surplus loops into the nostril or an unseen hole [#1 01:00:34] [#2 01:14:39]; never add loops into a finished mouth or eye [#2 00:52:35].
      - Eyebrow landmark: skin loops under the brow match the eyebrow mesh loop for loop [#1 01:05:17].
      - Square beats uniform: rectangles are fine if cage and subdivided shapes agree [#1 00:58:44]. Never subdivide the retopo to gain resolution [#1 01:26:04]. Head density deliberately high for rig control [#1 01:22:16].
      - Loop budget out of the face: merge exiting loops into the face-framing loop (two fewer each time), frame the chin with rerouted loops, turn loops back before the chest [#2 00:03:03] [00:41:44] [00:45:54].
      - Poles aligned into patches when cheap ("my own personal little OCD") [#2 00:12:57]; separate adjacent 5-poles [#2 00:53:13].
      - Rigor scales with deformation: exact counts at mouth and eyes, ears only need non-spiraling loops, hidden ear parts may be messy [#2 01:33:38] [01:38:30].
      
      ### 1.4 Hair shells, eyes, reuse (Snow #3)
      
      - Stylized hair stays a mesh; loops follow each clump's lines; start at "game asset low poly" minimum, every loop precious [#3 00:57:06] [01:03:11].
      - A bulge needs at least one loop or the subdivided shell collapses [#3 01:05:50]; hard edges between volumes by bevel (two close edges crease) [#3 01:09:41]; lay out everything before beveling [#3 01:18:27].
      - No poles on a shell's open rim; Solidify Only Rim for edge thickness [#3 00:36:54] [00:34:38]. Do not mirror hair clumps [#3 01:42:45].
      - Cornea, not the inner eye, touches the lids [#3 00:44:18]. Teeth, tongue, eyes reused from Rain [#3 00:37:34].
      - Triangles and n-gons: beginners never; experts where unseen or non-deforming, knowing why [#3 01:29:54]. Games triangulate before baking because normal maps depend on the exact triangulation [#3 01:51:35].
      
      ### 1.5 Limbs, hands, body (Snow #4, #5)
      
      - Separate objects per part; connecting early makes every loop run through arm, fingers and hand [#4 00:07:28].
      - Arm from an 8-vertex circle centered with the cursor snapped to Volume, "go as low as possible" [#4 00:06:16]; 3 loops per joint minimum [#4 00:08:01]; leg cylinder 8 then 12 [#5 01:21:00] [01:22:09]; knee inset twice so the sharp edge sits between two loops [#5 01:26:56].
      - Fingers from a cube, 3 loops per joint plus a nail loop, linked duplicates (Alt+D) so one edit updates all, thumb a real copy [#4 00:29:17] [00:35:53].
      - Ring counts at connections: wrist 10 vs hand 16, "six too many"; hand 18 vs arm 11 reduced by looping two loops back into each other; leg 12 vs body 14; foot 27 vs leg 14 [#4 01:27:33] [#5 00:33:59] [01:45:15] [02:00:22]. Diamonds terminate loop pairs on the back of the hand, the part that moves least [#4 01:30:37].
      - Put terminations and density where nothing moves but everything around moves (between the collarbones) [#5 00:58:19]. Muscles under clothes: "just a tube with a bit more definition" [#2 00:48:03].
      - His criteria: clean, all quads, subdivides well, captures forms, easy to rig; vertex count irrelevant [#4 00:58:52]; must also work unsubdivided [#4 01:09:22].
      - Merge: Ctrl J, Bridge Edge Loops, slide one loop into the other with Auto Merge [#5 02:06:31].
      
      ### 1.6 Volume (Snow #5, #6)
      
      - Shrinkwrapped low topology loses volume under subdivision [#5 02:08:30]. Apply Base trick: Multires subdivided 2 (body) or 3 (head, hair, clothes) times, Shrinkwrap after it applied onto the levels, Apply Base at level 0, swap for Subdivision [#5 02:09:01] [02:15:03].
      - It overshoots and depends on density ("the apply base ... doesn't consistently add too much thickness") [#6 01:00:06]; follow with a wire-display deflate pass until the wire just touches the skin [#6 00:14:27]; never re-shrinkwrap after it [#6 00:20:08]; stop snapping after the volume pass [#5 02:14:31].
      - Rebuild distorted sharp edges by dissolving and re-adding straight loops [#6 00:25:33]; leave rest creases softer so the rig can sharpen them [#6 00:39:00]; small lip gap, no overlap [#6 01:14:40].
      
      ### 1.7 Clothing (Snow #6, #7, Finale)
      
      - Clothing starts as a duplicate of the body retopo so loops match and weights transfer one to one [#6 01:33:34] [01:44:44] [Finale 01:51:51].
      - Folds: one loop per peak and valley; bevel a loop into two close loops for a crease, bevel twice to fan into a soft fold; diamonds split one loop into three and rejoin but stretch, so keep their ends off creases [#7 00:07:46] [00:08:54] [00:10:33] [00:33:42].
      - Pose-independent folds modeled into the cage so animators see them unsubdivided [Finale 00:27:25]. Triangles in pairs so they merge into quads [Finale 00:28:32].
      - A 6-edge pole becomes 5, 3, 5 by extending an edge [#7 00:11:41]. Split an object for partial asymmetry, Mirror on the symmetric part [#7 00:32:03].
      - No intersections even where the body is hidden [Finale 01:47:58]; keep an inner band at openings (Rain's clothes looked glued without) [Finale 01:49:42].
      
      ### 1.8 Handoff and effort (Finale, #6)
      
      - Modifiers applied, location/rotation zero, scale 1, a material per object, `GEO-` names; the character must read with Subdivision hidden, "the subdivision that the riggers, the animators are going to use" [Finale 01:56:19] [01:58:41] [#6 01:37:02]. Rig-prep shape keys (eyes open/closed, pupil dilation) belong to retopology [#7 01:50:55].
      - Time: about 2.5 working days for Snow; "a week at least" is normal for a main character [Finale 00:01:16] [#5 01:34:22]. Tools will not solve the puzzle: "you still have control over where your poles are" [Finale 00:30:46]; semi-automatic is the likely future [#4 01:05:18].
      
      ## 2. Jonathan Lampel (BCON24, PAK5zQONgvs): why topology behaves
      
      - Pinching comes from differences in vertex density in the subdivided result: a 3-pole makes a dense spot, a 5+-pole a sparse one; harmless on flat areas, harmful on curved or deforming ones [00:20:44] [00:21:18]. "All quads for subdiv" really means "watch your poles"; an n-gon under subdivision makes one N-pole plus 3-poles around it [00:19:39].
      - Quads are the best default; triangles and n-gons are tools when intentional: tris are efficient and a triangle wedge can keep a joint's outer volume [00:10:34] [00:14:22]; n-gons are fluid and make good temporary blockers so loop cuts do not run around the whole model [00:16:30]; deformed n-gons re-triangulate and flip shading [00:17:37].
      - See the density you create: wireframe at reduced opacity, Subsurf Optimal Display off [00:23:00]; n-gons and tris then redirect loops with a light cage [00:24:08].
      - Holding edges: loops; a Bevel modifier above Subsurf with even segments and Profile 1.0 (attribute-driven since 4.3); creases for background objects (little change between 0.5 and 1, then a pop); double smooth (Subsurf with creases at level 1, then Subsurf without creases at level 2) [00:26:29] [00:29:12] [00:30:34].
      - Junctions to step density down: 3-to-1 for odd counts, 2-to-1 for even, a corner variant; face loops running to a tail make it boxy [00:32:18] [00:32:50].
      - Low poly: every edge supports the silhouette, casts a shadow, divides materials or is a UV seam, else delete it [00:34:32]. Triangulate before exporting to a baker [00:12:45]; Flip Quad Tessellation keeps the quad but changes the split [00:12:13].
      - Clean-up list: non-planar and concave quads, long thin faces, overly dense geometry (the most common problem in RetopoFlow bug files), non-manifold, inconsistent normals, loose parts, internal faces [00:35:01] to [00:37:26]. Watertight is not a game requirement [00:37:59].
      - Problem solving: push problems into flexible areas (n-gons, boundaries, the symmetry axis, flat or still areas) [00:40:04]; stash a duplicate before rebuilding so curvature survives [00:42:14]; delete and start over, his best models were rebuilt 3 to 10 times [00:44:08]; mask unavoidable lumps with transferred normals from a clean duplicate [00:46:27].
      - Automatic retopology works for static objects, not deforming characters or cloth, because topology is deciding what matters [00:09:05].
      
      ## 3. Vilem Duha (BCON23, 7AR9-LxY6AQ): fit the subdivided surface
      
      - "Everyone is snapping the vertices to the surface ... it's kind of old school" [00:07:18]: with subdivision the surface shrinks inside convex areas [00:07:52]. A Shrinkwrap offset hides the error outside and makes cavities worse [00:08:24].
      - Place control vertices so the subdivided surface fits the sculpt; moving one vertex moves its neighbors' limit points, so solve iteratively like a cloth simulation [00:08:58] [00:11:15]. The remaining error shows where loops are missing or wasted [00:11:48]. Low poly also benefits from vertices slightly outside [00:09:32].
      - Fluent manual retopo of a complex character takes 1 to 2 hours; auto tools like QuadriFlow are for static assets [00:07:18].
      - [added] Implemented as `R.fit_subdiv`: limit positions of cage vertices from a level-1 limit-surface Subsurf (base vertices come first in the evaluated mesh; on the sheep QuadriFlow cage the limit point sat 0.08 edge lengths from its vertex on average, 1.0 at most), nearest sculpt point, neighbor-smoothed and clamped moves.
      
      ## 4. Dikko: face map (SwM19PgSdCM) and body layout (3WnW_ZFXudk)
      
      - Plan before building; the analysis matters more than the build steps [face 00:00:55] [00:33:01].
      - Five 5-poles around each eye mark muscle direction changes: nose bridge between the eyes, outer eye corner, middle of the cheek, where the nose meets the cheek, upper cheekbone; they form a pentagon mask [face 00:01:28]. A similar star near the jaw-mouth junction [00:02:02]; a 5-pole at the top corner of the upper lip [00:06:25].
      - Equal spans top and bottom of lids and lips, per quadrant (example 10/10, 3 per quadrant), non-negotiable for rigging; exact counts are circumstantial [face 00:03:42] [00:04:16]. Spiraling eye or lip loops mean rebuild; the face center line is one loop with no poles [00:04:49].
      - One loop over the nose around the chin (nasolabial) nests the lip loops; the cheek is the only plain grid; a loop frames jaw and forehead [face 00:02:34] [00:03:42]. Extrude lips as a loop, never a square [00:31:36]. Mouth bag toward the throat keeps lip volume [00:41:46]. Splay eyelid thickness edges instead of merging into a star [00:40:30].
      - Shrinkwrap does not work for ears, eyes and mouths; model ears by eye against orthographic references; ear rebuilt three times; identical spans each side of the ear hole make Grid Fill work [face 00:22:02] [00:34:06] [00:35:13].
      - Neck: redirect extra front edges around the neck base (a throat area), collapse at the back (merge 3, delete 2) [face 00:48:38] [00:49:12]; few vertical neck spans [body 01:01:12]; front and back neck density should match [body 01:08:30].
      - Body core loops first: chest loop under the breast or pectoral, over the shoulder cap, around the back; hip "bikini line" loop to the small of the back with a pole at the hip [body 00:01:51] [00:02:55] [00:04:37].
      - Arm spans at least 8, 10 to 12 typical, 14 on this character; legs 18; bevel the loop at the pole to separate limb loops from torso loops; forearm twist topology unnecessary with twist bones [body 00:13:47] [00:26:03] [00:27:47] [01:16:41].
      - Hip cap like a shoulder cap for buttock and thigh crease; no front reduction at a female crotch (bulge); no abs or back muscles on an animation body [body 00:33:35] [00:09:01] [00:32:28]. Route every added span to where it can end; break the neck temporarily so body spans do not offset eyelid counts [body 00:31:22] [01:09:39]. Prefer collapsing to adding [body 01:04:04]. A triangle at the back of the head under hair is fine [body 01:17:14].
      
      ## 5. Nikolay Naydenov (SpeedChar, Gameloft): game character low poly
      
      - Budget by detail and camera, not volume: head 2,000 to 3,000 tris (final 2,600), body 5,000 to 6,000, both hands about 1,000, typical character under 10,000, this one just under 12,000 with weapon; face about 3x body density [ep23 00:09:58] [00:34:34] [ep26 00:33:24] [ep24 00:50:34] [ep35 00:06:30].
      - Surround only forms that make the silhouette; small lips and creases go to the normal map [ep23 00:34:02] [ep24 00:05:08]. Loops only where animation needs them: mouth ring, eye ring plus one under-eye loop for blinks, finger bends and knuckles, a small polygon between fingers [ep25 00:10:24] [ep24 00:25:32].
      - Triangles are fine wherever nothing bends; slivers collapse to triangles; cut quads yourself where the automatic split would be wrong [ep24 00:00:32] [00:03:06] [00:11:06] [ep26 00:10:28].
      - Explode parts along one axis before baking so nothing bakes into anything else [ep23 00:01:06]. Edit the high poly without remorse to help the low (close gaps, symmetrize the high when the low is mirrored) [ep26 00:02:17] [00:48:55]. Deep cavities need low-poly geometry [ep25 00:39:02].
      - Very low poly: skip the Shrinkwrap, snap to faces and fit with a sculpt pass [ep23 00:24:13]. The fitting pass before UVs: In Front off, low in wireframe, Inflate until slightly proud; triangulate twisted quads along the diagonal you choose [ep35 00:10:18] [00:18:44] [00:21:30]. Perfect bakes do not exist at this ratio [ep35 00:22:37].
      - Front matters more than back unless the camera sits behind the character [ep26 00:04:25]; keep the face symmetric, split asymmetric bits into separate objects [ep25 00:04:29].
      - Hard edges vs UV splits were never discussed (he shades all smooth); the baking rule "every hard edge is a UV seam" comes from On Mars 3D [C_RqdNbYOjE 00:08:04] and Polycount (see scenario-blender-uv-baking).
      
      ## 6. Setup and confirmations (Jamie Dunbar, CG Boost, Grant Abbitt, Ryan King, Kenny)
      
      - Jamie Dunbar: Shrinkwrap failure modes: inward normals, one projection direction only, stale positions (popping), offset distortion, subdiv interference; fixes: recalc normals, both directions, duplicate + apply, recreate from scratch, no Subdivision while building [l-sALvdn3FI 00:05:22] [00:06:17] [00:15:09] [00:22:19] [00:22:50]. Do not retopologize lumps and bumps [00:17:15]. Auto Merge threshold scales with the model (0.01 for a 3 m character) [00:11:05].
      - CG Boost: snapping handles hand-moved vertices, the Shrinkwrap catches operator-made geometry (loop cuts) [X2GNyEUvpD4 00:08:04]; face snapping projects from the view [00:07:02]; important loops first, unconnected, low resolution [00:09:36]; finalize: apply Shrinkwrap and Mirror, check center merges [00:13:01].
      - Grant Abbitt: 4 M-triangle dragon head to about 3.5k tris for a game [6sE-p8PcHDo 00:00:31] [00:02:07]; apply rotation before mirroring [00:04:19]; auto retopo (Quad Remesher) gave "no edge flow around the eye or mouth and far too many faces under the nose", fine for bake-and-paint only [C249AnzAI40 00:00:32]; poles appear where two flows meet [00:05:22].
      - Ryan King: the Mirror modifier can make a click select the mirrored back vertex [1myOZaxtHes 00:07:37]; a triangle beats a 5-gon [00:14:04]; put an edge on the crease or the detail flattens [00:15:41]; back up before applying [00:18:39].
      - Kenny: five flows drawn first (eye, mouth, ear rings, nose bridge to chin, forehead down the jaw over the chin), same counts top and bottom, relax after every block [7XdbJX8roCg 00:00:35] [00:03:51] [00:11:00].
      
      ## 7. Ground truth: Blender Studio production meshes (bx_audit, calibration file)
      
      | Mesh              | Faces  | Quads   | e3 / e5+ poles | valence 6+ | edge CV |
      | ----------------- | ------ | ------- | -------------- | ---------- | ------- |
      | Rex head          | 3,890  | 99.90 % | 38 / 31        | 3          | 0.805   |
      | Snow body         | 9,402  | 99.96 % | 250 / 258      | 4          | 0.765   |
      | Rain body         | 16,526 | 99.98 % | 241 / 247      | 2          | 0.619   |
      | Rain face demo    | 1,653  | 100 %   | 6 / 22         | 0          | 0.600   |
      | Franck sheep body | 2,596  | 99.92 % | 64 / 72        | 0          | 0.491   |
      
      Takeaways: studio topology is 99.9 %+ quads; 3- and 5-poles come in near-equal numbers; valence 6+ rare but not zero; density is deliberately uneven (edge CV 0.33 on the hand base, 0.49 on the sheep, up to 0.8) while an unedited uniform auto-remesh reads about 0.17 to 0.27: a hint of automation, never a pass/fail gate; even studio meshes ship small defects (Snow: 4 non-manifold edges).
      
      ## 8. Disagreements and the deciding condition
      
      | Topic                  | Options                                                                                                                                                                                                             | Decide by                                                                                                                     |
      | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
      | Shrinkwrap wrap method | Target Normal Project (Kaspar Snow; also the panel in his BCON22 demo file, frame 00:22:53, though he says "Project" at [00:26:26]); Project both directions + Above Surface (Jamie); Nearest Surface Point (Dikko) | skin: Target Normal Project; clothing or double-sided: Project + Limit; Project leaves vertices whose ray misses in place     |
      | Loop density           | clean dense generic (Rain) vs minimal tailored (Snow, half the faces)                                                                                                                                               | realistic or generic style: generic base; strong stylized design: tailor (Kaspar)                                             |
      | Sharp edges            | loop cuts placed by hand vs Bevel                                                                                                                                                                                   | face: loop cuts (bevel destroys annotations); hair, limbs: bevel for speed (Kaspar #1, #3)                                    |
      | Hard edges             | supporting loops vs crease weight                                                                                                                                                                                   | loops by default; crease for background objects or double smooth (Kaspar #5 01:41:16; Lampel)                                 |
      | Triangles              | never (beginners, visible subdivided skin) vs freely (rigid, hidden, game)                                                                                                                                          | does it deform and is it seen (Kaspar, SpeedChar, Lampel, Dikko)                                                              |
      | Spirals                | rebuild (eyes, lips) vs tolerated                                                                                                                                                                                   | articulation rings must close; a spiral ending in a still area is fine (Dikko, Kaspar)                                        |
      | Exact loop counts      | Tindall minimum (Kaspar) vs circumstantial (Dikko)                                                                                                                                                                  | rigged expressive faces: use the minimum; always keep top = bottom                                                            |
      | Surface detail         | topology (stylized film) vs displacement or normal map                                                                                                                                                              | anything in the silhouette must be geometry; concave detail may be a map (Kaspar #7 01:15:10)                                 |
      | Relax                  | Relax Slide brush vs nudging                                                                                                                                                                                        | relax by default; near lids and deliberately placed loops nudge (Kaspar #6 00:26:41)                                          |
      | Volume fix             | Apply Base vs manual deflate vs Displace modifier                                                                                                                                                                   | Apply Base for speed, then a manual pass; Displace for a uniform offset (Kaspar #6); headless: `fit_subdiv` [added, measured] |
      
      ## 9. The sheep experiment: what an agent reaches (tests/code/blender-retopology/test_sheep.py)
      
      Setup [added]: input `sheep_sculpt.blend` (186,464 faces, posed, 21 % local symmetry, two 144-edge eye holes, open smile with fused teeth, rotated 27 degrees about Z). Kaspar's retopology covers the sculpt without its tail (sculpt area 1.015 vs 0.606 in his region), so the agent budget was area-matched: 2,596 x 1.015 / 0.606 = 4,347 faces. The reference was used only for numbers and a coverage mask. Metrics from `R.fidelity` (subdivision 2, % of the 0.79 m size) and `R.topology_report`.
      
      | Strategy                    | Faces | Mean  | p95  | Max  | Inside | Coverage p95 | Eye rings | Closed-loop edges | Spiral suspects | Self-int. |
      | --------------------------- | ----- | ----- | ---- | ---- | ------ | ------------ | --------- | ----------------- | --------------- | --------- |
      | Kaspar (reference)          | 2,596 | 0.24  | 0.80 | 2.58 | 47     | 0.82         | 5, 5      | 29.9 %            | 8               | 177       |
      | A QuadriFlow                | 4,372 | 0.14  | 0.40 | 3.19 | 87     | 1.75         | 0, 0      | 1.6 %             | 24              | 124       |
      | B A + relax                 | 4,372 | 0.14  | 0.44 | 3.19 | 87     | 2.22         | 0, 0      | 1.6 %             | 24              | 100       |
      | C B + aggressive fit        | 4,372 | 0.038 | 0.19 | 3.01 | 51     | 2.12         | 0, 0      | 1.6 %             | 24              | 183       |
      | C2 B + gentle fit (default) | 4,372 | 0.045 | 0.21 | 3.07 | 53     | 2.15         | 0, 0      | 1.6 %             | 24              | 158       |
      | D B + Apply Base            | 4,372 | 0.11  | 0.25 | 3.01 | 15     | 2.03         | 0, 0      | 1.6 %             | 24              | 169       |
      | G hybrid, aggressive fit    | 4,543 | 0.043 | 0.21 | 3.54 | 50     | 1.56         | 3, 3      | 7.2 %             | 19              | 393       |
      | G2 hybrid, gentle fit       | 4,544 | 0.042 | 0.20 | 2.08 | 52     | 1.59         | 3, 3      | 5.9 %             | 22              | 178       |
      
      Findings [added]:
      
      1. Raw `quadriflow_remesh` refused the manifold sculpt: edges under about 1e-4 fail its manifold check. Scaling the copy to about 10 units fixes it. It landed 14 % under target (corrected by rerunning), closed the eye holes, and is not guaranteed to repeat: the sheep runs repeated exactly, but a unit-test sphere gave socket borders of 20, 22 and 24 edges across three runs. Re-measure after every remesh, never reuse counts from an earlier run.
      2. QuadriFlow density is uniform: the tail, which Kaspar did not even retopologize, took a large share of faces while face, hooves and ears stayed coarse; the thin right ear tore into spikes.
      3. Relaxing a QuadriFlow mesh did not help and shrank extremities (coverage p95 1.75 % to 2.22 %).
      4. The snapped cage's subdivided surface was 87 % inside the sculpt. `fit_subdiv` brought it to 51 to 53 % inside and cut mean error 3 to 4x; Apply Base overshot to 15 % inside, as Kaspar warns.
      5. The aggressive fit crumpled the cage at features too small for its density (hooves, mouth) and doubled self-intersections; the clamped, smoothed fit kept the same mean error with a lower max and the expert's level of self-intersections. The gentle values are now the defaults.
      6. Carving rings at the eyes gave 3 closed rings per eye (Kaspar: 5); a projected ellipse at the open, teeth-filled mouth made that area lumpier, so G2 leaves the mouth to QuadriFlow and the report says so.
      7. Rebuilding both ears as tubes socketed into the auto base removed the tear and improved coverage (p95 2.3 % to 1.6 %).
      8. Every agent variant beats Kaspar's mean error and none looks as good: lumps on hooves and wrists, pinching at joints, a less clean face, 5.9 % of edges in closed loops against 29.9 %. Mean distance rewards chasing sculpt noise; the matcap and wire sheets decide.
      9. What stays out of reach procedurally: the articulation template with handle loops, crease loops placed from expression shapes, loop budgeting out of the face, pole placement by movement, the eyebrow landmark. These need the map (Kaspar's six layers) turned into explicit rings, which the building blocks support but the agent has to plan part by part.
      
      ### 9.1 The v2 tools, fresh end-to-end run [added] (`test_sheep_v2.py`, `compare/compare.md`)
      
      - Base: QuadriFlow 3,600 faces with a head density warp of 1.4 (1.8 and a 4,000 base forced more reductions and left a leg socket unbuilt; no warp raised poles and coverage error).
      - Face sets read as the landmark map (lip line = mouth-opening set border, eye masks, ears, hooves, tail pieces); limbs and the 1.05 m curled tail as geodesic socket tubes with automatic roots (0 to 2 cm from a person's reading) and 3 rings per detected joint.
      - Eyes and mouth with corner-split, geodesic rings and counts as ranges: eyes 24 (12/12), mouth 30 (15/15) on every clean ring; a nasolabial loop around nose and mouth; the closed-mouth variant gives exactly two 2-poles at the lip corners.
      - Result: 4,980 faces, 100 % quads, 0 six-poles, poles 5.7 % of faces (Kaspar 5.2 %, E1 4.6 %), 0 poles on clean rings, subdivision-2 mean error 0.028 %, coverage p95 0.53 % without the teeth (E1 0.77 %, Kaspar 0.78 % in his region), closed-loop edges 33.9 % (Kaspar 29.9 %). Subdivided, the eyes lose E1's rippled lids.
      - Pole sources measured: forced counts 9.5 %; a face frame +64 poles; staircase cut borders about half their vertices. Tried and rejected: pre-cutting island holes before QuadriFlow, sharp-edge guidance, smoothing cut selections, filling the whole nasolabial interior with mouth rings.
      - Still below Kaspar: the nose and upper-lip patch between the lip rings and the nasolabial loop is irregular auto quads; slight lumps at the mouth corners; no crease loops or expression-bound planning; eye and mouth counts above Kaspar's 18/24 (forcing them costs 40 poles).
      
      ## 10. GUI-only techniques and the headless substitute
      
      | Technique (who)                                                                    | Substitute                                                                                 |
      | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
      | Grease Pencil requirement map (Kaspar), painted plans (Dikko), annotations (Kenny) | landmark coordinates and loop counts as data; `R.surface_point`, `R.cross_section`         |
      | Poly Build, F2, extrude to cursor, Tweak drag with face snapping                   | `R.add_ring`, `R.bridge`, `R.tube`, `R.project` (nearest or along normals, never the view) |
      | Relax Slide / Smooth brush on the retopo                                           | `R.relax` (or `bx_gui` with the Relax Slide asset in a live session)                       |
      | Inflate/deflate against the wire-displayed sculpt                                  | `R.fit_subdiv`; game: `R.push_outside`                                                     |
      | Knife re-routing, Rip with Fill for a 6-pole                                       | `bmesh.ops.connect_verts`, `bpy.ops.mesh.vert_connect_path`; `mesh.rip` fails headless     |
      | Mark Seam / Sharp reminders                                                        | boolean edge attributes (`retopo_crease`), read by `R.relax` and `R.topology_report`       |
      | Cursor snapped to Volume                                                           | `R.volume_center`                                                                          |
      | Visual loop tracing                                                                | `R.loops_around`, `R.loop_stats`, wire sheet                                               |
      | Retopology overlay, In Front, Solidify back-face trick                             | not needed headless; `overlay.show_retopology` in a GUI session                            |
      
    • procedures.md 30.8 KB
      # Retopology procedures (tested bpy, Blender 5.2.1)
      
      Every block fenced as `python` below is executed, in order, in one namespace, by `tests/code/blender-retopology/test_procedures.py` on the Blender Studio sheep sculpt (`tests/fixtures/sheep_sculpt.blend`, 186k faces, object renamed `Sculpt`). Status of the last run is printed per block. Blocks fenced as `python-gui` need a live Blender window (MCP bridge) and were **not run headless**. The module functions are unit-tested separately in `test_units.py` (31 checks) and `test_units_v2.py` (32 checks, the v2 additions), measured on the sheep in `test_sheep.py` (`sheep_results.json`), and run end to end in `test_sheep_v2.py` (`sheep_v2_results.json`, compared with the E1 run and Kaspar in `compare/`).
      
      Order matters: later blocks reuse `sculpt`, `cage`, `ear`, `qf` from earlier ones.
      
      ## P0. Import (every session)
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      ```python
      import sys, math, bpy, bmesh
      from mathutils import Vector
      sys.path.append("<skills>/scenario-blender-retopology/scripts")   # also puts scenario-blender-expert/scripts on the path
      import bx_retopo as R
      import bx_review
      import numpy as np
      sculpt = bpy.data.objects["Sculpt"]
      ```
      
      ## P1. Inspect the source before planning
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      What changes the plan: holes, non-manifold input, scale, rotation, and whether the sculpt is actually symmetric in its own local space.
      
      ```python
      info = R.topology_report(sculpt)          # bx_audit + expert checks, local space
      print({k: info[k] for k in ("faces", "non_manifold_edges", "boundary_loops", "sym_local_pct",
                                  "self_intersections", "dimensions")})
      holes = R.boundary_loops(sculpt)          # sheep: two 144-edge eye holes, local symmetry 21 % (posed)
      print("rotation in world:", tuple(round(a, 3) for a in sculpt.rotation_euler))
      ```
      
      Decision rules: `sym_local_pct` below ~90 means no Mirror modifier and no QuadriFlow symmetry for the whole body (mirror only the parts that are symmetric, or symmetrize a copy of the sculpt first if the design allows it). A rotated object is fine as long as the cage copies its transform (P2). `non_manifold_edges` or loose parts in an AI mesh or scan: let `R.quadriflow` voxel-remesh it (P6), or run `bx_sculpt.voxel_remesh` from scenario-blender-sculpting.
      
      ## P1b. Read the sculptor's face sets: a ready-made landmark map
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      Sculptors partition characters with face sets (`.sculpt_face_set`). On the sheep they give the eye masks, nose, muzzle, both lips, the mouth opening, chin, ears, hooves and tail pieces: better landmarks than any ellipse [added, E1 run]. Print the map, render it in color, then name each id from its position and color.
      
      ```python
      fsm = R.face_set_map(sculpt)       # {id: faces, area, center, min, max, components, neighbors}
      for k, v in fsm.items():
          print(k, v["faces"], v["center"], v["components"], list(v["neighbours"])[:3])
      out_fs = (bpy.path.abspath("//") if bpy.data.filepath else "/tmp") + "/facesets"
      paths, palette = R.face_set_review(sculpt, out_fs, views=("front", "low"), focus=((0, -0.34, 0.42), 0.16), res=500)
      lip_line = max((c for c in R.face_set_border(sculpt, 42) if c[1]), key=lambda c: len(c[0]))[0]   # mouth-opening set
      ears = R.face_set_components(sculpt, 21)[:2]   # two touching-free pieces of one set (k-means on x split hooves wrongly)
      print(len(lip_line), "lip-line vertices;", [len(e) for e in ears], "ear faces")
      ```
      
      `face_set_border(sculpt, a, b)` gives the border against one other set; `face_verts(sculpt, faces)` the vertices of a component.
      
      ## P2. The retopo stack (Kaspar Snow Live #1 [00:11:32], BCON22 frames 00:22:53 to 00:26:38)
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      Raw bpy, same as `R.setup_cage(sculpt, name=..., wrap="TARGET_PROJECT")`:
      
      ```python
      cage = bpy.data.objects.new("GEO-sheep_retopo", bpy.data.meshes.new("GEO-sheep_retopo"))
      sculpt.users_collection[0].objects.link(cage)
      cage.matrix_world = sculpt.matrix_world.copy()   # Mirror X = the sculpt's own local X plane
      m = cage.modifiers.new("Mirror", 'MIRROR')
      m.use_clip = True; m.use_mirror_merge = True; m.merge_threshold = 0.001
      sw = cage.modifiers.new("Shrinkwrap", 'SHRINKWRAP')
      sw.target = sculpt; sw.wrap_method = 'TARGET_PROJECT'; sw.wrap_mode = 'ON_SURFACE'
      ss = cage.modifiers.new("Subdivision", 'SUBSURF')
      ss.levels = 2; ss.render_levels = 2; ss.show_in_editmode = False   # never build with subdiv on the cage
      print([md.type for md in cage.modifiers])
      ```
      
      Wrap choice: `TARGET_PROJECT` + On Surface for skin (Kaspar's default since Snow); `NEAREST_SURFACEPOINT` is the simplest and safest on patch borders but can snap across thin gaps; clothing shells or double-sided sculpts: `R.setup_cage(..., wrap="PROJECT")` sets both directions, Above Surface and a Limit (Kaspar Live #7 [00:48:52]). Keep `offset = 0`: an offset fixes convex areas and worsens cavities (Duha BCON23 [00:08:24]); fix volume with P9 instead.
      
      GUI session only (visibility and hand snapping):
      
      ```python-gui
      area = next(a for a in bpy.context.screen.areas if a.type == 'VIEW_3D')
      ov = area.spaces.active.overlay
      ov.show_retopology = True          # replaces Hidden Wire / In Front / Solidify tricks (3.6+)
      ov.retopology_offset = 0.01        # default since 4.5
      ts = bpy.context.scene.tool_settings
      ts.use_snap = True; ts.snap_elements_individual = {'FACE_PROJECT'}; ts.use_mesh_automerge = True
      ```
      
      ## P3. Build a part from sculpt sections (limb, ear, tail, finger, horn)
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      Kaspar builds limbs from an 8-vertex circle centered inside the limb and adds three loops per joint (Live #4 [00:06:54], [00:08:01]). Headless: stations along the part, `R.ring` slices the sculpt perpendicular to the path and resamples each section to `n` vertices; `R.tube` bridges them. Here: the sheep's right ear, stations = centers of horizontal sections.
      
      ```python
      def part_stations(target, z_top, z_step, side, y_range, x_min=0.1, n_max=40, every=3):
          cent = []
          for k in range(n_max):
              z = z_top - z_step * k
              best = None
              for pts, closed in R.cross_section(target, (0, 0, z), (0, 0, 1)):
                  c = sum(pts, Vector()) / len(pts)
                  if closed and side * c.x > x_min and y_range[0] < c.y < y_range[1]:
                      best = c
              if best is None:
                  break
              cent.append(best)
          return cent[::every] + ([cent[-1]] if (len(cent) - 1) % every else [])
      
      ear = R.setup_cage(sculpt, name="GEO-sheep_ear_R", mirror=False, subsurf=2)
      st = part_stations(sculpt, 0.44, 0.004, +1, (-0.36, -0.22))
      rings = R.tube(ear, sculpt, st, n=10, start_dir=(0, -1, 0))
      tip = min(R.boundary_loops(ear), key=lambda lp: sum(ear.data.vertices[i].co.z for i in lp))
      R.grid_fill(ear, tip, target=sculpt)      # even loop only; cap projected onto the ear tip
      print(len(rings), "rings,", len(ear.data.polygons), "faces")
      ```
      
      Joint loops for a limb with a known joint point (elbow, knee, wrist, finger joint):
      
      ```python
      stations = R.joint_stations(start=(0, 0, -0.45), joint=(0, 0, 0.0), end=(0, 0, 0.45), n_before=2, n_after=2)
      print([round(p.z, 3) for p in stations])   # ... -0.054, 0.0, 0.054 ...: the three joint loops
      ```
      
      Plane sections fold where a part bends (a knee, a curled ear, a tail): for anything that bends use P3b and P8 (geodesic contours). Landmarks come from renders plus ray casts: `R.surface_point(sculpt, origin, direction)` (the agent's 3D cursor), `R.volume_center(sculpt, point, direction)` (Kaspar's cursor snapped to Volume: midpoint across a limb), `R.nearest_point(sculpt, co)`. Flat, curved parts (ears) whose section does not contain its own centroid are handled by the nearest-centroid fallback in `ring`; check the wire render for twisted rings.
      
      ## P3b. Limbs from geodesic contours (bends, caps, touching parts)
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      Plane sections fold at bends and P3's horizontal slices only suit one ear. Iso-contours of the geodesic distance from the tip never cross and follow any bend (E1 run). `limb_profile` walks them, finds the root (works on caps: the contour length climbs over the cap, stays flat along the limb, then climbs for good onto the body; a short elbow bump does not count; an ear's narrow base wins) and the joints (bends of the contour-centroid path). Touching parts: block the other piece's vertices.
      
      ```python
      core = (0.03, -0.12, 0.28)                       # a point on the chest
      ear_profiles = []
      for comp in ears:
          vv = R.face_verts(sculpt, comp)
          tip = R.limb_tip(sculpt, vv, core)           # farthest from the chest
          other = np.zeros(len(sculpt.data.vertices), bool)
          for c2 in ears:
              if c2 is not comp:
                  other[R.face_verts(sculpt, c2)] = True
          pr = R.limb_profile(sculpt, tip, step=0.004, max_level=0.4, block=other)
          ear_profiles.append(pr)
          print("root", round(pr["root"], 3), "joints", pr["joints"], "levels", len(pr["levels"]))
      ```
      
      Measured on the sheep: roots 0 to 2 cm from the levels a person read off the printed profiles (ears 0.25/0.24, legs 0.26/0.24/0.18/0.18 m); the curled tail is about 1.05 m long, so walk it with `max_level=1.3`.
      
      ## P4. Freeze the Shrinkwrap, relax, keep the seam closed
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      "Duplicate and apply" (Kaspar, Dikko, CG Boost, Jamie Dunbar), raw bpy:
      
      ```python
      with bpy.context.temp_override(object=ear, active_object=ear):
          bpy.ops.object.modifier_copy(modifier="Shrinkwrap")
          bpy.ops.object.modifier_apply(modifier="Shrinkwrap")    # "not first" info is expected
      print([md.name for md in ear.modifiers])                    # a live copy stays: "Shrinkwrap.001"
      ```
      
      `R.freeze(obj)` does the same, renames the copy back and re-pins center vertices to x = 0. Relax (the Relax Slide substitute: tangential smoothing plus reprojection, boundary and crease-attribute verts pinned, no jumps across thin parts):
      
      ```python
      moved = R.relax(ear, sculpt, iterations=4, factor=0.3)
      print("mean move last iteration", round(moved, 5))
      ```
      
      Use relax on hand-built, rough layouts. Do not relax a QuadriFlow result globally: on the sheep it left the fit unchanged and worsened coverage of the sculpt (mean 0.44 % to 0.55 % of size) because extremities shrink.
      
      GUI session, the real brush (Kaspar relaxes in Sculpt Mode with Relax Slide, subdivision off):
      
      ```python-gui
      import bx_gui as G
      bpy.context.view_layer.objects.active = cage
      bpy.ops.object.mode_set(mode='SCULPT')
      G.set_view("FRONT", frame=cage)
      G.activate_brush("SCULPT", "Relax Slide")
      G.stroke("SCULPT", [Vector(p) for p in points_on_cage], size=120, strength=0.5)
      ```
      
      ## P5. Annotations as data (not Mark Sharp / Mark Seam)
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      Kaspar marks seams on articulation loops and sharps on crease edges as reminders (Snow Live #1 [00:30:25]). Since 4.1 `sharp_edge` changes shading and `uv_seam` drives unwrapping, so store plans in custom attributes. `R.relax` pins `retopo_crease` edges and `R.topology_report` counts poles on them.
      
      ```python
      me = ear.data
      crease = me.attributes.new("retopo_crease", 'BOOLEAN', 'EDGE')
      flags = [False] * len(me.edges)
      ring_mid = set(rings[len(rings) // 2])
      for e in me.edges:
          if e.vertices[0] in ring_mid and e.vertices[1] in ring_mid:
              flags[e.index] = True
      crease.data.foreach_set("value", flags)
      print("crease poles:", R.topology_report(ear)["crease_poles"])   # must stay 0
      ```
      
      ## P6. Auto start: QuadriFlow, with the traps handled, denser where the face is
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      ```python
      head_c, head_r = (0.0, -0.331, 0.43), 0.15       # from the head face sets' area-weighted center and extent
      qf, qinfo = R.quadriflow(sculpt, 3600, symmetry=False, name="GEO-sheep_qf", density=[(head_c, head_r, 1.4)])
      print(qinfo)   # sheep: 10 to 30 s; edge_ratio 1.3 to 1.5 = head edges that much shorter than the body's
      odd = [lp for lp in R.boundary_loops(qf) if len(lp) % 2]
      while len(odd) >= 2:                             # QuadriFlow's eye slits (3 edges): pair them up
          a_ = odd.pop(0)
          b_ = odd.pop(0)
          print("parity strip cut", R.parity_strip(qf, a_, b_), "edges")
      ```
      
      What `R.quadriflow` handles, all verified on 5.2.1:
      
      - Raw `bpy.ops.object.quadriflow_remesh` refuses the sheep sculpt ("needs to be manifold") although it is manifold: edges shorter than about 1e-4 fail its check. The copy is scaled to about 10 units, remeshed, scaled back.
      - `use_mesh_symmetry` defaults to True in the operator. Off here unless the mesh is symmetric about its local x = 0.
      - It lands 10 to 20 % under the target; the target is corrected and rerun. It is not deterministic: the same call gave eye slits in one session and no holes in another; re-measure every time.
      - Uniform density (the auto-remesh signature) has a remedy: `density=[(center, radius, factor)]` remeshes a copy inflated around the center by an invertible radial warp (`R.density_warp`, refuses a folding factor), maps it back and projects it. The E1 run used factor 1.8 (head edges 1.46x shorter). v2 settled on 1.4 with 3,600 faces: a denser head base only forced more ring reductions later (1.8/4,000 also left a leg socket unbuilt; 1.3/3,200 gave 6-poles).
      - Parity: it closes small holes, sometimes into 3-edge slits. An all-quad mesh has an even total of boundary edges, so odd boundaries come in pairs, and every island cut around one slit then has an odd border (E1: 47 and 45) that no all-quad annulus can make even. `R.parity_strip` loop-cuts the shortest quad strip between two odd boundaries (+1 vertex on each). Do it BEFORE carving islands.
      - Tried and rejected [added]: cutting the islands out of the sculpt copy first (QuadriFlow merged four holes into one with boundary preservation, returned ragged holes without); sharp edges along the planned curves (no loop within 6 mm of them).
      
      ## P7. Loops on the face: map lines first, then eye and mouth islands
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      Order matters because every carve flags its faces (`bx_keep` face attribute) and later cuts never touch them. **1. Map lines as loops** (`carve_band`): the face frame around eyes and muzzle and the nasolabial loop around nose and mouth (Dikko: "one loop over the nose around the chin", lip rings nest inside; Kaspar's edge-flow layer). The curve is a geodesic offset of face sets (`enclosing_contour`, `close=` fills fjords between features so the band cannot overlap itself), checked on the SURFACE against every limb socket (the posed ear lies on the cheek: 3D distance lies).
      
      ```python
      fs_ids = R.face_sets(sculpt)
      
      
      def verts_of(ids):
          return R.face_verts(sculpt, np.nonzero(np.isin(fs_ids, ids))[0])
      
      
      frame, frame_v = R.enclosing_contour(sculpt, verts_of([6, 2, 3, 23, 42, 1, 26]), 0.012, return_verts=True, close=0.03)
      clear = min(float(np.min(pr["field"][frame_v])) - pr["root"] for pr in ear_profiles)   # geodesic clearance
      print("face frame clearance to the ear sockets", round(clear, 3))
      if clear > 0.016:                                 # half a one-loop band, edge-length spacing
          fb = R.carve_band(qf, sculpt, frame, curve_verts=frame_v)
          print("face frame", fb["count"], "verts, borders", fb["borders"], "spacing", fb["spacing"])
          print("real loop on the curve:", R.find_loop_near(qf, frame, tol=0.4 * fb["spacing"]) is not None)
      nl, nl_v = R.enclosing_contour(sculpt, verts_of([3, 23, 42, 1]), 0.006, return_verts=True, close=0.02)
      nb = R.carve_band(qf, sculpt, nl, curve_verts=nl_v)
      print("nasolabial", nb["count"], "real loop:", R.find_loop_near(qf, nl, tol=0.4 * nb["spacing"]) is not None)
      ```
      
      Band rules (all measured on the sheep): the cut grows from the faces the curve crosses (chosen with normal agreement, so an ear lying on the cheek is never cut) and widens itself until it is an annulus whose two borders do not touch; the default count is the larger border's, so both reduction strips put their poles on the base side and the loop itself stays clean; ring spacing defaults to the local edge length and one ring is the default (a flow loop, exactly on the curve). Tight rings are a crease: 3.5 mm spacing on 12 mm faces left a ridge across the forehead after subdivision; use `rings=3, spacing=0.3 * edge` only for a real crease. On this sheep the gap between the eye masks and the ear roots is about 3.4 cm, so the frame fits only as a one-loop band.
      
      **2. Eye and mouth islands** (`carve_rings`): count as a range, split at the two corners (upper = lower by construction), rings on geodesic iso-contours. Transition rings (for a reduction from the border) are added outside the clean rings and carry its poles.
      
      ```python
      eyes = []
      for lp in R.boundary_loops(sculpt):              # the sculpt's own eye holes are the landmarks
          pts = [sculpt.data.vertices[i].co.copy() for i in lp]
          c = sum(pts, Vector()) / len(pts)
          nrm = sum((sculpt.data.vertices[i].normal for i in lp), Vector()).normalized()
          r = sum((p - c).length for p in pts) / len(pts)
          eyes.append((c, nrm, r, pts, min(pts, key=lambda p: abs(p.x)), max(pts, key=lambda p: abs(p.x))))
      for c, nrm, r, pts, nasal, outer in eyes:
          out = R.carve_rings(qf, sculpt, c, nrm, radius=2.0 * r, rings=4, inner_points=pts, corners=(nasal, outer),
                              count=(18, 24))          # keep the border's count when it is inside the range
          la = R.loops_around(qf, c, nrm, max_radius=2.6 * r, corners=(nasal, outer))
          print("eye border", out["border"], "transition", out["transition"], "lerp fallback", out["lerp_fallback_levels"],
                "rings", [(d["verts"], d["sides"]) for d in la])
      lip = [sculpt.data.vertices[i].co.copy() for i in lip_line]
      lp_ = np.array([p[:] for p in lip])
      mc = Vector(lp_.mean(0))
      mn = Vector(np.linalg.svd(lp_ - lp_.mean(0))[2][2])
      mn = -mn if mn.y > 0 else mn                     # the face looks down local -Y
      m_a, m_b = min(lip, key=lambda p: p.x), max(lip, key=lambda p: p.x)
      bvh_s = R.target_bvh(sculpt)
      
      
      def on_lips(p):
          hit = bvh_s.find_nearest(p)
          return hit[2] is not None and int(fs_ids[hit[2]]) in (23, 1, 42) and hit[3] < 0.02
      
      
      qm = qf.copy(); qm.data = qf.data.copy(); qm.name = "GEO-sheep_closed_mouth"   # for the 2-pole variant below
      sculpt.users_collection[0].objects.link(qm)
      mouth = R.carve_rings(qf, sculpt, mc, mn, radius=0.1, rings=4, inner_points=lip, corners=(m_a, m_b), count=(24, 30),
                            inside=on_lips)
      print("mouth", mouth["border"], "->", mouth["count"], "sides", mouth["sides"], "units", mouth["units"])
      ```
      
      What each option fixed, measured against the E1 run (v1 tools) on the same sculpt:
      
      - `corners` + `count`: exact top/bottom parity on every clean ring of both eyes (E1: 46 to 48, drifting to 27/21). Count reductions are Lampel's 3-to-1 junctions (`R.bridge_reduce`): each unit turns 3 edges into 1 and costs 4 poles (two 3-poles in the strip, two 5-poles on the smaller ring); units never touch each other or the corners, and keep their 5-poles non-adjacent when the ring is long enough (Kaspar: separate adjacent 5-poles).
      - Counts as ranges, measured on the sheep (`test_sheep_v2.py`): forcing Kaspar's eyes 18 / mouth 24 and fixed tube counts gave pole density 9.5 % of faces; `count=(18, 24)`, `(24, 30)` and tube ranges kept most border counts (eyes 24, mouth 30, legs 10 to 14) and gave 5.7 % (Kaspar 5.2 %). Forcing 18/24 on the eyes and mouth alone costs 10 more units, 40 poles.
      - Bands: the nasolabial loop cost 18 poles, the face frame 64 (two long staircase borders): the frame is optional, build it when the rig needs it.
      - `ring_mode="geodesic"` (default): rings on iso-contours of t = d_in / (d_in + d_out); they follow the hanging upper lip (E1: coverage p95 1.69 to 1.14 %). `ring_mode="lerp"` is the old straight interpolation.
      - Tried and rejected: letting the mouth island fill the whole nasolabial interior (rings of 24 over the nose became huge twisted faces; the rings were not star-shaped around the mouth center).
      
      **Closed mouth: Kaspar's 2-pole corners.** A 2-pole at each mouth corner turns the upper-lip loop into the lower lip so every added loop circles the mouth; it is ripped open later for the inner mouth (Kaspar Snow #1 [00:22:38], #2 [01:55:24]). `slit=True` welds the inner ring's halves into that closed lip line. An open mouth (the sheep, Kaspar's own sheep retopo) is the post-rip state: same corner-split rings, the corner vertex is a regular boundary vertex.
      
      ```python
      ms = R.carve_rings(qm, sculpt, mc, mn, radius=0.1, rings=4, inner_points=lip, corners=(m_a, m_b), count=(24, 30),
                         inside=on_lips, slit=True)
      bmv = bmesh.new(); bmv.from_mesh(qm.data)
      two = [v.co.copy() for v in bmv.verts if len(v.link_edges) == 2]
      bmv.free()
      print("closed mouth:", len(two), "2-poles, at the corners:",
            all(min((v - m_a).length, (v - m_b).length) < 0.01 for v in two), "slit vertices", len(ms["hole"]))
      bpy.data.objects.remove(qm)
      ```
      
      Also tested in `test_sheep_v2.py` and on a sphere in `test_units_v2.py`: exactly two valence-2 vertices, both at the lip corners.
      
      ## P8. Replace a limb the auto-remesh got wrong with a geodesic socket tube
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      `socket_tube` cuts the socket at the profile's root (trial cuts until exactly one new hole), puts rings on the geodesic contours every `spacing`, three rings over each detected joint (Kaspar: 3 loops per joint), reduces from the socket border to `count` and grid-fills the tip.
      
      ```python
      for pr in ear_profiles:
          res = R.socket_tube(qf, sculpt, pr, spacing=0.018, count=(16, 28))
          print("ear socket", res["border"], "->", res["ring_counts"][-1], "rings", len(res["rings"]) - 1, "cap", res["cap_faces"])
      t = R.topology_report(qf)
      print("quads", t["quads_pct"], "v6+", t["valence_6plus"], "rim poles", t["rim_poles"], "holes", t["boundary_loops"])
      ```
      
      Straight parts can still use `R.tube(qf, sculpt, stations, first_loop=border)` after `R.cut_region` (P3 stations). Sheep counts used: ears 20, legs 12 (Kaspar: legs 8 then 12), tail 24; spacing 1.4 cm on legs, 1.8 on ears.
      
      ## P9. Volume: fit the subdivided surface (Duha), or Kaspar's Multires Apply Base
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      Snapped cage + subdivision = a surface that shrinks inside (sheep QuadriFlow cage: 87 % of the subdivided surface inside the sculpt).
      
      ```python
      res = R.fit_subdiv(qf, sculpt)       # gentle defaults: 6 iterations, step 0.6, smooth 0.6, clamp 0.25
      print(res["before"]["mean_pct"], "->", res["after"]["mean_pct"], "% of size;",
            res["before"]["inside_pct"], "->", res["after"]["inside_pct"], "% inside")
      ```
      
      `fit_subdiv` disables live Shrinkwrap modifiers (they would re-snap the cage); ship the fitted cage with Mirror (if any) and Subdivision only. Kaspar's Apply Base trick, raw bpy (Snow Live #5 [02:09:01]); prerequisites: Mirror applied, Shrinkwrap and Subsurf removed, cage already on the surface:
      
      ```python
      mr_obj = ear.copy(); mr_obj.data = ear.data.copy(); mr_obj.name = "GEO-sheep_ear_R_multires"
      sculpt.users_collection[0].objects.link(mr_obj)
      for md in list(mr_obj.modifiers):
          mr_obj.modifiers.remove(md)
      with bpy.context.temp_override(object=mr_obj, active_object=mr_obj):
          mr = mr_obj.modifiers.new("Multires", 'MULTIRES')
          for _ in range(2):                                   # Kaspar: 2 body, 3 head, hair, clothes
              bpy.ops.object.multires_subdivide(modifier="Multires", mode='CATMULL_CLARK')
          swd = mr_obj.modifiers.new("SW_detail", 'SHRINKWRAP')   # MUST sit after the Multires
          swd.target = sculpt
          bpy.ops.object.modifier_apply(modifier="SW_detail")      # lands on the multires levels
          mr.levels = 0
          bpy.ops.object.multires_base_apply(modifier="Multires", apply_heuristic=True)   # "Apply Base"
          mr_obj.modifiers.remove(mr_obj.modifiers["Multires"])
      sub = mr_obj.modifiers.new("Subdivision", 'SUBSURF'); sub.levels = sub.render_levels = 2
      print(R.fidelity(mr_obj, sculpt, levels=2, coverage=False)["inside_pct"], "% inside (overshoot if << 50)")
      ```
      
      Measured on the sheep QuadriFlow cage (same cage for both): fit_subdiv 0.045 % mean error, 53 % inside; Apply Base 0.112 %, 15 % inside (overshoot, as Kaspar warns in Live #6 [00:20:08]). Never re-shrinkwrap after either.
      
      ## P10. Checks and review renders
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      ```python
      f = R.fidelity(qf, sculpt, levels=2)          # subdivided surface vs sculpt, plus coverage
      ls = R.loop_stats(qf)                         # closed loops share, spiral suspects
      t = R.topology_report(qf)
      print({k: f[k] for k in ("mean_pct", "p95_pct", "max_pct", "inside_pct", "cov_p95_pct")}, ls,
            {k: t[k] for k in ("quads_pct", "valence_6plus", "rim_poles", "center_poles", "adjacent_e5",
                               "pole_density_pct", "edge_len_cv", "self_intersections")})
      ```
      
      Review: the cage in wire (flow, rings, spirals, poles) and the subdivided surface in matcap (lumps, pinching), same views as the sculpt:
      
      ```python
      out = bpy.path.abspath("//") if bpy.data.filepath else "/tmp"
      for md in qf.modifiers:
          md.show_render = False
      sheet_cage = bx_review.review([qf], out + "/review_cage", views=("front", "right", "threequarter"), modes=("wire",), res=500)
      tmp = qf.modifiers.new("_review_subd", 'SUBSURF'); tmp.levels = tmp.render_levels = 2
      sheet_subd = bx_review.review([qf], out + "/review_subd2", views=("front", "right", "threequarter"), modes=("matcap",), res=500)
      qf.modifiers.remove(tmp)
      print(sheet_cage, sheet_subd)
      ```
      
      The test redirects `out` to `archive/tests/retopo_skill/procedures_run/`. Open both sheets with the image reader and judge with `critique.md`. On the sheep, the numbers alone were misleading: an aggressively fitted hybrid scored 0.043 % mean error (Kaspar's cage: 0.24 %) and looked clearly lumpier.
      
      ## P11. Game low poly finishing (SpeedChar, Lampel)
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      ```python
      low = qf.copy(); low.data = qf.data.copy(); low.name = "GEO-sheep_low"
      sculpt.users_collection[0].objects.link(low)
      for md in list(low.modifiers):
          low.modifiers.remove(md)
      print("tris", R.tri_count(low))                              # compare with the budget table
      moved = R.push_outside(low, sculpt, offset=0.002)            # slightly proud of the high (ep35)
      split = R.triangulate_twisted(low, high=sculpt, angle=15)    # choose each twisted quad's diagonal
      tri = low.modifiers.new("Triangulate", 'TRIANGULATE')        # same triangulation in baker and engine
      tri.quad_method = 'BEAUTY'; tri.keep_custom_normals = True
      print("pushed", moved, "split", split, "sharp without seam", R.hard_edges_without_seams(low))
      out_fbx = (bpy.path.abspath("//") if bpy.data.filepath else "/tmp") + "/sheep_low.fbx"
      with bpy.context.temp_override(selected_objects=[low], active_object=low):
          bpy.ops.export_scene.fbx(filepath=out_fbx, use_selection=True, use_mesh_modifiers=True,
                                   use_triangles=True, mesh_smooth_type='FACE')
      ```
      
      Hard edges: every hard edge must be a UV seam on a baked asset (On Mars 3D, Polycount, see scenario-blender-uv-baking); after unwrapping, `bpy.ops.uv.seams_from_islands(mark_seams=False, mark_sharp=True)` derives hard edges from islands.
      
      ## P12. Expression / deformation smoke test (Surface Deform follows shape keys)
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      ```python
      sd = ear.modifiers.new("ExprCheck", 'SURFACE_DEFORM')
      sd.target = sculpt
      with bpy.context.temp_override(object=ear, active_object=ear):
          bpy.ops.object.surfacedeform_bind(modifier=sd.name)
      print("bound:", sd.is_bound)
      # with expression shape keys on the sculpt: set key_blocks["Smile"].value = 1.0, render a
      # subdivided matcap, check creases form on the reserved loops without pinching at poles
      ear.modifiers.remove(sd)
      ```
      
      ## P13. Handoff hygiene (Kaspar Finale [01:56:19], [01:58:41])
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      ```python
      def handoff(obj):
          with bpy.context.temp_override(object=obj, active_object=obj, selected_objects=[obj],
                                         selected_editable_objects=[obj]):
              for md in list(obj.modifiers):
                  if md.type != 'SUBSURF':
                      bpy.ops.object.modifier_apply(modifier=md.name)
          me = obj.data
          for name in ("sharp_edge", "uv_seam", "retopo_crease"):      # annotation leftovers
              if name in me.attributes:
                  me.attributes.remove(me.attributes[name])
          if not obj.material_slots:
              obj.data.materials.append(bpy.data.materials.new(obj.name + "_viewport"))
          assert obj.name.startswith("GEO-")
          return [md.type for md in obj.modifiers]
      
      print(handoff(ear))
      ```
      
      Also: transforms applied or agreed with the rigger, reused parts stripped of vertex groups and shape keys, no intersections between body and clothing even where hidden, rig-prep shape keys (eyes open/closed) delivered with the mesh, and the character must read with Subdivision hidden.
      
      ## P14. Finishing an AI-generated or scanned mesh (composition)
      
      _Verified on 5.2.1 headless: `tests/code/blender-retopology/test_procedures.py` (python blocks); python-gui blocks not run headless._
      
      ```python
      def finish_generated_mesh(src, faces, deforming=False, landmarks=()):
          """Static, bake-only or background asset: auto path. Deforming: auto start plus
          carved rings at each (center, normal, radius) landmark, then the gentle subdiv fit."""
          obj, qi = R.quadriflow(src, faces, symmetry=R.topology_report(src)["sym_local_pct"] > 98)
          for c, nrm, rad in landmarks:
              R.carve_rings(obj, src, c, nrm, radius=rad, rings=3)
          fit = R.fit_subdiv(obj, src)
          return obj, {"quadriflow": qi, "fit_after_mean_pct": fit["after"]["mean_pct"],
                       "report": R.topology_report(obj), "loops": R.loop_stats(obj)}
      
      gen, rep = finish_generated_mesh(sculpt, 2500)
      print(rep["quadriflow"], rep["fit_after_mean_pct"], rep["report"]["quads_pct"], rep["loops"])
      ```
      
      For a triangle-soup AI mesh `R.quadriflow` falls back to a voxel remesh (size 1/250 of the object, or `voxel=`); bake textures from the ORIGINAL generated mesh onto the result (scenario-blender-uv-baking), not from the voxel copy.
      
    • sources.md 17 KB
      # Sources (24 videos, 2 Blender Studio files)
      
      Per-video notes with full timestamps: `notes/retopology/<id> - <title>.md`; batch digests: `_digest_snow_retopo_a.md`, `_digest_snow_retopo_b.md`, `_digest_speedchar_game_character.md`, `_digest_retopo_theory.md`; calibration: `_calibration_blender_studio_retopo.md`. Frame contact sheets for Kaspar's BCON22 talk: `frames/OLwtul3ngB8/sheet_01.jpg` to `sheet_11.jpg`.
      
      ## Film and animation retopology
      
      | Expert, credential                                                                         | Video                                                                                                        | Best for                                                                                                                                                    | Best timestamps                                                                                                                                                                                                                                                                            |
      | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
      | Julien Kaspar, Blender Studio character artist (Spring, Sprite Fright, Rain and Snow rigs) | The Puzzle of "Good" Retopology, BCON22, 50:30, https://www.youtube.com/watch?v=OLwtul3ngB8                  | the requirement map (six layers), articulation counts, style calibration (Rain, Snow, Rex, Anar), live start on the sheep character this skill is tested on | [00:07:55] map; [00:10:11] crease 1+2; [00:12:25] articulation; [00:13:32] expression bounds and landmarks; [00:20:16] realistic base mesh; [00:28:40] 2-pole aid; [00:43:49] poles; [00:44:59] local proximity loops; frames 00:22:53 (stack), 00:26:38 (subdiv 1/2), 00:34:08 (Solidify) |
      | same                                                                                       | Snow Live #1, 2:16:32, https://www.youtube.com/watch?v=sXhIz-5g3Gc                                           | setup, face order, mouth and eye articulation, creases, poles off creases                                                                                   | [00:11:32] Shrinkwrap Target Normal Project; [00:18:34] articulation; [00:22:38] mouth-corner 2-pole; [00:27:00] eye blocked closed; [00:36:27] poles; [01:31:00] duplicate and apply; [02:11:22] eyelid loops                                                                             |
      | same                                                                                       | Snow Live #2, 2:12:18, https://www.youtube.com/watch?v=tRqCeWZLqQo                                           | loop budget out of the face, ear, neck, inner mouth, Shrinkwrap mask group, mirror-apply-bisect                                                             | [00:03:03] framing loop; [00:30:36] relax across the seam; [00:41:44] chin framing; [01:01:22] sharp edge without pole; [02:02:06] mask group                                                                                                                                              |
      | same                                                                                       | Snow Live #3, 2:34:00, https://www.youtube.com/watch?v=7xMRyBHeDBo                                           | mesh hair shells, rim poles, bulge loops, bevel creases, triangles and n-gons, game vs film                                                                 | [00:36:54] rim poles; [01:05:50] bulge loop; [01:18:27] lay out before beveling; [01:29:22] tris/n-gons; [01:51:35] triangulate before baking                                                                                                                                              |
      | same                                                                                       | Snow Live #4, 2:11:54, https://www.youtube.com/watch?v=KWNcQZQkyjc                                           | arms, fingers, hand loop reduction, separate objects, his five quality criteria                                                                             | [00:06:54] 8-vertex arm ring; [00:08:01] 3 loops per joint; [00:35:53] linked-duplicate fingers; [00:58:52] criteria; [01:27:33] ring mismatch; [01:30:37] diamonds on the back of the hand                                                                                                |
      | same                                                                                       | Snow Live #5, 2:22:00, https://www.youtube.com/watch?v=8ljpBjgIST0                                           | hands, legs, merge by counts, two Shrinkwraps, Multires Apply Base                                                                                          | [00:33:59] hand 18 vs arm 11; [00:56:06] two Shrinkwraps by vertex group; [01:37:48] joints; [02:06:31] bridge and slide; [02:09:01] Apply Base                                                                                                                                            |
      | same                                                                                       | Snow Live #6, 2:03:08, https://www.youtube.com/watch?v=8UqpNwkTEXA                                           | post Apply Base volume pass, rebuilding sharp edges, naming, shirt from body                                                                                | [00:14:27] deflate to the skin; [00:20:08] never re-shrinkwrap; [00:25:33] rebuild sharp loops; [00:39:00] softer creases; [01:33:34] shirt base                                                                                                                                           |
      | same                                                                                       | Snow Live #7, 2:14:06, https://www.youtube.com/watch?v=EBet5sIZcKY                                           | cloth folds, 6-pole to 5-3-5, split for asymmetry, clothing Shrinkwrap Project + Limit                                                                      | [00:08:54] crease pair by bevel; [00:11:41] valence rule; [00:32:03] split object; [00:48:52] Project + Limit; [01:23:34] no spirals                                                                                                                                                       |
      | same                                                                                       | Snow Finale, 2:02:58, https://www.youtube.com/watch?v=FnTOVcPaWV8                                            | triangle pairs in folds, intersections, inner bands, handoff list                                                                                           | [00:28:32] triangle pairs; [01:04:16] adjacent diamonds; [01:47:58] no intersections; [01:49:42] inner band; [01:56:19] handoff; [01:58:41] cage-level check                                                                                                                               |
      | Jonathan Lampel, CG Cookie instructor, RetopoFlow project manager                          | Building Good Topology and Retopology, BCON24, 50:29, https://www.youtube.com/watch?v=PAK5zQONgvs            | why poles pinch (density), tris and n-gons as tools, holding edges, junctions, low-poly edge test                                                           | [00:09:05] auto only for static; [00:12:45] Triangulate before export; [00:21:18] density; [00:23:00] Optimal Display off; [00:30:34] double smooth; [00:32:18] junctions; [00:34:32] every edge has a job; [00:40:04] flexible areas                                                      |
      | Vilem Duha, BlenderKit co-founder, 20+ years in Blender                                    | A New Approach to Retopology, BCON23, 16:35, https://www.youtube.com/watch?v=7AR9-LxY6AQ                     | fit the subdivided surface instead of snapping the cage                                                                                                     | [00:07:18] the error; [00:08:24] offset makes cavities worse; [00:08:58] inverse subdivision; [00:11:15] iterative; [00:11:48] where to add loops                                                                                                                                          |
      | Dikko, character artist and rigger                                                         | Modeling for Animation 06: Retopologizing the Face, 52:16, https://www.youtube.com/watch?v=SwM19PgSdCM       | pole-by-pole face map, five eye poles, equal spans, center line, mouth bag, head-to-neck join                                                               | [00:01:28] five poles; [00:04:16] 10/10 spans; [00:04:49] no spirals, center line; [00:27:42] back-of-head fill; [00:41:46] mouth bag; [00:49:12] neck collapse                                                                                                                            |
      | Dikko                                                                                      | ULTIMATE Character Modeling Part 7: Retopo of the Body, 1:31:15, https://www.youtube.com/watch?v=3WnW_ZFXudk | body core loops, shoulder and hip caps, span counts, routing spans                                                                                          | [00:01:51] chest and hip loops; [00:13:47] arm spans; [00:26:03] bevel at the pole; [00:31:22] routing; [01:04:04] collapse over add; [01:16:41] leg 18                                                                                                                                    |
      
      ## Game low poly
      
      | Expert, credential                                                                       | Video                                                                           | Best for                                                              | Best timestamps                                                                                                                                                                          |
      | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
      | Nikolay Naydenov (SpeedChar), character artist at Gameloft Bulgaria (Heroes of the Dark) | ep23 Starting the Low Poly, 51:31, https://www.youtube.com/watch?v=6MccOhJZXPQ  | budgets, exploding for the bake, silhouette-only surrounding, fingers | [00:01:06] explode; [00:09:58] head vs body budget; [00:24:13] no Shrinkwrap on very low poly; [00:34:02] silhouette; [00:34:34] face 3x density; [00:50:07] unique UVs where AO differs |
      | same                                                                                     | ep24 Left Hand Retopology, 51:10, https://www.youtube.com/watch?v=YL2oRj3NGuY   | where triangles are fine, slivers, diagonal control, hand budget      | [00:00:32] triangles; [00:03:06] cut the diagonal; [00:11:06] rigid parts; [00:46:36] big flat polygons; [00:50:34] hands budget                                                         |
      | same                                                                                     | ep25 Head Retopology 01, 57:58, https://www.youtube.com/watch?v=TYjlg5ilozg     | game head loops only for animation, symmetry, cavities                | [00:01:08] rotate around the mouth; [00:04:29] split asymmetric parts; [00:10:24] under-eye loop; [00:14:39] follow planes; [00:39:02] ear cavity                                        |
      | same                                                                                     | ep26 Head Retopology 02, 50:46, https://www.youtube.com/watch?v=PsOBC6cpomk     | edit the high for the low, front vs back fidelity, final head count   | [00:02:17] edit the high; [00:04:25] front vs back; [00:10:28] triangles where nothing moves; [00:33:24] 2,600 tris; [00:48:55] symmetrize the high                                      |
      | same                                                                                     | ep35 Adjusting the Low Poly, 36:50, https://www.youtube.com/watch?v=EnzdtZnBigE | fitting pass, manual triangulation, final count                       | [00:06:30] 12,000 tris; [00:10:18] triangulate twisted quads; [00:18:44] fitting pass; [00:21:30] slightly proud                                                                         |
      | On Mars 3D (in notes/uv-baking)                                                          | 5 Tips to Master Texture Bakes, https://www.youtube.com/watch?v=C_RqdNbYOjE     | every hard edge is a UV seam                                          | [00:08:04]                                                                                                                                                                               |
      
      ## Setup, tools and beginner-level confirmations
      
      | Expert, credential                                                          | Video                                                                                       | Best for                                                                              | Best timestamps                                                                                                                          |
      | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
      | Jamie Dunbar, character artist, author of "Character Design for Production" | Retopology with Poly Build, 24:14, https://www.youtube.com/watch?v=l-sALvdn3FI              | Shrinkwrap failure modes, no bumps in topology, subdiv off while building             | [00:05:22] both directions; [00:06:17] normals; [00:15:09] popping fix; [00:17:15] bumps go to maps; [00:22:19] no subdiv while building |
      | Zach Reinhardt (CG Boost)                                                   | Retopology in Blender beginner tutorial, 14:14, https://www.youtube.com/watch?v=X2GNyEUvpD4 | snapping vs Shrinkwrap, important loops first unconnected, finalize order             | [00:07:02] view-angle trap; [00:08:04] Shrinkwrap catches loop cuts; [00:09:36] loops first; [00:13:01] finalize                         |
      | Grant Abbitt, Blender educator (GameDev.tv)                                 | Retopology Guide Part 1: Setup, 8:44, https://www.youtube.com/watch?v=6sE-p8PcHDo           | budget example, apply rotation before mirroring                                       | [00:00:31] 4 M to 3.5k tris; [00:04:19] apply rotation                                                                                   |
      | Grant Abbitt                                                                | Retopologize a Face, 7:24, https://www.youtube.com/watch?v=C249AnzAI40                      | critique of auto retopo, flows first, poles where flows meet                          | [00:00:32] auto critique; [00:04:28] flows first; [00:05:22] poles                                                                       |
      | Ryan King, Blender educator                                                 | Retopology for Beginners, 21:51, https://www.youtube.com/watch?v=1myOZaxtHes                | mirror selection trap, tri better than 5-gon, edge on the crease, backup before apply | [00:07:37]; [00:14:04]; [00:15:41]; [00:18:39]                                                                                           |
      | Kenny Praise (Kenny's Arts)                                                 | Retopology for Beginners, 22:40, https://www.youtube.com/watch?v=7XdbJX8roCg                | five flows drawn first, relax after each block, equal counts                          | [00:00:35]; [00:03:51]; [00:11:00]                                                                                                       |
      
      ## Ground truth files (CC-BY, Blender Studio)
      
      From https://studio.blender.org/blog/live-retopology-at-bcon22/ : `tests/fixtures/retopo_examples.blend` and `retopo_demo.blend` (Rex, Snow, Rain, stylized heads, hand, sheep), measured with bx_audit in `_calibration_blender_studio_retopo.md`. Test fixtures cut from them: `sheep_sculpt.blend` (input) and `sheep_reference_retopo.blend` (Kaspar's retopology of the sheep, used only for numbers).
      
  • scripts
    • bx_retopo.py 125.6 KB
      """
      bx_retopo: retopology building blocks for an agent with no mouse (Blender 5.2.1 LTS).
      
      Works headless (`blender --background`) and inside a live session. Everything operates on
      mesh data (bmesh / numpy / BVH), so no 3D view, brush stroke or modal tool is needed.
      Tested by tests/code/blender-retopology/test_bx_retopo.py on the Blender Studio sheep sculpt.
      
          import sys; sys.path.append("<skills>/scenario-blender-retopology/scripts"); import bx_retopo as R
      
      COORDINATES. All points passed to or returned by this module are in the TARGET's local
      space. `setup_cage` gives the cage the target's transform, so cage-local == target-local
      and Mirror X is the sculpt's own symmetry plane (sculpts are often rotated in world space;
      the test sheep is rotated 27 degrees about Z, world-X mirroring would be wrong).
      
      STACK AND FREEZE (Kaspar's setup, Dikko, CG Boost)
        setup_cage(target, ...)            new cage object: Mirror (clip, merge) > Shrinkwrap > Subsurf
        freeze(obj)                        "duplicate and apply" the Shrinkwrap, re-pin the centre line
        project(obj, target, idx)          move vertices onto the surface (nearest or along normals)
        snap_center(obj) / symmetrize(obj) keep the mirror seam closed / make one side win
      
      BUILD (the map method, as data instead of strokes)
        surface_point(target, origin, dir) ray cast a landmark onto the sculpt
        cross_section(target, co, no)      closed contours of the sculpt cut by a plane
        ring(target, center, normal, n)    n evenly spaced vertices around a limb/feature section
        tube(obj, target, stations, n)     extrude rings along a path and bridge them (limbs, ears, horns)
        joint_stations(a, joint, b, ...)   stations with Kaspar's 3 loops per joint
        add_ring / bridge / weld_loops / grid_fill / boundary_loops   assemble islands, match counts, merge
        cut_region(obj, seed, inside)      cut a socket out of an auto-remesh, returns its border (odd
                                           if the cut holds an odd boundary: parity_strip first)
        carve_rings(obj, target, ...)      concentric rings around an eye / mouth: corners (top = bottom),
                                           count or (lo, hi) range, geodesic rings, slit (2-pole corners)
        carve_band(obj, target, curve)     a clean loop along a map line (face frame, nasolabial, neck)
        bridge_reduce / reduce_loop        3-to-1 reduction strips between ring counts (4 poles a unit)
        parity_strip(obj, a, b)            loop-cut a quad strip between two odd boundaries
        limb_tip / limb_profile            geodesic contours from a tip: root and joints found
        socket_tube(obj, target, profile)  socket + rings on geodesic contours + 3 rings per joint + cap
        find_loop_near(obj, curve, tol)    verify that a planned line exists as a real edge loop
      
      FIELDS AND LANDMARKS (on the sculpt's own mesh)
        geodesic, iso_contours, surface_seeds, enclosing_contour, nearest_vertex
        face_sets, face_set_map, face_set_components, face_set_border, face_verts, face_set_review
      
      AUTO START (static assets, scans, AI meshes, first pass)
        quadriflow(target, faces, ...)     QuadriFlow with the fixes it needs headless (scale trick,
                                           voxel fallback); symmetry OFF unless the mesh is symmetric;
                                           density=[(center, radius, factor)] denser where the face is
        relax(obj, target, ...)            tangential smooth + reproject (Relax Slide substitute), pins
                                           boundary / centre line / crease-attribute vertices
      
      VOLUME (a shrinkwrapped cage shrinks under subdivision)
        fit_subdiv(obj, target, ...)       Duha: move cage verts so the SUBDIVIDED surface fits
        multires_recover(obj, target, ...) Kaspar: Multires + Shrinkwrap + Apply Base
      
      CHECKS
        fidelity(obj, target, levels=2)    distance of the subdivided surface to the sculpt, plus coverage
        topology_report(obj, ...)          bx_audit + expert checks: rim poles, centre-line poles, crease
                                           poles, adjacent 5-poles, valence 2, local symmetry, pole density
        loops_around(obj, center, ...)     closed edge rings that encircle a landmark (eye, mouth), parity
        loop_stats(obj)                    closed vs open loops, spiral suspects
      GAME
        tri_count, triangulate_twisted(obj, high), push_outside(obj, high, offset),
        hard_edges_without_seams(obj)
      """
      
      __version__ = "0.1"  # Blender Expert Skills v0.1 (2026-09-24)
      import math
      import os
      import sys
      import time
      
      import bmesh
      import bpy
      import numpy as np
      from mathutils import Matrix, Vector
      from mathutils.bvhtree import BVHTree
      from mathutils.kdtree import KDTree
      
      _HERE = os.path.dirname(os.path.abspath(__file__))
      _EXPERT = os.path.normpath(os.path.join(_HERE, "..", "..", "scenario-blender-expert", "scripts"))
      if _EXPERT not in sys.path:
          sys.path.append(_EXPERT)
      
      _BVH_CACHE = {}
      
      
      # --------------------------------------------------------------------------------------
      # small helpers
      # --------------------------------------------------------------------------------------
      def _local_size(target):
          me = target.data
          if not len(me.vertices):
              return 1.0
          co = np.empty(len(me.vertices) * 3, dtype=np.float64)
          me.vertices.foreach_get("co", co)
          co = co.reshape(-1, 3)
          return float((co.max(0) - co.min(0)).max()) or 1.0
      
      
      def clear_cache():
          """Drop cached target BVHs (call after editing a target sculpt)."""
          _BVH_CACHE.clear()
      
      
      def target_bvh(target):
          """BVH of the evaluated target in its LOCAL space (cached per target object)."""
          key = (target.name, len(target.data.vertices), len(target.data.polygons))
          hit = _BVH_CACHE.get(key)
          if hit is None:
              dg = bpy.context.evaluated_depsgraph_get()
              hit = BVHTree.FromObject(target, dg)
              _BVH_CACHE.clear()
              _BVH_CACHE[key] = hit
          return hit
      
      
      def _ctx(obj):
          return bpy.context.temp_override(object=obj, active_object=obj, selected_objects=[obj],
                                           selected_editable_objects=[obj])
      
      
      def _object_mode():
          if bpy.context.object and bpy.context.object.mode != "OBJECT":
              bpy.ops.object.mode_set(mode="OBJECT")
      
      
      def _local_size_of_mesh(me):
          if not len(me.vertices):
              return 1.0
          co = np.empty(len(me.vertices) * 3)
          me.vertices.foreach_get("co", co)
          co = co.reshape(-1, 3)
          return float((co.max(0) - co.min(0)).max()) or 1.0
      
      
      def _has_mirror(obj):
          return any(m.type == "MIRROR" and m.show_viewport for m in obj.modifiers)
      
      
      # --------------------------------------------------------------------------------------
      # stack
      # --------------------------------------------------------------------------------------
      def setup_cage(target, name="GEO-retopo", mirror=True, wrap="TARGET_PROJECT", subsurf=2,
                     offset=0.0, project_limit=None, mesh=None, collection=None):
          """Create the retopo cage object with the expert modifier stack.
      
          wrap: 'TARGET_PROJECT' (Kaspar's default for skin, On Surface), 'NEAREST_SURFACEPOINT'
          (Dikko, simplest), or 'PROJECT' (clothing shells or double-sided sculpts: both
          directions, Above Surface and a Limit so it cannot grab the inner side; Kaspar Live #7).
          The cage copies the target's matrix_world so Mirror X = the target's local X plane.
          Returns the object. Subsurf is hidden in edit mode (Kaspar, Jamie Dunbar: never build
          with subdivision showing on the cage).
          """
          me = mesh or bpy.data.meshes.new(name)
          obj = bpy.data.objects.new(name, me)
          coll = collection or (target.users_collection[0] if target.users_collection
                                else bpy.context.scene.collection)
          coll.objects.link(obj)
          obj.matrix_world = target.matrix_world.copy()
          size = _local_size(target)
          if mirror:
              m = obj.modifiers.new("Mirror", "MIRROR")
              m.use_axis = (True, False, False)
              m.use_clip = True
              m.use_mirror_merge = True
              m.merge_threshold = size * 1e-3
          sw = obj.modifiers.new("Shrinkwrap", "SHRINKWRAP")
          sw.target = target
          sw.wrap_method = wrap
          sw.offset = offset
          if wrap == "PROJECT":
              sw.use_negative_direction = True
              sw.use_positive_direction = True
              sw.wrap_mode = "ABOVE_SURFACE"
              sw.project_limit = size * 0.05 if project_limit is None else project_limit
          else:
              sw.wrap_mode = "ON_SURFACE"
          if subsurf:
              s = obj.modifiers.new("Subdivision", "SUBSURF")
              s.levels = subsurf
              s.render_levels = 2
              s.show_in_editmode = False
              s.show_on_cage = False
          obj.show_wire = True
          obj.show_in_front = True
          return obj
      
      
      def freeze(obj, modifier="Shrinkwrap"):
          """Kaspar's "duplicate and apply": bake the Shrinkwrap into the vertices, keep a live copy.
      
          Stops twitching, gives relax/bmesh steps real on-surface positions. Re-pins the mirror
          seam afterwards (the projection can pull centre vertices off x = 0).
          """
          _object_mode()
          if modifier not in obj.modifiers:
              raise KeyError(f"{obj.name} has no modifier '{modifier}'")
          with _ctx(obj):
              bpy.ops.object.modifier_copy(modifier=modifier)
              bpy.ops.object.modifier_apply(modifier=modifier)  # "not first" info is expected
          # the copy is named "<name>.001": give it the original name back
          for m in obj.modifiers:
              if m.type == "SHRINKWRAP" and m.name.startswith(modifier + "."):
                  m.name = modifier
                  break
          if _has_mirror(obj):
              snap_center(obj)
          return obj
      
      
      def snap_center(obj, eps=None):
          """Put every vertex within eps of x = 0 exactly on the mirror plane. Returns the count."""
          eps = eps if eps is not None else _local_size_of_mesh(obj.data) * 2e-3
          n = 0
          for v in obj.data.vertices:
              if abs(v.co.x) < eps and v.co.x != 0.0:
                  v.co.x = 0.0
                  n += 1
          obj.data.update()
          return n
      
      
      def symmetrize(obj, direction="-X", dist=None):
          """Mirror one half onto the other (bmesh.ops.symmetrize), then snap the seam.
      
          direction '-X' keeps the -X half and rebuilds +X from it ('+X' the reverse).
          """
          bm = bmesh.new()
          bm.from_mesh(obj.data)
          d = dist if dist is not None else _local_size_of_mesh(obj.data) * 1e-4
          bmesh.ops.symmetrize(bm, input=bm.verts[:] + bm.edges[:] + bm.faces[:], direction=direction, dist=d)
          bm.to_mesh(obj.data)
          bm.free()
          snap_center(obj)
          return obj
      
      
      def project(obj, target, indices=None, method="nearest", keep_center=None):
          """Move cage vertices (all, or `indices`) onto the target surface.
      
          method 'nearest' = Shrinkwrap Nearest Surface Point; 'normal' casts along the vertex
          normal both ways first (closer to Target Normal Project), falling back to nearest.
          """
          bvh = target_bvh(target)
          me = obj.data
          keep_center = _has_mirror(obj) if keep_center is None else keep_center
          eps = _local_size_of_mesh(me) * 1e-5
          idx = range(len(me.vertices)) if indices is None else indices
          for i in idx:
              v = me.vertices[i]
              on_center = keep_center and abs(v.co.x) < eps
              p = None
              if method == "normal" and v.normal.length > 0:
                  best = None
                  for d in (v.normal, -v.normal):
                      h = bvh.ray_cast(v.co, d)
                      if h[0] is not None and (best is None or h[3] < best[3]):
                          best = h
                  p = best[0] if best else None
              if p is None:
                  p = bvh.find_nearest(v.co)[0]
              if p is not None:
                  v.co = p
                  if on_center:
                      v.co.x = 0.0
          me.update()
      
      
      # --------------------------------------------------------------------------------------
      # relax
      # --------------------------------------------------------------------------------------
      def _adjacency(bm):
          nbr = [[e.other_vert(v).index for e in v.link_edges] for v in bm.verts]
          return nbr
      
      
      def relax(obj, target, iterations=10, factor=0.5, pin_boundary=True, pin_attr="retopo_crease",
                pin_indices=None, keep_center=None, tangential=True):
          """Relax Slide substitute: tangential Laplacian smoothing + reprojection onto the target.
      
          Evens out quad sizes without changing the form (Kaspar: "Slide Relax keeps the form
          alive", plain Smooth shrinks it). Pinned: boundary verts (Mesh Boundary auto-masking),
          verts on edges flagged by the boolean edge attribute `pin_attr` (crease loops you
          placed on purpose), explicit `pin_indices`. Centre-line verts stay on x = 0.
          Returns the mean vertex displacement of the last iteration (convergence hint).
          """
          me = obj.data
          bm = bmesh.new()
          bm.from_mesh(me)
          bm.verts.ensure_lookup_table()
          keep_center = _has_mirror(obj) if keep_center is None else keep_center
          pinned = set(pin_indices or [])
          if pin_boundary:
              pinned |= {v.index for v in bm.verts if v.is_boundary and
                         not (keep_center and abs(v.co.x) < _local_size_of_mesh(me) * 1e-5)}
          attr = me.attributes.get(pin_attr) if pin_attr else None
          if attr is not None and attr.domain == "EDGE":
              flags = [False] * len(me.edges)
              attr.data.foreach_get("value", flags)
              for e in me.edges:
                  if flags[e.index]:
                      pinned.update(e.vertices)
          nbr = _adjacency(bm)
          bm.free()
          n = len(me.vertices)
          co = np.empty(n * 3)
          me.vertices.foreach_get("co", co)
          co = co.reshape(-1, 3)
          eps = _local_size_of_mesh(me) * 1e-5
          center = np.abs(co[:, 0]) < eps if keep_center else np.zeros(n, bool)
          movable = np.array([i not in pinned and len(nbr[i]) > 0 for i in range(n)])
          bvh = target_bvh(target)
          last = 0.0
          for _ in range(iterations):
              lap = np.array([co[nb].mean(0) if nb else co[i] for i, nb in enumerate(nbr)]) - co
              if tangential:
                  nrm = np.zeros_like(co)
                  for i in range(n):
                      if movable[i]:
                          hit = bvh.find_nearest(Vector(co[i]))
                          if hit[1] is not None:
                              nrm[i] = hit[1]
                  lap -= (lap * nrm).sum(1)[:, None] * nrm
              new = co + factor * lap
              new[~movable] = co[~movable]
              for i in np.nonzero(movable)[0]:
                  p, pn, _, _ = bvh.find_nearest(Vector(new[i]))
                  if p is None:
                      new[i] = co[i]
                  elif tangential and nrm[i].any() and Vector(nrm[i]).dot(pn) < 0.2:
                      new[i] = co[i]  # would jump to the other side of a thin part (ear, lip, finger)
                  else:
                      new[i] = p
              new[center, 0] = 0.0
              last = float(np.linalg.norm(new - co, axis=1)[movable].mean()) if movable.any() else 0.0
              co = new
          me.vertices.foreach_set("co", co.ravel())
          me.update()
          return last
      
      
      # --------------------------------------------------------------------------------------
      # building: landmarks, sections, rings, tubes
      # --------------------------------------------------------------------------------------
      def surface_point(target, origin, direction):
          """First hit of a ray on the target (local space), or None. The agent's 3D cursor."""
          hit = target_bvh(target).ray_cast(Vector(origin), Vector(direction).normalized())
          return hit[0]
      
      
      def nearest_point(target, co):
          """(location, normal, distance) of the closest target surface point."""
          loc, nrm, _, d = target_bvh(target).find_nearest(Vector(co))
          return loc, nrm, d
      
      
      def volume_center(target, point, direction):
          """Midpoint between the two surface hits across a limb: Kaspar's 'cursor snapped to
          Volume' used to centre an 8-vertex circle inside an arm. `point` is outside or on
          the surface, `direction` points through the limb."""
          d = Vector(direction).normalized()
          bvh = target_bvh(target)
          a = bvh.ray_cast(Vector(point) - d * 1e-4, d)
          if a[0] is None:
              return None
          b = bvh.ray_cast(a[0] + d * 1e-5, d)
          return (a[0] + b[0]) / 2 if b[0] is not None else None
      
      
      def _mesh_arrays(target):
          dg = bpy.context.evaluated_depsgraph_get()
          ev = target.evaluated_get(dg)
          me = ev.to_mesh()
          nv, ne, nl, npoly = len(me.vertices), len(me.edges), len(me.loops), len(me.polygons)
          co = np.empty(nv * 3)
          me.vertices.foreach_get("co", co)
          ed = np.empty(ne * 2, dtype=np.int64)
          me.edges.foreach_get("vertices", ed)
          le = np.empty(nl, dtype=np.int64)
          me.loops.foreach_get("edge_index", le)
          ls = np.empty(npoly, dtype=np.int64)
          me.polygons.foreach_get("loop_start", ls)
          lt = np.empty(npoly, dtype=np.int64)
          me.polygons.foreach_get("loop_total", lt)
          ev.to_mesh_clear()
          return co.reshape(-1, 3), ed.reshape(-1, 2), le, ls, lt
      
      
      def _field_contours(co, ed, le, ls, lt, d):
          """Zero-level contours of a per-vertex scalar `d` (non-finite values never cross).
          Returns [(points ndarray (n, 3), closed bool)] ordered along each contour."""
          d = np.array(d, dtype=np.float64)
          d[d == 0] = 1e-12
          da, db = d[ed[:, 0]], d[ed[:, 1]]
          ok = np.isfinite(da) & np.isfinite(db)
          cross = np.nonzero(ok & (da * db < 0))[0]
          if not len(cross):
              return []
          t = da[cross] / (da[cross] - db[cross])
          pts = co[ed[cross, 0]] + (co[ed[cross, 1]] - co[ed[cross, 0]]) * t[:, None]
          slot = {int(e): k for k, e in enumerate(cross)}
          poly_of_loop = np.repeat(np.arange(len(ls)), lt)
          is_cross = np.zeros(len(ed), bool)
          is_cross[cross] = True
          mask = is_cross[le]
          faces = {}
          for f, e in zip(poly_of_loop[mask], le[mask]):
              faces.setdefault(int(f), []).append(slot[int(e)])
          links = [[] for _ in range(len(cross))]
          for lst in faces.values():
              for a in range(0, len(lst) - 1, 2):
                  links[lst[a]].append(lst[a + 1])
                  links[lst[a + 1]].append(lst[a])
          seen = np.zeros(len(cross), bool)
          out = []
          for s in range(len(cross)):
              if seen[s]:
                  continue
              # walk to one end first if open
              start = s
              prev, cur = None, s
              guard = 0
              while guard < len(cross) + 2:
                  guard += 1
                  nxt = [x for x in links[cur] if x != prev]
                  if not nxt or nxt[0] == s:
                      break
                  prev, cur = cur, nxt[0]
                  if len(links[cur]) < 2:
                      start = cur
                      break
              chain = [start]
              seen[start] = True
              prev, cur = None, start
              closed = False
              while True:
                  nxt = [x for x in links[cur] if x != prev]
                  if not nxt:
                      break
                  if nxt[0] == start:
                      closed = True
                      break
                  if seen[nxt[0]]:
                      break
                  prev, cur = cur, nxt[0]
                  chain.append(cur)
                  seen[cur] = True
              ci = cross[chain]
              near = np.where(t[chain] < 0.5, ed[ci, 0], ed[ci, 1])
              out.append((pts[chain], closed, near))
          return out
      
      
      def cross_section(target, plane_co, plane_no):
          """Cut the target with a plane. Returns a list of (points, closed) contours, points as
          Vectors in target-local space, ordered along the contour. Plane sections fold at bends
          (a knee, a curled ear): for limbs prefer geodesic contours (limb_profile)."""
          co, ed, le, ls, lt = _mesh_arrays(target)
          p = np.asarray(plane_co, float)
          n = np.asarray(plane_no, float)
          n = n / np.linalg.norm(n)
          return [([Vector(q) for q in pts], closed)
                  for pts, closed, _ in _field_contours(co, ed, le, ls, lt, (co - p) @ n)]
      
      
      # --------------------------------------------------------------------------------------
      # fields on the sculpt: topology cache, geodesic distance, iso-contours
      # --------------------------------------------------------------------------------------
      _TOPO_CACHE = {}
      
      
      def _topo(target):
          """Arrays and CSR edge graph of the target's ORIGINAL mesh data (local space, cached).
          Face sets and geodesic fields index these vertices; use a target without
          topology-changing modifiers (apply them on a copy first)."""
          me = target.data
          key = (target.name, len(me.vertices), len(me.edges), len(me.polygons))
          t = _TOPO_CACHE.get(key)
          if t is not None:
              return t
          nv, ne, nl, npoly = len(me.vertices), len(me.edges), len(me.loops), len(me.polygons)
          co = np.empty(nv * 3)
          me.vertices.foreach_get("co", co)
          co = co.reshape(-1, 3)
          ed = np.empty(ne * 2, dtype=np.int64)
          me.edges.foreach_get("vertices", ed)
          ed = ed.reshape(-1, 2)
          le = np.empty(nl, dtype=np.int64)
          me.loops.foreach_get("edge_index", le)
          lv = np.empty(nl, dtype=np.int64)
          me.loops.foreach_get("vertex_index", lv)
          ls = np.empty(npoly, dtype=np.int64)
          me.polygons.foreach_get("loop_start", ls)
          lt = np.empty(npoly, dtype=np.int64)
          me.polygons.foreach_get("loop_total", lt)
          ln = np.linalg.norm(co[ed[:, 0]] - co[ed[:, 1]], axis=1)
          src = np.concatenate([ed[:, 0], ed[:, 1]])
          dst = np.concatenate([ed[:, 1], ed[:, 0]])
          w = np.concatenate([ln, ln])
          order = np.argsort(src, kind="stable")
          src, dst, w = src[order], dst[order], w[order]
          ptr = np.searchsorted(src, np.arange(nv + 1))
          kd = KDTree(nv)
          for i, c in enumerate(co):
              kd.insert(c, i)
          kd.balance()
          vn = np.empty(nv * 3)
          me.vertices.foreach_get("normal", vn)
          t = {"co": co, "vn": vn.reshape(-1, 3), "ed": ed, "le": le, "lv": lv, "ls": ls, "lt": lt,
               "pol": np.repeat(np.arange(npoly), lt), "ptr": ptr.tolist(), "dst": dst.tolist(),
               "w": w.tolist(), "edge_len": ln, "kd": kd, "edge_p95": float(np.percentile(ln, 95)) if ne else 0.0}
          _TOPO_CACHE.clear()
          _TOPO_CACHE[key] = t
          return t
      
      
      def nearest_vertex(target, p, normal=None, k=12):
          """Target vertex nearest to p; with a normal, the nearest whose normal agrees (dot > 0.2)
          so a point on the cheek never maps onto an ear lying against it, and vice versa."""
          tt = _topo(target)
          if normal is None:
              return tt["kd"].find(p)[1]
          n = Vector(normal)
          for _, i, _ in tt["kd"].find_n(p, k):
              if n.dot(Vector(tt["vn"][i])) > 0.2:
                  return i
          return tt["kd"].find(p)[1]
      
      
      def geodesic(target, seeds, limit=None, block=None):
          """Geodesic distance (Dijkstra over the target's edges, local units) from seed vertices.
      
          seeds: vertex indices of the target. limit: stop beyond this distance (inf there).
          block: bool array per target vertex that paths may not enter (a closed chain of blocked
          vertices is a wall: edge paths cannot cross it). Edge-graph distance overestimates the
          true geodesic by a few %, which does not matter for iso-contours. Pure Python: about
          1 to 3 s on the 186k-vertex sheep without a limit."""
          import heapq
          t = _topo(target)
          ptr, dst, w = t["ptr"], t["dst"], t["w"]
          n = len(t["co"])
          dist = [math.inf] * n
          bl = np.asarray(block, bool).tolist() if block is not None else None
          lim = math.inf if limit is None else float(limit)
          h = []
          for s in set(int(x) for x in seeds):
              dist[s] = 0.0
              h.append((0.0, s))
          heapq.heapify(h)
          pop, push = heapq.heappop, heapq.heappush
          while h:
              d, u = pop(h)
              if d > dist[u]:
                  continue
              for k in range(ptr[u], ptr[u + 1]):
                  v = dst[k]
                  if bl is not None and bl[v]:
                      continue
                  nd = d + w[k]
                  if nd < dist[v] and nd <= lim:
                      dist[v] = nd
                      push(h, (nd, v))
          return np.array(dist)
      
      
      def surface_seeds(target, points, radius=None):
          """Target vertices along a polyline (e.g. a retopo border or a landmark contour), for
          use as geodesic seeds or as a wall. radius=None: the nearest vertex of each point,
          after sampling the polyline finer than the target's edges. With a radius (e.g. the
          target's p95 edge length), every vertex within it: a band thick enough that no edge
          path crosses it (use as `block` to wall off the inside of a contour). 3D-nearest:
          where two surfaces touch (an ear on a cheek) it can pick the wrong one; iso_contours
          (verts=True) gives exact vertices for contour curves."""
          t = _topo(target)
          kd = t["kd"]
          pts = [Vector(p) for p in points]
          step = max(t["edge_p95"] * 0.5, 1e-9)
          dense = []
          for i in range(len(pts)):
              a, b = pts[i], pts[(i + 1) % len(pts)]
              k = max(1, int((b - a).length / step))
              dense += [a.lerp(b, j / k) for j in range(k)]
          out = set()
          for p in dense:
              if radius:
                  out.update(i for _, i, _ in kd.find_range(p, radius))
              else:
                  out.add(kd.find(p)[1])
          return sorted(out)
      
      
      def iso_contours(target, field, level, close_gaps=0.0, verts=False):
          """Contours where a per-vertex scalar field (e.g. from `geodesic`) equals `level`.
          Returns [(points [Vector], closed, length)], plus the target vertex next to each point
          as a 4th item with verts=True (exact seeds on the right surface: a 3D nearest-vertex
          lookup jumps to another part where surfaces touch, e.g. an ear lying on the cheek).
          close_gaps > 0 treats an open contour whose end gap is under close_gaps x its length
          as closed (contours cut open by a blocked region or a hole, e.g. hooves that touch)."""
          t = _topo(target)
          f = np.asarray(field, float) - level
          out = []
          for P, closed, near in _field_contours(t["co"], t["ed"], t["le"], t["ls"], t["lt"], f):
              if len(P) < 2:
                  continue
              seg = np.linalg.norm(np.diff(np.vstack([P, P[:1]]) if closed else P, axis=0), axis=1)
              length = float(seg.sum())
              if not closed and close_gaps and len(P) >= 6:
                  gap = float(np.linalg.norm(P[0] - P[-1]))
                  if gap < close_gaps * length:
                      closed = True
                      length += gap
              item = ([Vector(p) for p in P], closed, length)
              out.append(item + (near.tolist(),) if verts else item)
          return out
      
      
      def enclosing_contour(target, seeds, level, block=None, return_verts=False, close=0.0):
          """The largest closed iso-contour at `level` of the geodesic distance from `seeds`:
          a loop that encloses a feature group at a set distance (face frame around eyes and
          muzzle, nasolabial loop around nose and mouth, a neck ring). Returns [Vector] or None;
          with return_verts=True (points, target vertex indices along it): pass those to
          carve_band(curve_verts=...) so the band never snaps onto a touching part.
          close: morphological closing radius (geodesic): gaps between the features narrower
          than about 2 x (close + level) are filled, so the loop has no fjords whose two strands
          would make a band overlap itself (the sheep's face frame between the eye masks)."""
          if close:
              tt = _topo(target)
              R_ = level + close
              f1 = geodesic(target, seeds, limit=R_ + 4 * tt["edge_p95"], block=block)
              shell = np.nonzero((f1 > R_) & (f1 <= R_ + 2 * tt["edge_p95"]))[0]
              f2 = geodesic(target, shell, limit=close * 1.5, block=block)
              field = np.where(f1 <= R_, f2, -1.0)          # distance inward from the dilated rim
              lvl = close
          else:
              field = geodesic(target, seeds, limit=level * 1.5, block=block)
              lvl = level
          cs = [c for c in iso_contours(target, field, lvl, verts=True) if c[1]]
          if not cs:
              return (None, None) if return_verts else None
          best = max(cs, key=lambda c: c[2])
          return (best[0], best[3]) if return_verts else best[0]
      
      
      # --------------------------------------------------------------------------------------
      # the sculptor's face sets: a ready-made landmark map
      # --------------------------------------------------------------------------------------
      def face_sets(target):
          """Per-face face-set ids (numpy int array) of the target, or None if it has none.
          Sculptors partition characters with face sets (eye masks, lips, mouth, ears, hooves);
          they give lip lines, feature outlines and limb pieces better than any ellipse."""
          a = target.data.attributes.get(".sculpt_face_set")
          if a is None:
              return None
          fs = np.empty(len(target.data.polygons), np.int32)
          a.data.foreach_get("value", fs)
          return fs
      
      
      def _face_components(t, face_mask):
          """Edge-connected components of the masked faces, largest first (face index arrays)."""
          faces = np.nonzero(face_mask)[0]
          parent = {int(f): int(f) for f in faces}
      
          def find(x):
              while parent[x] != x:
                  parent[x] = parent[parent[x]]
                  x = parent[x]
              return x
          lm = face_mask[t["pol"]]
          e, f = t["le"][lm], t["pol"][lm]
          o = np.argsort(e, kind="stable")
          e, f = e[o], f[o]
          same = np.nonzero(e[1:] == e[:-1])[0]
          for i in same:
              a, b = find(int(f[i])), find(int(f[i + 1]))
              if a != b:
                  parent[a] = b
          groups = {}
          for x in parent:
              groups.setdefault(find(x), []).append(x)
          return sorted((np.array(sorted(g)) for g in groups.values()), key=len, reverse=True)
      
      
      def face_set_components(target, set_ids):
          """Connected pieces of one face set (or a set of ids), largest first, as face-index
          arrays. Separates touching left/right parts (two hooves, two ears in one face set)
          where k-means on a coordinate splits them wrongly."""
          fs = face_sets(target)
          ids = [set_ids] if np.isscalar(set_ids) else list(set_ids)
          return _face_components(_topo(target), np.isin(fs, ids))
      
      
      def face_verts(target, faces):
          """Vertex indices used by the given faces (face-index array)."""
          t = _topo(target)
          m = np.zeros(len(t["ls"]), bool)
          m[np.asarray(faces, np.int64)] = True
          return np.unique(t["lv"][m[t["pol"]]])
      
      
      def face_set_map(target):
          """Summary of every face set: {id: dict(faces, area, center, min, max, components,
          neighbours={id: shared edges})}. Print it, render the sets in colour
          (face_set_review) and name the features from their positions: the landmark map."""
          fs = face_sets(target)
          if fs is None:
              return {}
          t = _topo(target)
          me = target.data
          cen = np.empty(len(me.polygons) * 3)
          me.polygons.foreach_get("center", cen)
          cen = cen.reshape(-1, 3)
          ar = np.empty(len(me.polygons))
          me.polygons.foreach_get("area", ar)
          e, f = t["le"], t["pol"]
          o = np.argsort(e, kind="stable")
          e, f = e[o], f[o]
          same = np.nonzero(e[1:] == e[:-1])[0]
          a, b = fs[f[same]], fs[f[same + 1]]
          diff = a != b
          nb = {}
          for x, y in zip(a[diff].tolist(), b[diff].tolist()):
              nb.setdefault(x, {}).setdefault(y, 0)
              nb.setdefault(y, {}).setdefault(x, 0)
              nb[x][y] += 1
              nb[y][x] += 1
          out = {}
          for s in np.unique(fs).tolist():
              m = fs == s
              out[s] = {"faces": int(m.sum()), "area": round(float(ar[m].sum()), 5),
                        "center": [round(float(v), 4) for v in cen[m].mean(0)],
                        "min": [round(float(v), 4) for v in cen[m].min(0)],
                        "max": [round(float(v), 4) for v in cen[m].max(0)],
                        "components": [len(c) for c in _face_components(t, m)][:6],
                        "neighbours": dict(sorted(nb.get(s, {}).items(), key=lambda kv: -kv[1]))}
          return out
      
      
      def face_set_border(target, set_a, set_b=None, include_boundary=False):
          """Ordered vertex chains on the border of face set(s) `set_a` (against `set_b`, or
          against anything else). Returns [(vertex indices, closed)] longest first. Example:
          the lip line = border between the mouth-opening set and the lip sets."""
          fs = face_sets(target)
          t = _topo(target)
          A = np.isin(fs, [set_a] if np.isscalar(set_a) else list(set_a))
          B = None if set_b is None else np.isin(fs, [set_b] if np.isscalar(set_b) else list(set_b))
          e, f = t["le"], t["pol"]
          o = np.argsort(e, kind="stable")
          e, f = e[o], f[o]
          cnt = np.bincount(e, minlength=len(t["ed"]))
          first = np.searchsorted(e, np.arange(len(t["ed"])))
          sel = []
          for ei in range(len(t["ed"])):
              c = cnt[ei]
              if c == 2:
                  f1, f2 = int(f[first[ei]]), int(f[first[ei] + 1])
                  if A[f1] != A[f2]:
                      other = f2 if A[f1] else f1
                      if B is None or B[other]:
                          sel.append(ei)
              elif c == 1 and include_boundary and A[int(f[first[ei]])]:
                  sel.append(ei)
          adj = {}
          for ei in sel:
              u, v = (int(x) for x in t["ed"][ei])
              adj.setdefault(u, []).append(v)
              adj.setdefault(v, []).append(u)
          seen, chains = set(), []
          for s in sorted(adj, key=lambda x: len(adj[x])):
              if s in seen:
                  continue
              chain, prev, cur, closed = [s], None, s, False
              seen.add(s)
              while True:
                  nxt = [x for x in adj[cur] if x != prev and (x not in seen or (x == s and len(chain) > 2))]
                  if not nxt:
                      break
                  if nxt[0] == s:
                      closed = True
                      break
                  prev, cur = cur, nxt[0]
                  chain.append(cur)
                  seen.add(cur)
              chains.append((chain, closed))
          return sorted(chains, key=lambda c: len(c[0]), reverse=True)
      
      
      def face_set_review(target, out_dir, views=("front",), focus=None, res=800, view_dirs=None):
          """Render the face sets in flat colours (Workbench, attribute colour) to read which id
          is which feature. views: 'front' (-Y), 'right' (+X), 'left', 'back', 'top', 'low'
          in the TARGET's local axes; focus=((x, y, z) local, radius) frames a close-up.
          Returns (paths, palette {id: rgb})."""
          import colorsys
          fs = face_sets(target)
          os.makedirs(out_dir, exist_ok=True)
          me = target.data
          ids = np.unique(fs)
          pal = {int(s): colorsys.hsv_to_rgb((k * 0.618) % 1.0, 0.7, 0.95) for k, s in enumerate(ids)}
          t = _topo(target)
          rgb = np.array([pal[int(s)] for s in fs], np.float32)[t["pol"]]
          name = "bx_face_sets"
          if name in me.color_attributes:
              me.color_attributes.remove(me.color_attributes[name])
          att = me.color_attributes.new(name, "BYTE_COLOR", "CORNER")
          att.data.foreach_set("color", np.hstack([rgb, np.ones((len(rgb), 1), np.float32)]).ravel())
          prev_name = me.color_attributes.active_color_name
          me.color_attributes.active_color = att
          sc = bpy.data.scenes.new("BX_facesets")
          sc.render.engine = "BLENDER_WORKBENCH"
          sc.display.shading.light = "STUDIO"
          sc.display.shading.color_type = "VERTEX"
          sc.render.resolution_x = sc.render.resolution_y = res
          sc.view_settings.view_transform = "Standard"
          sc.world = bpy.data.worlds.new("BX_fs_world")
          sc.world.color = (0.25, 0.25, 0.25)
          inst = bpy.data.objects.new("BX_fs_obj", me)
          inst.matrix_world = target.matrix_world.copy()
          sc.collection.objects.link(inst)
          cam = bpy.data.objects.new("BX_fs_cam", bpy.data.cameras.new("BX_fs_cam"))
          sc.collection.objects.link(cam)
          sc.camera = cam
          cam.data.type = "ORTHO"
          dirs = {"front": (0, -1, 0), "back": (0, 1, 0), "right": (1, 0, 0), "left": (-1, 0, 0),
                  "top": (0, 0, 1), "low": (0, -0.6, -1)}
          dirs.update(view_dirs or {})
          co = t["co"]
          if focus is not None:
              c_loc, half = Vector(focus[0]), float(focus[1])
          else:
              c_loc = Vector(((co.min(0) + co.max(0)) / 2).tolist())
              half = float((co.max(0) - co.min(0)).max()) * 0.6
          mw = target.matrix_world
          paths = []
          try:
              for v in views:
                  d = (mw.to_3x3() @ Vector(dirs[v])).normalized()
                  c = mw @ c_loc
                  cam.location = c + d * (half * 8 + 1)
                  fwd = -d
                  up = Vector((0, 0, 1)) if abs(fwd.z) < 0.95 else Vector((0, 1, 0))
                  right = fwd.cross(up).normalized()
                  up = right.cross(fwd)
                  cam.rotation_euler = Matrix((right, up, -fwd)).transposed().to_euler()
                  cam.data.ortho_scale = 2 * half * max(mw.to_scale())
                  cam.data.clip_end = half * 20 + 10
                  p = os.path.join(out_dir, f"facesets_{v}.png")
                  sc.render.filepath = p
                  bpy.ops.render.render(write_still=True, scene=sc.name)
                  paths.append(p)
          finally:
              bpy.data.objects.remove(inst)
              bpy.data.objects.remove(cam)
              bpy.data.worlds.remove(sc.world)
              bpy.data.scenes.remove(sc)
              me.color_attributes.remove(me.color_attributes[name])
              if prev_name and prev_name in me.color_attributes:
                  me.color_attributes.active_color_name = prev_name
          return paths, pal
      
      
      def _resample(points, n, closed=True):
          pts = [Vector(p) for p in points]
          if closed:
              pts = pts + [pts[0]]
          seg = [(pts[i + 1] - pts[i]).length for i in range(len(pts) - 1)]
          total = sum(seg)
          count = n if closed else n - 1
          step = total / count
          out, acc, i = [pts[0].copy()], 0.0, 0
          for k in range(1, count if closed else n):
              goal = k * step
              while i < len(seg) - 1 and acc + seg[i] < goal:
                  acc += seg[i]
                  i += 1
              t = (goal - acc) / seg[i] if seg[i] else 0.0
              out.append(pts[i].lerp(pts[i + 1], min(max(t, 0.0), 1.0)))
          if not closed:
              out[-1] = pts[-1].copy()
          return out
      
      
      def ring(target, center, normal, n=8, start_dir=(0, 0, 1)):
          """n evenly spaced surface points around the target's section through `center`.
      
          Kaspar's limb start: an 8-vertex circle inside the arm, "go as low as possible"
          (Live #4 [00:06:54]); legs 8 then 12 (Live #5 [01:21:00]). Picks the closed contour
          that encircles `center` (smallest one if several); for thin curved parts (ears,
          folds) whose section does not contain its own centroid, falls back to the closed
          contour whose centroid is nearest. Starts at the point furthest along `start_dir`
          and winds counter-clockwise around `normal`, so consecutive rings bridge untwisted.
          """
          c0 = Vector(center)
          nrm = Vector(normal).normalized()
          u = Vector(start_dir) - nrm * Vector(start_dir).dot(nrm)
          if u.length < 1e-9:
              u = nrm.orthogonal()
          u.normalize()
          w = nrm.cross(u)
      
          def winding(pts, c):
              ang = [math.atan2((p - c).dot(w), (p - c).dot(u)) for p in pts]
              return sum(((ang[(i + 1) % len(ang)] - ang[i] + math.pi) % (2 * math.pi)) - math.pi
                         for i in range(len(ang)))
      
          enc, near = [], []
          for pts, closed in cross_section(target, c0, nrm):
              if not closed or len(pts) < 3:
                  continue
              cc = sum(pts, Vector()) / len(pts)
              radius = sum((p - cc).length for p in pts) / len(pts)
              if abs(winding(pts, c0)) > math.pi:
                  enc.append((radius, pts, c0))
              elif (cc - c0).length < radius:
                  near.append(((cc - c0).length, pts, cc))
          if enc:
              _, pts, c = min(enc, key=lambda x: x[0])
          elif near:
              _, pts, c = min(near, key=lambda x: x[0])
          else:
              raise ValueError("no closed section at this centre; move the centre inside the part")
          if winding(pts, c) < 0:
              pts = pts[::-1]
          k = max(range(len(pts)), key=lambda i: (pts[i] - c).dot(u))
          pts = pts[k:] + pts[:k]
          return _resample(pts, n, closed=True)
      
      
      def add_ring(obj, points, closed=True):
          """Append vertices (and edges between them) to the cage mesh. Returns vertex indices."""
          bm = bmesh.new()
          bm.from_mesh(obj.data)
          vs = [bm.verts.new(Vector(p)) for p in points]
          rng = range(len(vs)) if closed else range(len(vs) - 1)
          for i in rng:
              bm.edges.new((vs[i], vs[(i + 1) % len(vs)]))
          bm.verts.index_update()
          idx = [v.index for v in vs]
          bm.to_mesh(obj.data)
          bm.free()
          obj.data.update()
          return idx
      
      
      def _loop_edges(bm, idx, closed=True):
          bm.verts.ensure_lookup_table()
          es = []
          rng = range(len(idx)) if closed else range(len(idx) - 1)
          for i in rng:
              a, b = bm.verts[idx[i]], bm.verts[idx[(i + 1) % len(idx)]]
              e = bm.edges.get((a, b))
              if e is None:
                  raise ValueError(f"vertices {idx[i]} and {idx[(i + 1) % len(idx)]} are not connected")
              es.append(e)
          return es
      
      
      def bridge(obj, loop_a, loop_b, align=True):
          """Quad strip between two closed vertex loops of EQUAL count (Kaspar: count and match
          before connecting). align=True picks the cyclic shift and direction of loop_b that
          minimises total edge length, so loops built independently connect without a twist.
          Returns the number of faces created."""
          if len(loop_a) != len(loop_b):
              raise ValueError(f"ring counts differ ({len(loop_a)} vs {len(loop_b)}): add or remove "
                               "loops on one side first, do not let bridge make triangles")
          me = obj.data
          n = len(loop_a)
          lb = list(loop_b)
          if align:
              A = [me.vertices[i].co.copy() for i in loop_a]
              best = None
              for cand in (lb, lb[::-1]):
                  B = [me.vertices[i].co.copy() for i in cand]
                  for sft in range(n):
                      cost = sum((A[i] - B[(i + sft) % n]).length for i in range(n))
                      if best is None or cost < best[0]:
                          best = (cost, cand[sft:] + cand[:sft])
              lb = best[1]
          bm = bmesh.new()
          bm.from_mesh(me)
          bm.verts.ensure_lookup_table()
          faces = []
          for i in range(n):
              q = [bm.verts[loop_a[i]], bm.verts[loop_a[(i + 1) % n]],
                   bm.verts[lb[(i + 1) % n]], bm.verts[lb[i]]]
              faces.append(bm.faces.new(q))
          bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
          bm.to_mesh(me)
          bm.free()
          me.update()
          return len(faces)
      
      
      def joint_stations(start, joint, end, n_before=1, n_after=1, spacing=None):
          """Stations for a limb segment start > joint > end with Kaspar's three loops over the
          joint (centre plus one each side, Live #4 [00:08:01], Live #5 [01:37:48]).
          spacing defaults to 12% of the shorter segment. Returns a list of Vectors."""
          a, j, b = Vector(start), Vector(joint), Vector(end)
          sp = spacing or 0.12 * min((j - a).length, (b - j).length)
          da, db = (j - a).normalized(), (b - j).normalized()
          out = [a]
          for k in range(1, n_before + 1):
              out.append(a.lerp(j - da * sp, k / (n_before + 1)))
          out += [j - da * sp, j, j + db * sp]
          for k in range(1, n_after + 1):
              out.append((j + db * sp).lerp(b, k / (n_after + 1)))
          out.append(b)
          return out
      
      
      def tube(obj, target, stations, n=8, start_dir=(0, 0, 1), first_loop=None):
          """Build a quad tube through the target along `stations` (rings from cross sections,
          bridged in order). Returns the list of ring vertex-index lists (first/last are open
          boundaries you can cap with grid_fill or connect to another part).
          first_loop: an existing boundary loop of `obj` (a socket cut with cut_region) used as
          the first ring; n is then forced to its vertex count and stations[0] is ignored."""
          st = [Vector(s) for s in stations]
          rings = []
          prev_u = Vector(start_dir)
          if first_loop is not None:
              n = len(first_loop)
              rings.append(list(first_loop))
              c0 = sum((obj.data.vertices[i].co for i in first_loop), Vector()) / n
              prev_u = obj.data.vertices[first_loop[0]].co - c0
              st[0] = c0
          for i, c in enumerate(st):
              if first_loop is not None and i == 0:
                  continue
              a = st[max(i - 1, 0)]
              b = st[min(i + 1, len(st) - 1)]
              nrm = (b - a).normalized()
              pts = ring(target, c, nrm, n, start_dir=prev_u)
              rings.append(add_ring(obj, pts))
              prev_u = pts[0] - c
          for i in range(len(rings) - 1):
              bridge(obj, rings[i], rings[i + 1])
          return rings
      
      
      def boundary_loops(obj):
          """Ordered vertex-index loops of every open boundary (hole, cut, part border)."""
          bm = bmesh.new()
          bm.from_mesh(obj.data)
          todo = {e for e in bm.edges if e.is_boundary}
          loops = []
          while todo:
              e = todo.pop()
              a, b = e.verts
              chain = [a.index, b.index]
              cur = b
              while True:
                  nxt = [x for x in cur.link_edges if x in todo]
                  if not nxt:
                      break
                  x = nxt[0]
                  todo.remove(x)
                  cur = x.other_vert(cur)
                  if cur.index == chain[0]:
                      break
                  chain.append(cur.index)
              loops.append(chain)
          bm.free()
          return sorted(loops, key=len, reverse=True)
      
      
      def weld_loops(obj, loop_a, loop_b):
          """Merge two boundary loops with equal counts into one (Kaspar's Bridge Edge Loops,
          then slide one loop into the other with Auto Merge). Pairs vertices by nearest
          position around the loop. Returns the number of merged vertices."""
          if len(loop_a) != len(loop_b):
              raise ValueError(f"counts differ ({len(loop_a)} vs {len(loop_b)})")
          me = obj.data
          A = [me.vertices[i].co.copy() for i in loop_a]
          n = len(A)
          best = None
          for rev in (False, True):
              bb = list(reversed(loop_b)) if rev else list(loop_b)
              BB = [me.vertices[i].co for i in bb]
              for s in range(n):
                  cost = sum((A[i] - BB[(i + s) % n]).length for i in range(n))
                  if best is None or cost < best[0]:
                      best = (cost, bb, s)
          _, bb, s = best
          bm = bmesh.new()
          bm.from_mesh(me)
          bm.verts.ensure_lookup_table()
          targetmap = {}
          for i in range(n):
              va, vb = bm.verts[loop_a[i]], bm.verts[bb[(i + s) % n]]
              mid = (va.co + vb.co) / 2
              va.co = mid
              targetmap[vb] = va
          bmesh.ops.weld_verts(bm, targetmap=targetmap)
          bm.to_mesh(me)
          bm.free()
          me.update()
          return n
      
      
      def grid_fill(obj, loop, span=None, offset=0, target=None):
          """Fill a closed boundary loop with a quad grid (Edit Mode Grid Fill, headless-safe).
      
          Needs an EVEN vertex count: an odd loop "finishes" with zero faces (silent failure in
          5.2.1), so this raises instead. Dikko and Kenny count before every fill.
          target: project the new interior vertices onto it (a cap that follows a tip or dome).
          Returns the number of faces created.
          """
          if len(loop) % 2:
              raise ValueError(f"grid fill needs an even loop, got {len(loop)}: add or dissolve one vertex")
          _object_mode()
          before = len(obj.data.polygons)
          nv0 = len(obj.data.vertices)
          vl = bpy.context.view_layer
          prev_active = vl.objects.active
          vl.objects.active = obj
          obj.select_set(True)
          with _ctx(obj):
              bpy.ops.object.mode_set(mode="EDIT")
              bm = bmesh.from_edit_mesh(obj.data)
              bm.verts.ensure_lookup_table()
              for v in bm.verts:
                  v.select = False
              for e in bm.edges:
                  e.select = False
              for f in bm.faces:
                  f.select = False
              for e in _loop_edges(bm, loop):
                  e.select = True
              bm.select_flush_mode()
              bmesh.update_edit_mesh(obj.data)
              kw = {"offset": offset}
              if span:
                  kw["span"] = span
              bpy.ops.mesh.fill_grid(**kw)
              bpy.ops.object.mode_set(mode="OBJECT")
          if prev_active is not None and prev_active is not obj:
              vl.objects.active = prev_active
          made = len(obj.data.polygons) - before
          if made <= 0:
              raise RuntimeError("grid fill created no faces (loop not closed or shape too irregular)")
          if target is not None and len(obj.data.vertices) > nv0:
              project(obj, target, indices=range(nv0, len(obj.data.vertices)))
          return made
      
      
      # --------------------------------------------------------------------------------------
      # auto start: QuadriFlow
      # --------------------------------------------------------------------------------------
      def density_warp(co, center, radius, factor):
          """Monotone radial space warp p -> c + d * g(|d|), g(r) = 1 + (factor - 1) / (1 + (r/radius)^4):
          linear scale `factor` near the centre, identity far away. Raises if not invertible
          (factor above about 2.7). co: (n, 3) array. Returns the warped array."""
          c = np.asarray(center, float)
          s, R0 = float(factor), float(radius)
          rr = np.linspace(0, 8 * R0, 4000)
          f = rr * (1 + (s - 1) / (1 + (rr / R0) ** 4))
          if (np.diff(f) <= 0).any():
              raise ValueError(f"density factor {factor} makes the warp fold; use a factor <= 2.5")
          d = np.asarray(co, float) - c
          r = np.linalg.norm(d, axis=1)
          return c + d * (1 + (s - 1) / (1 + (r / R0) ** 4))[:, None]
      
      
      def density_unwarp(co, center, radius, factor):
          """Inverse of density_warp (bisection on the radius)."""
          c = np.asarray(center, float)
          s, R0 = float(factor), float(radius)
          d = np.asarray(co, float) - c
          rp = np.linalg.norm(d, axis=1)
          lo, hi = np.zeros_like(rp), rp / min(1.0, s) + 1e-12
          for _ in range(64):
              mid = (lo + hi) / 2
              f = mid * (1 + (s - 1) / (1 + (mid / R0) ** 4))
              lo = np.where(f < rp, mid, lo)
              hi = np.where(f >= rp, mid, hi)
          r = (lo + hi) / 2
          return c + d * np.where(rp > 1e-12, r / np.maximum(rp, 1e-12), 1.0)[:, None]
      
      
      def quadriflow(target, faces, symmetry=False, preserve_sharp=False, preserve_boundary=True,
                     seed=0, voxel=None, name=None, tolerance=0.06, density=None):
          """QuadriFlow remesh of a COPY of the target at about `faces` quads. Returns (obj, info).
      
          Fixes needed headless in 5.2.1:
          - QuadriFlow refuses meshes with edges shorter than ~1e-4 (absolute) as "not manifold";
            a 0.8 m sculpt with 186k faces fails. The copy is scaled to ~10 units, remeshed and
            scaled back (face-count target is scale invariant).
          - Non-manifold input (AI meshes, scans): falls back to a voxel remesh first
            (voxel size = `voxel` or 1/250 of the size), which also closes holes.
          - `use_mesh_symmetry` DEFAULTS TO TRUE in the operator: here it is off unless asked.
            Only enable it when the mesh is symmetric about its LOCAL x = 0.
          - It lands 10 to 20 % under `target_faces`; the target is corrected and rerun (up to
            twice) until within `tolerance`.
          - It closes small holes (the sculpt's 144-edge eye holes vanished) and does not keep
            boundary vertex counts: carve holes back with carve_rings.
          QuadriFlow output: all quads, even density, no feature loops (no rings around eyes or
          mouth, loops wander). Treat it as a start, not a finished deforming character.
          density: [(center, radius, factor), ...] local space. QuadriFlow is uniform; this
            remeshes a copy inflated around each centre (density_warp, monotone and invertible),
            maps the result back and projects it onto the target, so edges near the centre come
            out about `factor` times shorter (sheep head, factor 1.8: 1.46x measured). The
            returned info has edge_ratio (mean edge length outside / inside the first radius).
          """
          _object_mode()
          dg = bpy.context.evaluated_depsgraph_get()
          me = bpy.data.meshes.new_from_object(target.evaluated_get(dg))
          obj = bpy.data.objects.new(name or f"{target.name}_qf", me)
          (target.users_collection[0] if target.users_collection else bpy.context.scene.collection).objects.link(obj)
          obj.matrix_world = target.matrix_world.copy()
          size = _local_size_of_mesh(me)
          s = 10.0 / size
          info = {"scale_trick": s, "voxel": None}
      
          def run(n):
              obj.data.transform(Matrix.Scale(s, 4))
              with _ctx(obj):
                  r = bpy.ops.object.quadriflow_remesh(
                      target_faces=int(n), use_mesh_symmetry=symmetry, use_preserve_sharp=preserve_sharp,
                      use_preserve_boundary=preserve_boundary, smooth_normals=False, seed=seed, mode="FACES")
              obj.data.transform(Matrix.Scale(1.0 / s, 4))
              return r
      
          t = time.time()
          warps = [(np.asarray(c_, float), float(r_), float(f_)) for c_, r_, f_ in (density or [])]
          if warps:
              wco = np.empty(len(me.vertices) * 3)
              me.vertices.foreach_get("co", wco)
              wco = wco.reshape(-1, 3)
              for c_, r_, f_ in warps:
                  wco = density_warp(wco, c_, r_, f_)
              me.vertices.foreach_set("co", wco.ravel())
              me.update()
              s = 10.0 / _local_size_of_mesh(me)
              info["scale_trick"] = s
          if voxel:
              _voxel(obj, voxel)
              info["voxel"] = voxel
          src = obj.data.copy()
          r = run(int(faces))
          if r != {"FINISHED"}:
              vs = voxel or size / 250.0
              _voxel(obj, vs)
              src = obj.data.copy()
              info["voxel"] = vs
              r = run(int(faces))
          if r != {"FINISHED"}:
              raise RuntimeError(f"QuadriFlow failed: {info}")
          ask = int(faces)
          for _ in range(2):  # QuadriFlow lands 10 to 20 % under the target: correct once or twice
              got = len(obj.data.polygons)
              if abs(got - faces) <= tolerance * faces:
                  break
              ask = int(ask * faces / max(got, 1))
              old = obj.data
              obj.data = src.copy()
              bpy.data.meshes.remove(old)
              run(ask)
          bpy.data.meshes.remove(src)
          if warps:
              qme = obj.data
              qco = np.empty(len(qme.vertices) * 3)
              qme.vertices.foreach_get("co", qco)
              qco = qco.reshape(-1, 3)
              for c_, r_, f_ in reversed(warps):
                  qco = density_unwarp(qco, c_, r_, f_)
              qme.vertices.foreach_set("co", qco.ravel())
              qme.update()
              project(obj, target, method="nearest", keep_center=False)
              ed = np.empty(len(qme.edges) * 2, np.int64)
              qme.edges.foreach_get("vertices", ed)
              ed = ed.reshape(-1, 2)
              qco = np.empty(len(qme.vertices) * 3)
              qme.vertices.foreach_get("co", qco)
              qco = qco.reshape(-1, 3)
              mid = (qco[ed[:, 0]] + qco[ed[:, 1]]) / 2
              ln = np.linalg.norm(qco[ed[:, 0]] - qco[ed[:, 1]], axis=1)
              near = np.linalg.norm(mid - warps[0][0], axis=1) < 0.8 * warps[0][1]
              if near.any() and (~near).any():
                  info["edge_ratio"] = round(float(ln[~near].mean() / ln[near].mean()), 3)
          info.update(status=list(r)[0], seconds=round(time.time() - t, 1), faces=len(obj.data.polygons),
                      asked=ask)
          return obj, info
      
      
      def _voxel(obj, voxel_size):
          obj.data.remesh_voxel_size = voxel_size
          obj.data.use_remesh_preserve_volume = True
          obj.data.use_remesh_fix_poles = True
          with _ctx(obj):
              bpy.ops.object.voxel_remesh()
      
      
      # --------------------------------------------------------------------------------------
      # sockets and islands: cut a region of an auto-remesh, rebuild it with planned loops
      # --------------------------------------------------------------------------------------
      PROTECT = "bx_keep"   # INT face attribute: faces built by carve_rings / carve_band / socket_tube
      
      
      def _protect_from(obj, first_face, name=PROTECT):
          """Flag faces [first_face:] (just built) so later cuts leave them alone."""
          me = obj.data
          a = me.attributes.get(name)
          if a is None:
              a = me.attributes.new(name, "INT", "FACE")
          vals = np.zeros(len(me.polygons), np.int32)
          a.data.foreach_get("value", vals)
          vals[first_face:] = 1
          a.data.foreach_set("value", vals)
      
      
      def _cut(obj, seed, inside, guard=True, protect=PROTECT, keep_islands=False, want_deleted=False,
               oriented=False, force=None):
          """Delete faces satisfying inside(center) (flood from the face nearest `seed`, or every
          such face when seed is None), never faces flagged by the `protect` attribute. Returns
          (new boundary loops, pre-cut BVH or None, deleted-face mask of the pre-cut mesh or None).
          oriented=True: inside(center, normal) receives the face normal too. force (with seed None):
          face indices always cut, and the cut grows from them only (a connected region).
          Every border vertex where the cut staircases across the base grid (1 or 3 of its 4 faces
          cut) becomes a 3- or 5-pole once bridged: on the sheep, cut borders carried about half of
          all poles. Smoothing the selection (fill notches, drop teeth) was tried and made it worse."""
          me = obj.data
      
          def key(lp):   # by position: vertex indices shift when the cut deletes vertices
              return frozenset(tuple(round(x, 6) for x in me.vertices[i].co) for i in lp)
          before = {key(lp) for lp in boundary_loops(obj)}
          bm = bmesh.new()
          bm.from_mesh(me)
          bm.faces.ensure_lookup_table()
          lay = bm.faces.layers.int.get(protect) if protect else None
      
          def ok(f):
              return lay is None or not f[lay]
      
          def ins(f):
              return inside(f.calc_center_median(), f.normal) if oriented else inside(f.calc_center_median())
          if seed is None and force:
              # grow from the forced faces only: nothing disconnected from them can join the cut
              sel = {bm.faces[i] for i in force if i < len(bm.faces) and ok(bm.faces[i])}
              stack = list(sel)
              while stack:
                  f = stack.pop()
                  for e in f.edges:
                      for g in e.link_faces:
                          if g not in sel and ok(g) and ins(g):
                              sel.add(g)
                              stack.append(g)
          elif seed is None:
              sel = {f for f in bm.faces if ok(f) and ins(f)}
          else:
              c = Vector(seed)
              cand = [f for f in bm.faces if ok(f)]
              start = min(cand, key=lambda f: (f.calc_center_median() - c).length)
              sel = {start}
              stack = [start]
              while stack:
                  f = stack.pop()
                  for e in f.edges:
                      for g in e.link_faces:
                          if g not in sel and ok(g) and ins(g):
                              sel.add(g)
                              stack.append(g)
          if guard:
              # no border vertex may keep 5+ edges (a new spoke would make it a 6-pole). Such a
              # vertex is a pole with only 1 or 2 of its faces in the cut: shrink the cut away from
              # it (growing over it instead runs away across the base's poles); grow only when a
              # forced face is involved and growth is possible (not blocked by protected faces)
              forced = {bm.faces[i] for i in (force or []) if i < len(bm.faces)}
              for _ in range(60):
                  grow, shrink = set(), set()
                  for f in sel:
                      for v in f.verts:
                          kept = [e for e in v.link_edges if any(g not in sel for g in e.link_faces)]
                          if len(kept) >= 5 or (v.is_boundary and len(kept) >= 4):
                              mine = [g for g in v.link_faces if g in sel]
                              free = [g for g in v.link_faces if g not in sel and ok(g)]
                              if any(g in forced for g in mine) and free:
                                  grow.update(free)
                              else:
                                  shrink.update(mine)
                  if not grow and not shrink:
                      break
                  sel = (sel | grow) - shrink
          if True:
              rest = set(bm.faces) - sel
              comps, seen = [], set()
              for f in rest:
                  if f in seen:
                      continue
                  comp, st = [f], [f]
                  seen.add(f)
                  while st:
                      x = st.pop()
                      for e in x.edges:
                          for g in e.link_faces:
                              if g in rest and g not in seen:
                                  seen.add(g)
                                  comp.append(g)
                                  st.append(g)
                  comps.append(comp)
              comps.sort(key=len, reverse=True)
              # without keep_islands every enclosed piece under 5 % goes with the cut (one clean
              # border); with it (bands) only stranded scraps of 1 to 3 faces go, the enclosed
              # interior (the face inside a face-frame band, a small nose-and-mouth patch) stays
              small = 4 if keep_islands else 0.05 * len(bm.faces)
              for comp in comps[1:]:
                  if len(comp) < max(small, 1) and all(ok(f) for f in comp):
                      sel.update(comp)
          bvh, deleted = None, None
          if want_deleted:
              bvh = BVHTree.FromBMesh(bm)
              deleted = np.zeros(len(bm.faces), bool)
              deleted[[f.index for f in sel]] = True
          bmesh.ops.delete(bm, geom=list(sel), context="FACES_ONLY")
          bmesh.ops.delete(bm, geom=[v for v in bm.verts if not v.link_faces], context="VERTS")
          bmesh.ops.delete(bm, geom=[e for e in bm.edges if not e.link_faces], context="EDGES")
          bm.to_mesh(me)
          bm.free()
          me.update()
          me = obj.data
          new = [lp for lp in boundary_loops(obj) if ke
  • SKILL.md 21.5 KB
    ---
    name: scenario-blender-retopology
    description: "Use when turning a dense sculpt, scan or AI-generated mesh into clean topology in Blender, for animation, subdivision or a game low poly with a triangle budget. Also when choosing between QuadriFlow or voxel remesh and manual retopo, placing loops around eyes, mouth and joints, deciding pole placement, setting up a Shrinkwrap cage, fixing volume loss after subdivision, preparing a low poly for baking, or auditing topology for spirals, 6-poles and triangles. Keywords: retopo, retopology, quad remesh, clean up an AI mesh, edge flow, low poly, bake prep."
    license: MIT
    ---
    
    # Blender retopology
    
    Expert retopology is a plan executed with the fewest loops that capture the forms and let the mesh deform: loops close around eyes, mouth and joints, poles sit where nothing moves, and the subdivided surface, not the cage, matches the sculpt. An agent without a mouse can build every piece of this from data, but it reaches expert quality only on the parts it plans explicitly. Automatic remeshing is a finish for static assets and a start for everything else. 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-blender-expert (execution channel, review loop, 5.2 API changes). Toolkit: [`scripts/bx_retopo.py`](scripts/bx_retopo.py) (`import sys; sys.path.append("<skills>/scenario-blender-retopology/scripts"); import bx_retopo as R`), all coordinates in the sculpt's local space.
    
    ## Stance (the expert delta)
    
    - **Map first, as data.** Kaspar draws six layers on the sculpt before any vertex: edge flow, primary creases, secondary (expression-only) creases, articulation, expression bounds, landmarks (BCON22 [00:07:55], shown in frames 00:07:53 to 00:14:38). The agent writes the same map as coordinates and loop counts (landmarks from renders plus `R.surface_point`), then builds.
    - **Loops are the budget, not vertices.** Articulation minimum (Kaspar after Brian Tindall): lids, lips and brows as curves with 3 controls and 2 handle loops each, plus 2 corner loops on eyes; upper count = lower count (Dikko, Kaspar, Kenny); 3 loops per joint; a crease is 1 loop on it plus 2 hugging it. Elsewhere "as much as it needs to be" (Kaspar Live #3 [01:56:33]).
    - **Poles: valence 3 or 5, where nothing moves, never on a crease loop, a shell rim or the face center line** (Kaspar Live #7 [00:11:41], Live #3 [00:36:54]; Dikko [00:04:49]). Lampel's reason: pinching is a density jump in the subdivided result, harmless on flat still areas (BCON24 [00:21:18]). [added] An agent's poles come from ring-count reductions (4 per unit) and from cut borders that staircase across an auto base (about half their vertices): budget both.
    - **Loops close on themselves.** A spiral around eyes or lips means rebuild (Dikko); a spiral that terminates in a still area is tolerated (Dikko body [00:09:34], Kaspar nose).
    - **Judge the subdivided surface.** A snapped cage shrinks inside when subdivided; a Shrinkwrap offset fixes convex areas and worsens cavities. Fit the cage so its subdivided surface matches (Duha BCON23 [00:08:58]): `R.fit_subdiv`. Kaspar's Multires Apply Base does the same job but overshoots (measured below).
    - **Game low poly = silhouette plus animation loops** (SpeedChar): face about 3x body density, triangles wherever nothing bends, twisted quads cut by hand so baker and engine agree, low poly slightly proud of the high; every hard edge is a UV seam (On Mars 3D).
    - **Auto-remesh fits static, scanned, background or bake-only assets** (Lampel [00:09:05], Grant Abbitt, Duha, Jamie Dunbar). For deforming characters it lacks the judgment calls: on the sheep, QuadriFlow at expert density gave no rings around eyes or mouth and tore a thin ear.
    
    ## Establish first
    
    | Input          | Why it changes the plan                                                        | Default when silent                                                                                                 |
    | -------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
    | Purpose        | film cage renders at subdivision 2 (Kaspar); game ships triangles and bakes    | deforming character: film-style all-quad cage                                                                       |
    | Budget         | film: no vertex budget, minimum loops (Kaspar); game: triangle budget          | film stylized head 1,650 to 4,100 faces, body 9,400 to 16,500 (Blender Studio meshes); game character < 10,000 tris |
    | Deformation    | face rig, joints, how far the mouth travels (expression bounds)                | face + limbs deform                                                                                                 |
    | Camera         | detail and density follow the closest view (SpeedChar)                         | close-up face                                                                                                       |
    | Style          | stylized: creases in topology; realistic: generic base + displacement (Kaspar) | stylized                                                                                                            |
    | Symmetry       | `R.topology_report(sculpt)["sym_local_pct"]`, local space                      | Mirror only above ~98 % (the sheep is 21 %, posed)                                                                  |
    | Separate parts | eyes, teeth, hair clumps, clothing (from the body duplicate)                   | separate objects                                                                                                    |
    
    ## Workflow
    
    1. **Inspect** (procedures P1): holes, manifoldness, scale, rotation, symmetry. GATE: all four written down.
    2. **Map**: read the sculptor's face sets first (`R.face_set_map`, `R.face_set_review`, P1b: lip line, eye masks, ears, hooves); landmarks with coordinates, loop counts per feature from the Numbers table, crease list (check expression shape keys), per-part density. GATE: map exists before any geometry.
    3. **Choose a method per part:**
    
       | Part                                     | Method                                                                                                                                                                            | Evidence                                                                                             |
       | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
       | Eyes, mouth, brows (deforming)           | `carve_rings(corners=, count=(lo, hi))`: upper = lower by construction, geodesic rings, the border's count kept when inside the range; pair odd boundaries first (`parity_strip`) | v2: eyes 24 (12/12), mouth 30 (15/15) on every clean ring; forcing Kaspar's 18/24 cost 40 more poles |
       | Nasolabial, face frame, neck (map lines) | `carve_band` along `enclosing_contour` of face sets (P7): one clean loop, poles pushed onto the base                                                                              | v2: nasolabial +18 poles; face frame opt-in, +64 poles on the sheep                                  |
       | Limbs, ears, tail, fingers, horns        | `limb_profile` (geodesic contours, root and joints found) + `socket_tube(count=)` (P3b, P8); straight parts: `tube` from sections                                                 | v2: all 7 parts incl. the 1 m curled tail, 3 rings per joint                                         |
       | Skull, torso, smooth masses              | QuadriFlow start acceptable, else grid patches                                                                                                                                    | little deformation                                                                                   |
       | Static prop, scan, bake-only             | QuadriFlow whole object (`R.quadriflow`)                                                                                                                                          | Lampel, Grant Abbitt                                                                                 |
       | Realistic human head                     | shrinkwrap a clean generic base mesh, then fit (Kaspar BCON22 [00:20:16])                                                                                                         | not scripted here                                                                                    |
    
    4. **Build** islands separately and roughly (Kaspar: "don't focus on tweaking all the points"). Stack via `R.setup_cage` (Mirror clip > Shrinkwrap Target Normal Project > Subsurf hidden in edit mode). GATE: topology gate below passes per island.
    5. **Relax and freeze** hand-built layouts: `R.relax`, `R.freeze`. Not after QuadriFlow (worsened coverage 0.44 % to 0.55 %).
    6. **Connect** by counts: `R.boundary_loops`, then `R.bridge` / `R.weld_loops` on equal counts; reduce the larger side first (Kaspar: wrist 10 vs hand 16 was "six too many"). GATE: no triangles created.
    7. **Volume**: film `R.fit_subdiv` (gentle defaults); game `R.push_outside` then `R.triangulate_twisted` and a Triangulate modifier on export.
    8. **Review and hand off**: gates, two render sheets, handoff hygiene (P13). Report what the agent did not plan.
    
    ## What an agent reaches (measured on the sheep; `tests/code/blender-retopology/sheep_results.json` and `compare/metrics.json`)
    
    | Strategy                                                                                                                   | Faces | Subdiv-2 mean error | Inside | Eye / mouth rings                              | Closed-loop edges | Looks                                                                                                                           |
    | -------------------------------------------------------------------------------------------------------------------------- | ----- | ------------------- | ------ | ---------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
    | Kaspar's own retopo                                                                                                        | 2,596 | 0.24 %              | 47 %   | 5,5 / 2                                        | 29.9 %            | clean, reads unsubdivided                                                                                                       |
    | QuadriFlow, area-matched budget                                                                                            | 4,372 | 0.14 %              | 87 %   | 0 / 0                                          | 1.6 %             | faceted, torn ear, uniform density                                                                                              |
    | + gentle `fit_subdiv`                                                                                                      | 4,372 | 0.045 %             | 53 %   | 0 / 0                                          | 1.6 %             | smooth masses, lumpy hooves                                                                                                     |
    | + Multires Apply Base instead                                                                                              | 4,372 | 0.11 %              | 15 %   | 0 / 0                                          | 1.6 %             | overshoot                                                                                                                       |
    | Hybrid: eye rings + ear tubes + gentle fit                                                                                 | 4,544 | 0.042 %             | 52 %   | 3,3 / 0                                        | 5.9 %             | first skill version                                                                                                             |
    | E1 run with the skill (fresh agent: face sets, warp, 4 eye rings, 5 lip rings, limb tubes)                                 | 5,778 | 0.040 %             | 51 %   | 4,4 / 5 (46 to 48 verts)                       | 19.4 %            | planned face, rippled lids, lumps at leg joins                                                                                  |
    | v2 tools, final (`test_sheep_v2.py`: face sets, counts as ranges, corner split, geodesic rings and tubes, nasolabial loop) | 4,980 | 0.028 %             | 52 %   | 4,4 clean 24-vert rings, 12/12 / 5 x 30, 15/15 | 33.9 %            | best agent result: poles 5.7 % (Kaspar 5.2 %), none on clean rings, no 6-poles; nose and upper-lip patch irregular (human pass) |
    
    Numbers flatter the agent: an aggressive fit scored 0.043 % and looked lumpier than Kaspar's cage. What stays human-level only: articulation counts with handle loops, crease loops placed from expression shapes, pole routing and loop budgets out of the face. Say so in the report.
    
    ## Numbers
    
    | Item                          | Value                                                                                                         | Source                                           |
    | ----------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
    | Render subdivision (film)     | 2 levels; viewport 1                                                                                          | Kaspar Live #6 [00:21:53]; BCON22 frame 00:26:38 |
    | Joint                         | 3 loops over the bending part                                                                                 | Kaspar Live #5 [01:37:48]                        |
    | Limb start ring               | 8 verts (legs 8 then 12); Dikko arms 8 min, 10 to 12 typical, legs 18                                         | Kaspar Live #4 [00:06:54]; Dikko [00:13:47]      |
    | Eyelid                        | 2 loops per lid crease + ~4 across; 10/10 spans example                                                       | Kaspar Live #1 [02:11:22]; Dikko [00:04:16]      |
    | Studio mesh                   | 99.9 %+ quads, 0 to 4 valence 6+ (report and justify each), poles ~1.8 % head, ~5 % body, edge CV 0.33 to 0.8 | calibration (bx_audit on Blender Studio files)   |
    | Auto-remesh hint (not a gate) | edge CV ~0.17 to 0.27 on an unedited uniform remesh, near 0 % closed loops                                    | calibration, sheep                               |
    | Game                          | head 2,000 to 3,000 tris, hands ~1,000 both, body 5,000 to 6,000                                              | SpeedChar ep23 [00:09:58], ep24 [00:50:34]       |
    | Apply Base levels             | 2 body, 3 head, hair, clothes                                                                                 | Kaspar Live #5 [02:15:03]                        |
    
    ## Quality gates
    
    Code (thresholds [added], calibrated on Kaspar's sheep: 0.24 % mean, 47 % inside, coverage p95 0.82 %):
    
    ```python
    retopo_gate = R.topology_report(retopo)
    fit_gate = R.fidelity(retopo, sculpt, levels=2)
    gate = {"six_poles_to_justify": retopo_gate["valence_6plus"], "no_rim_poles": retopo_gate["rim_poles"] == 0,
            "quads": retopo_gate["non_quads_visible"] == 0 or retopo_gate["quads_pct"] >= 99.0,
            "subd_fit": fit_gate["mean_pct"] <= 0.3 and 40 <= fit_gate["inside_pct"] <= 60,
            "coverage": fit_gate["cov_p95_pct"] <= 1.0, "loops": R.loop_stats(retopo)}
    print(gate)
    ```
    
    Edge-length CV is never pass/fail (studio meshes read 0.33 to 0.8); 6-poles are reported and justified, not banned (studio 0 to 4). Also: `crease_poles == 0`, `center_poles == 0` on mirrored faces, `R.loops_around(retopo, eye, normal, max_radius)` returns at least 3 closed rings per eye and mouth, ring counts equal before every merge, triangles only in hidden or rigid areas, budget met (`R.tri_count`). Visual (`bx_review`, same views as the sculpt): cage in wire (rings close, no wandering loops, poles in still areas) and subdivided matcap (lumps, pinching at poles, creases held), then the cage alone: it must read without subdivision (Kaspar Finale [01:58:41]). Score with [`references/critique.md`](references/critique.md).
    
    ## Common mistakes
    
    | Mistake                                                   | Looks like                                           | Fix                                                                                          |
    | --------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- |
    | Carving around QuadriFlow's closed eye slits              | every eye ring odd (47, 45), no all-quad fix         | `R.parity_strip` between the two odd boundaries BEFORE carving                               |
    | Counts inherited from a dense cut, or forced far below it | eye rings 46 to 48; or pole density 9.5 %            | `count=(lo, hi)`: each reduction unit costs 4 poles; ranges took the sheep from 9.5 to 5.7 % |
    | 3D-nearest lookups where surfaces touch                   | a band or socket cut bites an ear lying on the cheek | geodesic fields, `curve_verts`, oriented `nearest_vertex` (built into bands and tubes)       |
    | Tight rings for an edge-flow loop                         | a ridge after subdivision                            | band `spacing` = edge length; tight only for creases                                         |
    | Shipping a snapped cage                                   | subdivided surface inside the sculpt (87 %)          | `R.fit_subdiv`                                                                               |
    | Shrinkwrap offset for volume                              | cavities worse                                       | fit, offset 0                                                                                |
    | Raw `quadriflow_remesh` on a small dense mesh             | CANCELED, "needs to be manifold"                     | `R.quadriflow` (scale trick, voxel fallback)                                                 |
    | QuadriFlow symmetry left at its default True              | wrong half on a posed mesh                           | `symmetry=False` unless symmetric                                                            |
    | Relaxing an auto-remesh                                   | tips and ears shrink                                 | relax hand-built layouts only                                                                |
    | Mark Sharp / Seam as reminders                            | shading breaks (4.1+), unwrap cuts                   | `retopo_crease` attribute (P5)                                                               |
    | Grid fill on an odd loop                                  | FINISHED, 0 faces                                    | count first; `R.grid_fill` raises                                                            |
    | Connecting parts early                                    | every new loop runs everywhere                       | islands, count, merge last                                                                   |
    | Trusting mean error                                       | lumpy fit passes                                     | matcap review, gentle fit                                                                    |
    | Re-shrinkwrap after Apply Base                            | volume lost again                                    | never (Kaspar Live #6 [00:20:08])                                                            |
    | Game quads left non-planar                                | lines in the normal map                              | `R.triangulate_twisted`, Triangulate on export                                               |
    
    ## Blender 5.2 notes
    
    - Hidden Wire is the **Retopology overlay** (`overlay.show_retopology`, offset 0.01 m since 4.5); "Project Individual Elements" is `snap_elements_individual = {'FACE_PROJECT'}`. GUI only.
    - Slide Relax is the **Relax Slide** brush asset (strokes need a window, `bx_gui`); headless use `R.relax`. F2, LoopTools, BSurfaces are extensions; `mesh.circularize`, `space_edge_loops_evenly`, `flatten` are built in; `mesh.select_by_pole_count` exists (4.4).
    - The BCON22 build's Shrinkwrap showed Smooth Factor 0.05 / Repeat 1 (frame 00:22:53); 5.2.1's Shrinkwrap has no such option.
    - `sharp_edge` is always honored since 4.1; Auto Smooth is gone (Smooth by Angle modifier).
    - Apply Base = `object.multires_base_apply(apply_heuristic=True)`; QuadriFlow lost its shortcut and has the traps above.
    
    ## References
    
    - [`references/procedures.md`](references/procedures.md): tested code, P0 to P14 (inspect, face sets P1b, stack, tubes, geodesic limbs P3b, freeze/relax, annotations, QuadriFlow, map-line loops and rings, sockets, volume, checks, game, expression test, handoff, AI-mesh finishing). Load before writing any code.
    - [`references/expert-notes.md`](references/expert-notes.md): principles by expert with timestamps, face and body loop maps, clothing, game rules, the sheep experiment in full. Load when planning a face, body, clothing or game mesh.
    - `references/critique.md`: the rubric to judge your own retopo. Load at every review.
    - [`references/sources.md`](references/sources.md): all 24 source videos with best timestamps.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related