feat: clothing lane, character sources, and DCC bridges
Bulk import of the working lanes that were living untracked on the PC. Content: - characters/ Lena/male body lanes, bakes, texture work, run logs - clothing/ garment pipeline, configs, gates, contract docs - garments/ MD-authored garment sources (.zprj/.zpac) - UAL-Lib/ Universal Animation Library 2 source (.blend/.fbx/.glb) - tools/ blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent - docs/, plans/, dev/, .agents/plans/ Repo hygiene: - .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out on purpose — ~250 are already tracked raw and converting them would rewrite every one without shrinking history. - .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies dropped by a `>/dev/null` redirect on Windows). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
# Cape v1 -- open-front feathered cloak, 3 panels, hung from the shoulders.
|
||||
#
|
||||
# python tools/md_bridge.py --file tools/tailor/md_cape_v1.py --timeout 700
|
||||
#
|
||||
# Reference: tools/tailor/references/cape_test1_ref.png (5 views incl. the BACK,
|
||||
# which matters because ExportSnapshot3D structurally cannot show a rear -- see
|
||||
# the skill's references/tooling.md SS1.1. A cape is a back garment, so the
|
||||
# turntable is the PRIMARY view here, not an afterthought.)
|
||||
#
|
||||
# -- WHAT THE REFERENCE ACTUALLY IS -------------------------------------------
|
||||
# A feather cloak: densely layered teal/turquoise feathers, a stand-up red collar
|
||||
# with a woven diamond band, and twin vertical bands with shell-button rows down
|
||||
# both front edges. Full length, open down the front.
|
||||
#
|
||||
# Per doctrine (SKILL.md, "textiles not tailoring"): the feathers are the DESIGN,
|
||||
# not the tailoring. ~500 feathers as geometry would blow the game tri budget
|
||||
# outright. So v1 models the SHAPE only -- a plain cloak -- and the feathers,
|
||||
# collar band and button rows become a generated texture in v2. Judge shape with
|
||||
# the texture OFF (PLAIN=True): cloth renders white on its front face and grey on
|
||||
# its back, so grey-from-outside is inside out and a white streak is a fold. A
|
||||
# busy feather motif hides both completely.
|
||||
#
|
||||
# -- CONSTRUCTION -------------------------------------------------------------
|
||||
# Three panels: one back, two fronts. Topology is the pari's (front+back+shoulder
|
||||
# seams) extended to cloak length, with the front split down the middle and no
|
||||
# armholes.
|
||||
#
|
||||
# SIDE SEAMS RUN HIP->SHOULDER ONLY. Below the hip both sides are open vents.
|
||||
# This is the one deliberate departure from a coat and it is load-bearing:
|
||||
# - Fully sewn sides would make a closed tube around the legs at knee length,
|
||||
# which is precisely the piupiu defect (hem 0.5 m below the hip pivot, 30 deg
|
||||
# of hip flexion sweeps the leg ~25 cm forward -- no silhouette clears that).
|
||||
# - Open front + side vents let the leg-swing envelope escape through a gap
|
||||
# instead of driving through cloth. Leg visible through a vent is CORRECT.
|
||||
# - Sewn hip->shoulder still constrains the garment enough that
|
||||
# SetPatternStrengthen is safe (tooling.md SS6: strengthening ROTATES an
|
||||
# under-constrained garment -- a shoulder-only cape would be exactly that).
|
||||
# These are whole-edge seams, not partial ones: the vent boundary is a real point
|
||||
# inserted in the outline at hip height, which splits the side into two distinct
|
||||
# LINES. Partial seams twist unpredictably; cutting features into the outline is
|
||||
# how the tee's neck gap and the piupiu's 32 teeth were done.
|
||||
#
|
||||
# -- SEAM FLAG DERIVATION (do not "simplify" this) ----------------------------
|
||||
# A closed polygon must traverse one way around, so a symmetric garment cut from
|
||||
# one outline ALWAYS has its left edges running opposite to its right edges. That
|
||||
# is why the flags differ per side, and it is not a copy-paste artefact:
|
||||
#
|
||||
# RIGHT pairs -> (False, False)
|
||||
# back L_SIDE_R (hip->shoulder) vs frontR L_SIDE (hip->shoulder) same dir
|
||||
# back L_SHLDR_R (outer->inner) vs frontR L_SHLDR(outer->inner) same dir
|
||||
# LEFT pairs -> (True, False) <- exactly ONE reversal
|
||||
# back L_SIDE_L (shoulder->hip) vs frontL L_SIDE (hip->shoulder) opposed
|
||||
# back L_SHLDR_L (inner->outer) vs frontL L_SHLDR(outer->inner) opposed
|
||||
#
|
||||
# Reversing BOTH flags is equivalent to reversing NEITHER in terms of which
|
||||
# endpoints meet ((T,T) == (F,F) for correspondence), so a pair whose traversals
|
||||
# oppose needs exactly one flip. Getting this wrong pairs shoulder-to-hip and
|
||||
# twists the panel -- and a twisted seam and surplus cloth look IDENTICAL once
|
||||
# textured, which is why PLAIN=True comes first.
|
||||
#
|
||||
# The front-left panel is drafted as an explicit mirror with its point list
|
||||
# ordered to match, rather than reusing the front-right list offset by dx.
|
||||
# tooling.md SS3.3: mirrored coords with the list order kept makes the garment
|
||||
# FALL OFF, because mirroring permutes the indices.
|
||||
#
|
||||
# -- v1a: THE CAPE MUST HANG FROM STRAPS, NOT BE A TUBE (fixed 2026-08-04) ----
|
||||
# First run put W_TOP_B at 700 mm and the garment PUDDLED ON THE FLOOR -- probe
|
||||
# measured span 0.198 m with 100% of verts below the knee. Cause is geometric, not
|
||||
# physical: the shoulder seams sit at the back panel's top corners, so a 700 mm top
|
||||
# edge places them 700 mm apart on a body whose shoulder_width is 319 mm. Both
|
||||
# seams hung in mid-air ~190 mm outboard of the arms, the garment caught on
|
||||
# nothing, and a tube wider than the 1095 mm hips has nothing to stop it.
|
||||
#
|
||||
# This is SKILL.md's "straps beat tubes / a strapless tube slides down to the
|
||||
# narrowest catch" arriving from an unfamiliar direction -- it reads as a skirt
|
||||
# rule, but it governs any garment whose support is a seam rather than a closure.
|
||||
# So: W_TOP_B now spans the SHOULDERS (340 mm = 319 + ease) and the panel flares to
|
||||
# a 950 mm hem below. The shoulder seams become 95 mm straps landing on the
|
||||
# acromions -- the same strap width the pari settled on.
|
||||
#
|
||||
# It also matches the reference better than the wide version did: a narrow yoke
|
||||
# flaring outward IS what the photo shows, including the deep V at the chest.
|
||||
#
|
||||
# SIDE-SEAM LENGTHS ARE NOW DERIVED, NOT CHOSEN. The front panel's outer edge is
|
||||
# built with the SAME dx/dy as the back's side edge, so the paired edges are equal
|
||||
# BY CONSTRUCTION. Choosing W_HEM_F independently made them 472.3 vs 469.8 -- a
|
||||
# 2.5 mm inequality, and on the piupiu a dismissed 0.075 mm discrepancy turned out
|
||||
# to be a slanted seam that notched the band and exposed the reverse face.
|
||||
#
|
||||
# MIRROR: do NOT re-order the mirrored point list. v1's "clever" reorder
|
||||
# ([pts[0]] + pts[1:][::-1]) permuted the left panel's line indices, so index 3 was
|
||||
# its VENT (552 mm) instead of its shoulder (95 mm) and the left shoulder seam was
|
||||
# sewn to the wrong edge. The plain mirror x -> W_HEM_F - x already yields the
|
||||
# intended traversal. The length fingerprint caught this before any render did.
|
||||
#
|
||||
# -- v1b: THE COLLAR IS STRUCTURAL, NOT DECORATION (fixed 2026-08-04) ---------
|
||||
# With the straps corrected to 95 mm at shoulder width the seam fingerprints came
|
||||
# out exact (95.0 x4, 472.347 x4) and it STILL puddled -- span 0.211 m. The pre-sim
|
||||
# showed why: head through the neck slot, straps sitting at the right width, and
|
||||
# nothing whatsoever holding them there. An open-front cape hanging from two narrow
|
||||
# shoulder straps has no closed loop anywhere. Gravity pulls the back panel
|
||||
# backwards and the fronts forwards, the straps rotate off the acromion, and the
|
||||
# whole thing slides down.
|
||||
#
|
||||
# A real cloak does not hang from its shoulders. It hangs from a CLASP AT THE
|
||||
# THROAT. v1 deferred the reference's stand-up collar as "decoration, do it in v2"
|
||||
# and that was the actual error: the collar is the load-bearing element, and no
|
||||
# amount of strap tuning substitutes for it.
|
||||
#
|
||||
# Fix: CLASP_H of each front panel's inner edge is sewn to its opposite number,
|
||||
# closing the throat into a ring of (back neckline 230 mm + 2 x CLASP_H). That ring
|
||||
# is far smaller than the shoulder girth, so the garment physically cannot slide
|
||||
# down -- the same "cut it smaller than what is below it" trick the piupiu waistband
|
||||
# uses against the hips, applied at the neck.
|
||||
#
|
||||
# The clasp is a whole-edge seam: a point inserted in the outline at
|
||||
# PANEL_H - CLASP_H splits each inner edge into a CLASP line and a FRONT_OPEN line.
|
||||
# The two front panels ARE true mirrors of each other, so this one pairs same-index
|
||||
# with (False, False) -- the documented case, unlike the front-to-back seams.
|
||||
#
|
||||
# v2 can replace the clasp with the reference's actual collar band (a strip cut
|
||||
# shorter than the neckline so it stands up and cinches). The clasp is the minimum
|
||||
# that makes the physics correct.
|
||||
#
|
||||
# -- v1c: IT STARTED AROUND HER FOREHEAD (fixed 2026-08-04) -------------------
|
||||
# The clasp did not stop the fall either, so the failure was traced rather than
|
||||
# guessed at again. A stepped sim measuring z-span per step separated two
|
||||
# hypotheses cleanly:
|
||||
# H1 seams never created -> panels fall as independent sheets REFUTED
|
||||
# all 5 AddSeamlinePairGroup calls returned True, GetSeamlinePairGroupCount
|
||||
# == 5, and GetSeamlinePairGroupListInPattern showed back{0,1,2,3}
|
||||
# front_r{0,1,4} front_l{2,3,4} -- exactly the intended wiring.
|
||||
# H2 correctly sewn, nothing supports it -> slides as a unit CONFIRMED
|
||||
# span held 1.000 -> 0.989 -> 0.978 while top fell
|
||||
# 1.644 -> 1.589 -> 1.121 -> 0.598. An intact garment sliding off.
|
||||
#
|
||||
# And the trace handed over the actual cause: step_000 top = 1.644 m. The garment
|
||||
# was being MATERIALISED AROUND HER FOREHEAD (head top 1.777), so the 470 mm throat
|
||||
# ring began clamped around the skull, interpenetrating it. MD resolves a frame-0
|
||||
# intersection by shoving the cloth out of the body -- downward here -- and once the
|
||||
# stiffened assembly had that momentum it plowed straight past the shoulders. The
|
||||
# ring never got a chance to seat.
|
||||
#
|
||||
# ALWAYS CAPTURE THE RETURN VALUES. v1a/v1b threw away every AddSeamlinePairGroup
|
||||
# result, so "the seams took" stayed an assumption through two failed sims when one
|
||||
# boolean would have settled it.
|
||||
#
|
||||
# ARR_Y was then swept with zero simulation frames -- step_000 is measurable
|
||||
# straight after ResetClothArrangement, so the whole question cost seconds instead
|
||||
# of one sim per guess. The control (ARR_Y=50) reproduced 1.644 exactly, so the
|
||||
# harness was valid. Also learned: the arrangement POINT does not move the panel
|
||||
# vertically at all (Body_Back_Center_2 registered identically to _1); only ARR_Y
|
||||
# does. Neck_Collar stretched the span to 1.31 m -- do not use it for a panel.
|
||||
#
|
||||
# -- v1d: THE "RING" WAS PINCHED SHUT (fixed 2026-08-04) ----------------------
|
||||
# Correcting the start height did not stop the fall either, because the throat ring
|
||||
# I thought I had built did not exist. The clasp ran from the VERY TOP of each front
|
||||
# panel's inner edge -- but that top point is already sewn to a back neck corner. So
|
||||
# sewing front-R's top to front-L's top forced the back's two neck corners onto each
|
||||
# other, collapsing the 230 mm back neckline into a pinched slit. There was no neck
|
||||
# hole at all: the garment was a closed bag hanging beside the body, and a bag has
|
||||
# nothing to hang ON.
|
||||
#
|
||||
# A cloak neckline needs THREE segments on that inner edge, not two:
|
||||
# [NECKLINE_H free] the neck hole, wrapping from the back neck corner to the throat
|
||||
# [CLASP_H sewn] the throat closure, at the collarbone
|
||||
# [rest free] the open front
|
||||
# Neck hole = back neckline 230 + 2 x NECKLINE_H = 430 mm around a 392 mm neck, and
|
||||
# the torso immediately below it is 1083 mm, so it cannot pass.
|
||||
#
|
||||
# GENERAL LESSON worth more than this garment: when a seam endpoint is ALREADY
|
||||
# seamed to something else, sewing it again constrains that other thing too. Two
|
||||
# seams sharing an endpoint are one constraint on three panels. Check what a seam's
|
||||
# ENDPOINTS are already attached to before adding it -- the length fingerprint
|
||||
# cannot see this class of error, because every individual edge length was correct.
|
||||
#
|
||||
# -- v1e: THE ACTUAL CAUSE -- OffsetX (fixed 2026-08-04) ----------------------
|
||||
# THIS is what made it fall, through five runs and four wrong hypotheses. Every
|
||||
# arrangement point carries its OWN default ArrangementOffsetX/Y/Z, readable from
|
||||
# GetArrangementList(), and for this garment they differ BY DESIGN:
|
||||
# Body_Back_Center_1 X=0 Y=90 Z=50
|
||||
# Body_Front_1_L / _R X=60 Y=90 Z=50
|
||||
# Every run up to here forced all three to X=50. With the point defaults restored
|
||||
# the cape HOLDS -- top 1.586 m against 0.20 m for the X=50 control, same geometry,
|
||||
# same seams, same everything else.
|
||||
#
|
||||
# The piupiu had already written this lesson down -- "the x arg selects which half
|
||||
# each panel wraps and the split is load-bearing" -- and its header even warns not to
|
||||
# harmonise x across recipes. I harmonised it. The generalisation the repo was
|
||||
# missing, now recorded: DO NOT INVENT ARRANGEMENT OFFSETS. Read the point's own
|
||||
# defaults and override only what you specifically need (here, the height).
|
||||
#
|
||||
# Cost of getting there: 5 failed 300-frame sims. What would have caught it in one:
|
||||
# dumping GetArrangementList()'s full entries BEFORE the first drape instead of
|
||||
# projecting them down to {name: index}. The index was the only field being read and
|
||||
# four other fields were carrying the answer.
|
||||
#
|
||||
# Also disproved along the way, so nobody re-tries them: seam wiring was NOT crossed
|
||||
# (swapping arrangement points and swapping the seam pairing both still fell -- the X
|
||||
# in the top-down view was a symptom, not the cause), and the avatar collides fine.
|
||||
#
|
||||
# -- LENGTH IS THE OPEN RISK --------------------------------------------------
|
||||
# The reference is a product shot on an invisible mannequin, so it carries no
|
||||
# absolute scale and capes read longer without a body in them. HEM_Z 0.42 m
|
||||
# (just below Lena's 0.517 m knee) is a judgement call matched to the reference's
|
||||
# drama, and it sits 0.53 m below the hip pivot -- the same envelope that beat the
|
||||
# piupiu. The vents are the mitigation; G5 is the judge. If G5 reports real
|
||||
# penetration, SHORTENING is one parameter here, not a re-author.
|
||||
#
|
||||
# QC targets (measured off the probe OBJ below, not eyeballed -- eyeballing is how
|
||||
# garments shipped 8-14 cm off their own written targets):
|
||||
# top edge 1.40 m +-3 cm (shoulder line, 1.3973)
|
||||
# hem 0.42 m +-5 cm
|
||||
#
|
||||
# -- STATUS 2026-08-04: SHAPE HOLDS, DECORATION NOT STARTED -------------------
|
||||
# Measured on the exported drape: top 1.508 m, hem 0.449 m (IN tolerance), span
|
||||
# 1.060, hem circumference 2350 mm, 4315 verts (already game budget). Radius by
|
||||
# height: chest 0.188, hip 0.213, knee 0.228 -- it flares outward instead of
|
||||
# clinging, which is what the reference does. Exported to lena_cape_v1.{zprj,fbx,
|
||||
# obj,zpac}; screenshots front + back in screenshots/cape_v1_*.png.
|
||||
#
|
||||
# top 1.508 is 10 cm above the 1.40 target and that is FINE, arguably better: the
|
||||
# garment self-seats at the neck rather than the shoulder line, which reads as the
|
||||
# reference's stand-up collar. Do not "correct" it downward without looking.
|
||||
#
|
||||
# OPEN DEFECT -- INVERTED LAPEL. The front view shows a GREY band across the upper
|
||||
# chest, and grey from outside means inside out. The NECKLINE_H free cloth above the
|
||||
# clasp has nowhere to go once the clasp pulls the panels together, so it folds
|
||||
# outward and presents its reverse face. Options, in order of preference:
|
||||
# 1. Build the reference's actual COLLAR BAND -- a strip sewn along the whole
|
||||
# neckline, cut shorter than it so it stands up. Fixes the fold by giving the
|
||||
# neckline a structural edge, and is needed for fidelity anyway.
|
||||
# 2. Shrink NECKLINE_H so there is less loose cloth to fold.
|
||||
# Do NOT reach for a seam-flag change: the clasp pairs same-index on TRUE mirrors,
|
||||
# which is the documented (False, False) case, and the fold is slack, not a twist.
|
||||
#
|
||||
# STILL TO DO for reference fidelity (all of it is texture/trim, not tailoring):
|
||||
# feather field, stand collar band, twin front bands with shell-button rows, shaggy
|
||||
# hem (the piupiu's teeth-cut-into-the-outline technique transfers directly).
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
# -- parameters -- this block exists so iteration is one edit -----------------
|
||||
PANEL_H = 1000.0 # mm, hem (y=0) to shoulder line (y=PANEL_H). 2D y+ = UP in 3D;
|
||||
# drafting y-down drapes the garment upside-down over the head.
|
||||
W_TOP_B = 340.0 # mm, back panel at the shoulder line. MUST span the shoulders
|
||||
# (shoulder_width 319 + ease), NOT the cloak's width -- the
|
||||
# shoulder seams live at this edge's corners and are the only
|
||||
# thing holding the garment up. 700 here puddled it on the floor.
|
||||
W_HEM_B = 1250.0 # mm, back panel at the hem. ALL the volume lives here -- the top
|
||||
# edge cannot grow (that is the strapless-tube failure), so drama
|
||||
# has to come from flare. 950 read as a close-fitting A-line dress
|
||||
# in the back view rather than the reference's voluminous cloak.
|
||||
# Everything downstream of this is DERIVED, so the paired seam
|
||||
# edges stay equal automatically when you change it.
|
||||
NECK_W = 150.0 # mm, back neck scoop width (neck_circ 392 -> ~125 dia + ease)
|
||||
NECK_D = 40.0 # mm, back neck scoop depth
|
||||
FRONT_GAP = 60.0 # mm, the open front. Reference shows a narrow gap showing lining.
|
||||
NECKLINE_H = 100.0 # mm of each front inner edge left FREE at the top. This is the
|
||||
# neck hole, and it must exist -- see the v1d note. Neck hole
|
||||
# perimeter = back neckline (NECK_D + NECK_W + NECK_D = 230)
|
||||
# + 2*NECKLINE_H = 430 mm against a 392 mm neck: snug.
|
||||
CLASP_H = 120.0 # mm sewn to the opposite front panel, BELOW the neckline, closing
|
||||
# the throat at the collarbone. THIS IS WHAT HOLDS THE CAPE ON.
|
||||
# Below the neck hole the torso is 1083 mm around, so a 430 mm
|
||||
# hole physically cannot pass it -- the same "cut it smaller than
|
||||
# what is below it" trick the piupiu waistband uses on the hips.
|
||||
|
||||
SHOULDER_Z = 1397.3 # mm, Lena's shoulder. Panel y=PANEL_H maps here.
|
||||
HIP_Z = 945.5 # mm, hip pivot -- where the side seam STOPS and the vent starts.
|
||||
Y_HIP = PANEL_H - (SHOULDER_Z - HIP_Z) # = 548.2 in panel space
|
||||
|
||||
# -- DERIVED, so paired seam edges are equal BY CONSTRUCTION ------------------
|
||||
INSET_B = (W_HEM_B - W_TOP_B) / 2.0 # 305.0 -- back's flare per side
|
||||
SLOPE = INSET_B / PANEL_H # dx/dy of the back's side edge
|
||||
SIDE_RUN = SLOPE * (PANEL_H - Y_HIP) # horizontal run of the sewn side seam
|
||||
W_TOP_F = (W_TOP_B - NECK_W) / 2.0 # 95.0 -- the shoulder STRAP width
|
||||
W_HEM_F = W_TOP_F + INSET_B # 400.0 -- same slope over the full height
|
||||
X_HIP_F = W_TOP_F + SIDE_RUN # front outer edge at the hip
|
||||
|
||||
PARTICLE_MM = 25.0 # This is a BIG garment: back 950x1000 + two 380x1000 fronts.
|
||||
# 20 mm (the piupiu value) puts it at ~4.2k verts; 25 gives
|
||||
# ~2.7k, inside a sane game budget with room for the collar.
|
||||
COLLISION_MM = 10.0 # MD resolves the standoff, so it holds everywhere. Better than
|
||||
# the downstream Blender normal-push, which self-intersects in
|
||||
# concave regions.
|
||||
ARR_BACK = "Body_Back_Center_1"
|
||||
ARR_FRONT_R = "Body_Front_1_R"
|
||||
ARR_FRONT_L = "Body_Front_1_L"
|
||||
USE_POINT_DEFAULT_X = True # DO NOT SET THIS FALSE. See the v1e note: each
|
||||
# arrangement point carries its OWN default OffsetX, and they
|
||||
# differ BY DESIGN -- Body_Back_Center_1 is 0, Body_Front_1_L/R
|
||||
# are 60. Forcing all three to a single value is what made the
|
||||
# cape fall on the floor five runs in a row.
|
||||
ARR_Y = 30 # Height. Swept with a valid control; only Y moves the panel
|
||||
# vertically (the arrangement POINT does not -- Body_Back_Center_2
|
||||
# registers identically to _1). With the correct X, Y trades off
|
||||
# against bunching rather than moving rigidly:
|
||||
# Y=90 -> top 1.631 hem 0.827 span 0.804 (bunched at the neck)
|
||||
# Y=50 -> top 1.586 hem 0.618 span 0.969
|
||||
# The TOP self-seats at the neck/shoulders either way; lower Y
|
||||
# just bunches less, so the hem drops. 30 for a longer line.
|
||||
ZFAB = r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab"
|
||||
# fabric_api has NO physics setters -- stiffness comes from the .zfab. A feather
|
||||
# cloak wants body, so if this reads limp climb the ladder one step at a time:
|
||||
# V2_Woven_Canvas_1.zfab < V2_Woven_Denim_1.zfab < V2_Non-Fabric_Tyvek_1.zfab
|
||||
PLAIN = True # SHAPE FIRST. See the header.
|
||||
EXPORT = True # Guarded until the drape held and was measured (2026-08-04).
|
||||
SIM_MAIN = 250
|
||||
SIM_RELAX = 50
|
||||
VERSION = "v1"
|
||||
|
||||
AVATAR_FBX = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx"
|
||||
OUT = r"C:\Users\Jeremy\tinqs\animation\tools\tailor"
|
||||
SHOTS = r"C:\Users\Jeremy\tinqs\animation\tools\tailor\screenshots"
|
||||
|
||||
report = {"params": {"PANEL_H": PANEL_H, "W_TOP_B": W_TOP_B, "W_HEM_B": W_HEM_B,
|
||||
"W_TOP_F": W_TOP_F, "W_HEM_F": W_HEM_F, "NECK_W": NECK_W,
|
||||
"Y_HIP": round(Y_HIP, 1), "PARTICLE_MM": PARTICLE_MM,
|
||||
"COLLISION_MM": COLLISION_MM, "PLAIN": PLAIN,
|
||||
"EXPORT": EXPORT, "zfab": os.path.basename(ZFAB)}}
|
||||
|
||||
utility_api.NewProject() # NB: deletes the avatar -- import after
|
||||
op = ApiTypes.ImportExportOption()
|
||||
op.scale = 10.0 # Blender-exported FBX avatars import 10x small
|
||||
op.bAddArrangementPoints = True # else there is nowhere to hang cloth
|
||||
op.bAutoTranslate = True
|
||||
report["avatar"] = import_api.ImportFBX(AVATAR_FBX, op)
|
||||
|
||||
# -- outlines ----------------------------------------------------------------
|
||||
# Named landmarks, indices DERIVED. Hardcoded line indices broke this repo's
|
||||
# bodice recipe twice; inserting a point shifts every index after it.
|
||||
CX_B = W_HEM_B / 2.0 # 475.0
|
||||
x_side_b_r = W_HEM_B - SLOPE * Y_HIP # right side edge x at hip
|
||||
x_side_b_l = SLOPE * Y_HIP # left side edge x at hip
|
||||
|
||||
|
||||
def back_outline(dx):
|
||||
"""Back panel, counter-clockwise from the hem-left corner.
|
||||
|
||||
Points, in order: hem, right side (via the HIP VENT POINT), right shoulder,
|
||||
neck scoop, left shoulder, left side (via the hip vent point).
|
||||
"""
|
||||
pts = [
|
||||
(0.0, 0.0), # 0 hem-left
|
||||
(W_HEM_B, 0.0), # 1 hem-right
|
||||
(x_side_b_r, Y_HIP), # 2 right @ hip <- vent boundary
|
||||
(INSET_B + W_TOP_B, PANEL_H), # 3 top-right (shoulder outer)
|
||||
(CX_B + NECK_W / 2.0, PANEL_H), # 4 neck right
|
||||
(CX_B + NECK_W / 2.0, PANEL_H - NECK_D), # 5 scoop
|
||||
(CX_B - NECK_W / 2.0, PANEL_H - NECK_D), # 6 scoop
|
||||
(CX_B - NECK_W / 2.0, PANEL_H), # 7 neck left
|
||||
(INSET_B, PANEL_H), # 8 top-left (shoulder outer)
|
||||
(x_side_b_l, Y_HIP), # 9 left @ hip <- vent boundary
|
||||
]
|
||||
return [(x + dx, y, 0) for (x, y) in pts]
|
||||
|
||||
|
||||
def front_outline(dx, mirror):
|
||||
"""One front panel. Inner edge (the open front) is vertical at local x=0.
|
||||
|
||||
mirror=False -> body's RIGHT: outer edge on the high-x side.
|
||||
mirror=True -> body's LEFT : the plain mirror x -> W_HEM_F - x. Do NOT
|
||||
re-order the list afterwards. v1 did, and the reorder permuted
|
||||
the line indices so index 3 was the 552 mm VENT instead of the
|
||||
95 mm shoulder strap -- the left shoulder seam was then sewn to
|
||||
the wrong edge. The plain mirror already yields the intended
|
||||
traversal (hem, vent, side hip->shoulder, shoulder outer->inner,
|
||||
front opening), just with outer now on the LOW-x side.
|
||||
"""
|
||||
pts = [
|
||||
(0.0, 0.0), # inner hem
|
||||
(W_HEM_F, 0.0), # outer hem
|
||||
(X_HIP_F, Y_HIP), # outer @ hip <- vent boundary, derived
|
||||
(W_TOP_F, PANEL_H), # outer top (shoulder strap outer)
|
||||
(0.0, PANEL_H), # inner top -- meets the BACK's neck corner
|
||||
(0.0, PANEL_H - NECKLINE_H), # neckline bottom / clasp top
|
||||
(0.0, PANEL_H - NECKLINE_H - CLASP_H), # clasp bottom <- throat closure
|
||||
]
|
||||
if mirror:
|
||||
pts = [(W_HEM_F - x, y) for (x, y) in pts]
|
||||
return [(x + dx, y, 0) for (x, y) in pts]
|
||||
|
||||
|
||||
pts_b = back_outline(600.0)
|
||||
pts_fr = front_outline(0.0, mirror=False)
|
||||
pts_fl = front_outline(1700.0, mirror=True)
|
||||
|
||||
pb = pattern_api.CreatePatternWithPoints(pts_b)
|
||||
pfr = pattern_api.CreatePatternWithPoints(pts_fr)
|
||||
pfl = pattern_api.CreatePatternWithPoints(pts_fl)
|
||||
report["ids"] = {"back": pb, "front_r": pfr, "front_l": pfl}
|
||||
|
||||
# Line i runs pts[i] -> pts[i+1]; the last line closes back to pts[0].
|
||||
B = {"hem": 0, "vent_r": 1, "side_r": 2, "shldr_r": 3,
|
||||
"neck_a": 4, "neck_b": 5, "neck_c": 6, "shldr_l": 7, "side_l": 8, "vent_l": 9}
|
||||
F = {"hem": 0, "vent": 1, "side": 2, "shldr": 3,
|
||||
"neckline": 4, "clasp": 5, "front_open": 6}
|
||||
report["lines"] = {"back_n_pts": len(pts_b), "front_n_pts": len(pts_fr),
|
||||
"back": B, "front": F}
|
||||
|
||||
# Fingerprint by LENGTH before sewing. Paired edges must be EQUAL, not close --
|
||||
# a dismissed 0.075 mm discrepancy on the piupiu turned out to be a slanted seam
|
||||
# that notched the waistband and exposed the reverse face.
|
||||
try:
|
||||
# Both panels' side edges share SIDE_RUN and (PANEL_H - Y_HIP), so this single
|
||||
# expected length applies to all four -- that is the point of deriving them.
|
||||
side_len = (SIDE_RUN ** 2 + (PANEL_H - Y_HIP) ** 2) ** 0.5
|
||||
report["derived_mm"] = {"INSET_B": round(INSET_B, 2), "SLOPE": round(SLOPE, 4),
|
||||
"SIDE_RUN": round(SIDE_RUN, 2), "W_TOP_F": W_TOP_F,
|
||||
"W_HEM_F": W_HEM_F, "X_HIP_F": round(X_HIP_F, 2),
|
||||
"hem_circ": round(W_HEM_B + 2 * W_HEM_F, 1)}
|
||||
report["expect_mm"] = {"shoulder": W_TOP_F, "side_all_four": round(side_len, 2)}
|
||||
report["measured_mm"] = {
|
||||
"back_shldr_r": pattern_api.GetLineLength(pb, B["shldr_r"]),
|
||||
"back_shldr_l": pattern_api.GetLineLength(pb, B["shldr_l"]),
|
||||
"front_r_shldr": pattern_api.GetLineLength(pfr, F["shldr"]),
|
||||
"front_l_shldr": pattern_api.GetLineLength(pfl, F["shldr"]),
|
||||
"back_side_r": pattern_api.GetLineLength(pb, B["side_r"]),
|
||||
"back_side_l": pattern_api.GetLineLength(pb, B["side_l"]),
|
||||
"front_r_side": pattern_api.GetLineLength(pfr, F["side"]),
|
||||
"front_l_side": pattern_api.GetLineLength(pfl, F["side"]),
|
||||
"front_r_clasp": pattern_api.GetLineLength(pfr, F["clasp"]),
|
||||
"front_l_clasp": pattern_api.GetLineLength(pfl, F["clasp"]),
|
||||
"front_r_neckline": pattern_api.GetLineLength(pfr, F["neckline"]),
|
||||
"front_l_neckline": pattern_api.GetLineLength(pfl, F["neckline"]),
|
||||
}
|
||||
report["neck_hole_mm"] = 2 * NECK_D + NECK_W + 2 * NECKLINE_H # vs neck_circ 392
|
||||
except Exception as e:
|
||||
report["line_length_error"] = "%s: %s" % (type(e).__name__, e)
|
||||
|
||||
# -- seams -- flags derived in the header, ONE reversal on the left ----------
|
||||
# CAPTURE THE RETURN VALUES. v1a/v1b discarded them and "the seams took" stayed an
|
||||
# assumption through two failed sims when one boolean would have settled it.
|
||||
report["seams"] = {
|
||||
"shldr_r": pattern_api.AddSeamlinePairGroup(pb, B["shldr_r"], pfr, F["shldr"], False, False),
|
||||
"side_r": pattern_api.AddSeamlinePairGroup(pb, B["side_r"], pfr, F["side"], False, False),
|
||||
"shldr_l": pattern_api.AddSeamlinePairGroup(pb, B["shldr_l"], pfl, F["shldr"], True, False),
|
||||
"side_l": pattern_api.AddSeamlinePairGroup(pb, B["side_l"], pfl, F["side"], True, False),
|
||||
# The throat clasp -- the seam that actually holds the cape on. The two front
|
||||
# panels are TRUE mirrors of each other, so this is the documented same-index
|
||||
# (False, False) case, and both clasp lines run top -> down. It sits BELOW the
|
||||
# free neckline: clasping at the top pinches the neck hole shut (v1d).
|
||||
"clasp": pattern_api.AddSeamlinePairGroup(pfr, F["clasp"], pfl, F["clasp"], False, False),
|
||||
}
|
||||
try:
|
||||
report["seam_count"] = pattern_api.GetSeamlinePairGroupCount()
|
||||
except Exception as e:
|
||||
report["seam_count_err"] = "%s: %s" % (type(e).__name__, e)
|
||||
|
||||
fab = fabric_api.AddFabric(ZFAB) # index 0 is the SHARED default -- colouring
|
||||
# it dyes every garment in the scene
|
||||
for p in (pb, pfr, pfl):
|
||||
pattern_api.SetPatternPieceFabricIndex(p, fab)
|
||||
|
||||
for setter, val, key in ((pattern_api.SetParticleDistanceOfPattern, PARTICLE_MM, "particle"),
|
||||
(pattern_api.SetAddlThicknessCollision, COLLISION_MM, "collision")):
|
||||
if not val:
|
||||
continue
|
||||
try:
|
||||
for p in (pb, pfr, pfl):
|
||||
setter(p, val)
|
||||
report[key] = val
|
||||
except Exception as e:
|
||||
report[key + "_error"] = "%s: %s" % (type(e).__name__, e)
|
||||
|
||||
# Arrangement indices REGENERATE per avatar import -- always look up by name. Keep
|
||||
# the WHOLE entry, not just the index: the per-point default offsets are the payload.
|
||||
arr = {str(a["ArrangementName"]): a for a in pattern_api.GetArrangementList()}
|
||||
missing = [n for n in (ARR_BACK, ARR_FRONT_R, ARR_FRONT_L) if n not in arr]
|
||||
report["arr_missing"] = missing
|
||||
if not missing:
|
||||
used = {}
|
||||
for p, nm in ((pb, ARR_BACK), (pfr, ARR_FRONT_R), (pfl, ARR_FRONT_L)):
|
||||
a = arr[nm]
|
||||
pattern_api.SetArrangement(p, int(a["ArrangementIndex"]))
|
||||
# Take the point's own X and Z; override only the height. SetArrangementPosition
|
||||
# takes 4 INTS -- a float raises TypeError.
|
||||
ox = int(a["ArrangementOffsetX"]) if USE_POINT_DEFAULT_X else 50
|
||||
oz = int(a["ArrangementOffsetZ"])
|
||||
pattern_api.SetArrangementPosition(p, ox, int(ARR_Y), oz)
|
||||
used[nm] = {"x": ox, "y": int(ARR_Y), "z": oz,
|
||||
"orientation": str(a["ArrangementOrientation"]),
|
||||
"type": str(a["ArrangementType"])}
|
||||
report["arrangement_used"] = used
|
||||
|
||||
for p in (pb, pfr, pfl):
|
||||
pattern_api.SetPatternStrengthen(p, True) # stiff so cloth wraps, not crumples
|
||||
utility_api.ResetClothArrangement() # SetArrangement only ASSIGNS
|
||||
|
||||
# THE DIAGNOSTIC THAT UNSTICKS EVERYTHING: zero simulation frames, right after the
|
||||
# arrangement is applied. Shows where the panels actually start before physics
|
||||
# muddies it. Seven "seam bug" iterations on the tee were really an upside-down
|
||||
# garment; one pre-sim snapshot would have caught it immediately.
|
||||
utility_api.Refresh3DWindow()
|
||||
utility_api.SetCamViewPoint(2) # front -- always set, so shots compare
|
||||
pre = os.path.join(SHOTS, "cape_%s_presim_front.png" % VERSION)
|
||||
export_api.ExportSnapshot3D(pre)
|
||||
report["presim_front"] = pre
|
||||
utility_api.SetCamViewPoint(5) # top-down: the ONLY view that shows
|
||||
utility_api.Refresh3DWindow() # whether front and back meet over
|
||||
pret = os.path.join(SHOTS, "cape_%s_presim_top.png" % VERSION)
|
||||
export_api.ExportSnapshot3D(pret) # the shoulder
|
||||
report["presim_top"] = pret
|
||||
|
||||
report["sim1"] = utility_api.Simulate(SIM_MAIN) # main settle, still stiff
|
||||
for p in (pb, pfr, pfl):
|
||||
pattern_api.SetPatternStrengthen(p, False) # relax into natural folds
|
||||
report["sim2"] = utility_api.Simulate(SIM_RELAX)
|
||||
utility_api.Refresh3DWindow()
|
||||
|
||||
utility_api.SetCamViewPoint(2)
|
||||
snap = os.path.join(SHOTS, "cape_%s_front.png" % VERSION)
|
||||
export_api.ExportSnapshot3D(snap)
|
||||
report["front"] = snap
|
||||
utility_api.SetCamViewPoint(4)
|
||||
utility_api.Refresh3DWindow()
|
||||
side = os.path.join(SHOTS, "cape_%s_side.png" % VERSION)
|
||||
export_api.ExportSnapshot3D(side)
|
||||
report["side"] = side
|
||||
utility_api.SetCamViewPoint(2)
|
||||
# THE BACK. ExportSnapshot3D cannot show it at any viewpoint -- this is the only
|
||||
# way, and for a cape it is the primary view. Turntable REUSES output*.png names,
|
||||
# so the return value is the only handle on the files: copy them out immediately.
|
||||
report["turntable"] = export_api.ExportTurntableImages(4) # 0 front 1 side 2 BACK 3 side
|
||||
|
||||
# -- measurement -- numbers, not eyeballs ------------------------------------
|
||||
mop = ApiTypes.ImportExportOption()
|
||||
mop.bExportGarment = True
|
||||
mop.bExportAvatar = False
|
||||
probe = os.path.join(tempfile.gettempdir(), "tinqs_md_cape_%s_probe.obj" % VERSION)
|
||||
export_api.ExportOBJ(probe, mop)
|
||||
|
||||
vs = []
|
||||
with open(probe) as fh:
|
||||
for line in fh:
|
||||
if line.startswith("v "):
|
||||
_, x, y, z = line.split()[:4]
|
||||
vs.append((float(x), float(y), float(z)))
|
||||
if vs:
|
||||
S = 0.001 # ExportOBJ writes MM. The FBX->census path reads DECIMETRES
|
||||
# (that is what align.scale_z 0.1 is for) -- they disagree.
|
||||
zs = sorted(v[1] * S for v in vs)
|
||||
cx = sum(v[0] for v in vs) / len(vs)
|
||||
cz = sum(v[2] for v in vs) / len(vs)
|
||||
|
||||
def band(lo, hi):
|
||||
r = [(((v[0] - cx) ** 2 + (v[2] - cz) ** 2) ** 0.5) * S
|
||||
for v in vs if lo <= v[1] * S <= hi]
|
||||
if not r:
|
||||
return None
|
||||
r.sort()
|
||||
return {"n": len(r), "r_med": round(r[len(r) // 2], 3), "r_max": round(r[-1], 3)}
|
||||
|
||||
report["measured"] = {
|
||||
"verts": len(vs),
|
||||
"top_m": round(zs[-1], 3), # target 1.40 +-0.03 (shoulder line)
|
||||
"hem_m": round(zs[0], 3), # target 0.42 +-0.05
|
||||
"span_m": round(zs[-1] - zs[0], 3),
|
||||
"radius_at_chest_1.22_1.30": band(1.22, 1.30),
|
||||
"radius_at_hip_0.90_0.96": band(0.90, 0.96),
|
||||
"radius_at_knee_0.50_0.56": band(0.50, 0.56),
|
||||
"frac_below_knee_0517": round(sum(1 for z in zs if z < 0.517) / len(zs), 3),
|
||||
}
|
||||
|
||||
if EXPORT:
|
||||
base = os.path.join(OUT, "lena_cape_%s" % VERSION)
|
||||
eop = ApiTypes.ImportExportOption()
|
||||
eop.bExportGarment = True
|
||||
eop.bExportAvatar = False
|
||||
report["zprj"] = export_api.ExportZPrj(base + ".zprj")
|
||||
report["fbx"] = export_api.ExportFBX(base + "_garment.fbx", eop)
|
||||
report["obj"] = export_api.ExportOBJ(base + "_garment.obj", eop)
|
||||
report["zpac"] = export_api.ExportZPac(base + ".zpac")
|
||||
|
||||
result = report
|
||||
Reference in New Issue
Block a user