tailor(mako): kilt v2 on Mako via Lena donor-swap -- MD rejects all Mako meshes
First male garment through the MD lane. MD's body analyzer rejects every
Tripo-Mako-derived mesh (0 arrangement points, 'avatar must be in T-pose or
A-pose' despite a verified 0.4-deg-perfect skeleton; same wall as ariki-game
e244e8a). Chimera test pins it on the MESH -- Lena's mesh on Mako's armature
gets 109 points. Ruled out: finger weights, decimation, head/hand chops, leg
spread, rename, OBJ import, export settings.
Workaround that ships: arrange+sew+elastic-lock the garment on a 1.07x-scaled
LENA donor, ImportFBX Mako with bAdd, DeleteAvatar(Lena), re-settle. A
seamless single-wrap slides off Mako's legs during the swap -- only SEWN
garments survive it, so the flat pattern's wrap reads are faked: overlap flap
seamed to a split front-waist segment, comb-teeth fringe (piupiu technique)
carved into the hem, rope belt as rigid Blender geometry (build_rope_belt.py).
Other findings baked into work/mako_kilt_v1/ scripts + config:
- fabric texture anchor: pattern y=0 maps to texture v=0.5 and WRAPS
(striped-probe verified; make_kilt_cloth.py mm_from_row)
- waist elastic never reaches TotalLength at default strength; strength 60
moved placement from 1.019 to 1.043
- SetAvatarActivation returns False (unusable); GetArrangementList dies on
any scene avatar mutation
QC PASS (turntable render, alpha mask, height 1.818): top 1.063 vs 1.054
target (tol .03), fringe tips 0.666 vs 0.665 (tol .04).
Avatars: _fingers = full finger weights restored via UV-space transfer from
rig-work/mako_fullhead_rigged.fbx (build_mako_avatar_fingers.py); shipped-body
avatar landed for future diagnosis. v1 (pre-flap/teeth) kept as provenance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -0,0 +1,26 @@
|
||||
# Import Mako (latest male base, mako_base_v01) as the MD avatar.
|
||||
# Run via: python tools/md_bridge.py --file tools/tailor/_import_mako_avatar.py --timeout 300
|
||||
import os
|
||||
|
||||
AVATAR_FBX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar.fbx"
|
||||
SNAP = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\screenshots\_mako_import_check.png"
|
||||
|
||||
report = {}
|
||||
|
||||
utility_api.NewProject()
|
||||
op = ApiTypes.ImportExportOption()
|
||||
op.scale = 10.0 # Blender-exported FBX lands 10x small
|
||||
op.bAddArrangementPoints = True # ~98 named points to hang cloth on
|
||||
op.bAutoTranslate = True
|
||||
report["avatar_imported"] = import_api.ImportFBX(AVATAR_FBX, op)
|
||||
|
||||
report["avatar_count"] = export_api.GetAvatarCount()
|
||||
report["avatar_names"] = export_api.GetAvatarNameList()
|
||||
arr = pattern_api.GetArrangementList()
|
||||
report["arrangement_points"] = len(arr) if arr else 0
|
||||
|
||||
utility_api.SetCamViewPoint(2) # front view for comparable QC shots
|
||||
snaps = export_api.ExportSnapshot3D(SNAP)
|
||||
report["snapshot"] = SNAP if os.path.exists(SNAP) else snaps
|
||||
|
||||
result = report
|
||||
|
After Width: | Height: | Size: 129 KiB |
@@ -0,0 +1,10 @@
|
||||
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
|
||||
newmtl (Default_for_Simulation)_FRONT_50521
|
||||
Ka 0.257132 0.257132 0.257132
|
||||
Kd 1.000000 1.000000 1.000000
|
||||
Ks 0.075000 0.075000 0.075000
|
||||
Ns 28.763239
|
||||
illum 2
|
||||
d 1.000000
|
||||
map_Ka C:/Users/CAN/tinqs-ltd/animation/tools/tailor/kilt_cloth.png
|
||||
map_Kd C:/Users/CAN/tinqs-ltd/animation/tools/tailor/kilt_cloth.png
|
||||
|
After Width: | Height: | Size: 130 KiB |
@@ -0,0 +1,12 @@
|
||||
# Blender 5.1.2 MTL File: 'None'
|
||||
# www.blender.org
|
||||
|
||||
newmtl RopeBrown
|
||||
Ns 10.000005
|
||||
Ka 1.000000 1.000000 1.000000
|
||||
Kd 0.420000 0.300000 0.170000
|
||||
Ks 0.500000 0.500000 0.500000
|
||||
Ke 0.000000 0.000000 0.000000
|
||||
Ni 1.500000
|
||||
d 1.000000
|
||||
illum 2
|
||||
@@ -0,0 +1,10 @@
|
||||
# MTL Exporter v1.0 by Seungwoo Oh at CLO - Virtual Fashion Inc.
|
||||
newmtl (Default_for_Simulation)_FRONT_88393
|
||||
Ka 0.257132 0.257132 0.257132
|
||||
Kd 1.000000 1.000000 1.000000
|
||||
Ks 0.075000 0.075000 0.075000
|
||||
Ns 28.763239
|
||||
illum 2
|
||||
d 1.000000
|
||||
map_Ka C:/Users/CAN/tinqs-ltd/animation/tools/tailor/kilt_cloth.png
|
||||
map_Kd C:/Users/CAN/tinqs-ltd/animation/tools/tailor/kilt_cloth.png
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 314 KiB |
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
make_kilt_cloth.py -- beige woven cloth with black triangle border band for the
|
||||
male feather-cape kilt (mako_kilt_v1).
|
||||
|
||||
python tools/tailor/textures/make_kilt_cloth.py
|
||||
|
||||
Derived from the reference renders
|
||||
`male-clothing-feather-cape-gpt-piece-kilt{,-front,-back}.png`: a light beige
|
||||
coarse plain weave (harakeke/tapa in character) with a black triangle border
|
||||
band above a short fringe at the hem.
|
||||
|
||||
UNLIKE hunter_cloth THIS TILE IS POSITIONED, NOT GENERIC. It spans the kilt
|
||||
panel's full 390 mm height exactly once (dpi = SIZE / (390 / 25.4)), so the
|
||||
band and fringe land at fixed garment heights:
|
||||
|
||||
garment y (mm, hem=0) zone
|
||||
0-30 fringe strands (drawn, not geometry -- v1)
|
||||
30-90 black band, beige triangles (6 cm, per flat pattern)
|
||||
90-390 plain beige weave
|
||||
|
||||
Horizontally the same 390 mm tiles ~1.7x across the 650 mm panel; the band is
|
||||
horizontal and the triangles repeat 8/tile, so the wrap is seamless.
|
||||
|
||||
IF THE BAND RENDERS AT THE WAIST INSTEAD OF THE HEM, MD anchored the texture
|
||||
the other way up -- regenerate with FLIP=True rather than touching the pattern.
|
||||
|
||||
Same doctrine as make_hunter_cloth.py: albedo baked in (pipeline has no alpha,
|
||||
texture replaces base color), warm ramp r>g>b, deterministic LCG noise.
|
||||
"""
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(HERE, "kilt_cloth.png")
|
||||
|
||||
SIZE = 1024 # px, square
|
||||
CLOTH_MM = 390.0 # spans the kilt panel height exactly once
|
||||
DPI = SIZE / (CLOTH_MM / 25.4)
|
||||
FLIP = False # set True if MD anchors the texture upside-down
|
||||
|
||||
BASE = (206, 188, 152) # light beige albedo (reference lit mean ~(196,180,148))
|
||||
BAND = (44, 38, 30) # near-black band, warm
|
||||
THREAD_PITCH = 8 # px per thread; 1024px/390mm -> ~3 mm threads
|
||||
OVER_LIFT = 12
|
||||
UNDER_DROP = 16
|
||||
ROUND_SHADE = 10
|
||||
SLUB_RANGE = 8
|
||||
MOTTLE = 12
|
||||
MOTTLE_CELL = 64
|
||||
|
||||
# zone boundaries in garment mm (hem = 0)
|
||||
# v2: fringe is CUT GEOMETRY now (comb teeth, 0-30 mm) -- the texture paints the
|
||||
# teeth as strand-beige instead of drawing fake strands. Band triangles refined
|
||||
# 8 -> 12 per tile (32.5 mm) to match the reference's tighter zigzag.
|
||||
FRINGE_TOP_MM = 30.0
|
||||
BAND_TOP_MM = 90.0
|
||||
TRIANGLES_PER_TILE = 12
|
||||
|
||||
|
||||
def lcg(seed):
|
||||
state = seed
|
||||
while True:
|
||||
state = (1103515245 * state + 12345) % (2 ** 31)
|
||||
yield state / float(2 ** 31)
|
||||
|
||||
|
||||
def thread_tones(n, seed):
|
||||
g = lcg(seed)
|
||||
return [int((next(g) * 2.0 - 1.0) * SLUB_RANGE) for _ in range(n)]
|
||||
|
||||
|
||||
def value_noise(cells, seed):
|
||||
g = lcg(seed)
|
||||
grid = [[(next(g) * 2.0 - 1.0) for _ in range(cells + 1)] for _ in range(cells + 1)]
|
||||
|
||||
def sample(x, y):
|
||||
fx, fy = x * cells / SIZE, y * cells / SIZE
|
||||
x0, y0 = int(fx), int(fy)
|
||||
tx, ty = fx - x0, fy - y0
|
||||
tx, ty = tx * tx * (3 - 2 * tx), ty * ty * (3 - 2 * ty)
|
||||
a = grid[y0][x0] * (1 - tx) + grid[y0][x0 + 1] * tx
|
||||
b = grid[y0 + 1][x0] * (1 - tx) + grid[y0 + 1][x0 + 1] * tx
|
||||
return a * (1 - ty) + b * ty
|
||||
|
||||
return sample
|
||||
|
||||
|
||||
def mm_from_row(y):
|
||||
"""Garment height in mm for an image row.
|
||||
|
||||
ANCHOR DISCOVERED BY PROBE (2026-08-18, striped texture on the kilt panels,
|
||||
scratchpad probe_front/back.png): MD maps pattern y=0 (hem) to image v=0.5
|
||||
and WRAPS -- v = (0.5 + (390 - y)/390) mod 1. Both panels map identically.
|
||||
Inverted here: row -> garment mm."""
|
||||
v = (y + 0.5) / SIZE
|
||||
return CLOTH_MM - CLOTH_MM * ((v - 0.5) % 1.0)
|
||||
|
||||
|
||||
def in_triangle(x, y):
|
||||
"""Beige zigzag triangles inside the band: alternating up/down, classic
|
||||
taniko-style row. Returns True where the pixel is beige (triangle), False
|
||||
where it stays black band."""
|
||||
band_lo = FRINGE_TOP_MM + 4.0 # thinner black margins (v2, per reference)
|
||||
band_hi = BAND_TOP_MM - 4.0
|
||||
mm = mm_from_row(y)
|
||||
if not (band_lo <= mm <= band_hi):
|
||||
return False
|
||||
v = (mm - band_lo) / (band_hi - band_lo) # 0 bottom .. 1 top
|
||||
tri_w = SIZE / float(TRIANGLES_PER_TILE)
|
||||
u = (x % tri_w) / tri_w # 0..1 across one triangle
|
||||
k = int(x / tri_w) % 2
|
||||
if k == 0: # upward triangle: wide at bottom
|
||||
half = (1.0 - v) * 0.5
|
||||
else: # downward: wide at top
|
||||
half = v * 0.5
|
||||
return abs(u - 0.5) < half * 0.88 # slim gutters (v2)
|
||||
|
||||
|
||||
def fringe_strand(x, y, gap_noise):
|
||||
"""Fringe zone: vertical beige strands over shadow, ragged tips."""
|
||||
mm = mm_from_row(y)
|
||||
strand_w = 6
|
||||
u = x % strand_w
|
||||
# per-strand ragged length: strands end 0..10 mm above the tile bottom
|
||||
tip = gap_noise[(x // strand_w) % len(gap_noise)] * 10.0
|
||||
if mm < tip:
|
||||
return None # past the tip: shadow
|
||||
return u >= 1 # 1px gap between strands
|
||||
|
||||
|
||||
def main():
|
||||
n_threads = SIZE // THREAD_PITCH
|
||||
warp = thread_tones(n_threads, seed=20260818)
|
||||
weft = thread_tones(n_threads, seed=31337)
|
||||
mottle = value_noise(SIZE // MOTTLE_CELL, seed=777)
|
||||
g = lcg(424242)
|
||||
gap_noise = [next(g) for _ in range(64)]
|
||||
|
||||
img = Image.new("RGB", (SIZE, SIZE))
|
||||
px = img.load()
|
||||
|
||||
for y in range(SIZE):
|
||||
j = (y // THREAD_PITCH) % n_threads
|
||||
vy = ((y % THREAD_PITCH) / (THREAD_PITCH - 1.0)) * 2.0 - 1.0
|
||||
mm = mm_from_row(y)
|
||||
for x in range(SIZE):
|
||||
i = (x // THREAD_PITCH) % n_threads
|
||||
vx = ((x % THREAD_PITCH) / (THREAD_PITCH - 1.0)) * 2.0 - 1.0
|
||||
|
||||
warp_on_top = ((i + j) % 2) == 0
|
||||
if warp_on_top:
|
||||
lift = OVER_LIFT - int(ROUND_SHADE * vx * vx)
|
||||
slub = warp[i]
|
||||
else:
|
||||
lift = -UNDER_DROP + int(ROUND_SHADE * (1.0 - vy * vy))
|
||||
slub = weft[j]
|
||||
wear = int(mottle(x, y) * MOTTLE)
|
||||
d = lift + slub + wear
|
||||
|
||||
if mm < FRINGE_TOP_MM:
|
||||
# v2: teeth are cut geometry; paint them as strand fibre --
|
||||
# duller beige with vertical grain, no fake shadow gaps.
|
||||
u = x % 6
|
||||
base = (192, 174, 140) if u >= 1 else (160, 143, 112)
|
||||
elif mm < BAND_TOP_MM:
|
||||
base = BASE if in_triangle(x, y) else BAND
|
||||
else:
|
||||
base = BASE
|
||||
|
||||
r = base[0] + d
|
||||
gg = base[1] + int(d * 0.82)
|
||||
b = base[2] + int(d * 0.66)
|
||||
px[x, y] = (max(0, min(255, r)), max(0, min(255, gg)),
|
||||
max(0, min(255, b)))
|
||||
|
||||
if FLIP:
|
||||
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
img.save(OUT, dpi=(DPI, DPI))
|
||||
print("wrote %s (%dx%d, dpi %.2f -> %.0f mm of cloth, band %d-%d mm, fringe 0-%d mm)"
|
||||
% (OUT, SIZE, SIZE, DPI, CLOTH_MM, FRINGE_TOP_MM, BAND_TOP_MM, FRINGE_TOP_MM))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Chimera A: Lena's mesh bound to Mako's armature. Tests which half kills MD's
|
||||
arrangement-point generation (bone names are identical, so weights just work)."""
|
||||
import bpy
|
||||
|
||||
MAKO = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar_fingers.fbx"
|
||||
LENA = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx"
|
||||
OUT = r"C:\Users\CAN\AppData\Local\Temp\Chimera_LenaMesh_MakoArm.fbx"
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.fbx(filepath=MAKO)
|
||||
mako_arm = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||||
mako_mesh = next(o for o in bpy.data.objects if o.type == "MESH")
|
||||
bpy.data.objects.remove(mako_mesh, do_unlink=True)
|
||||
|
||||
pre = set(bpy.data.objects)
|
||||
bpy.ops.import_scene.fbx(filepath=LENA)
|
||||
new = [o for o in bpy.data.objects if o not in pre]
|
||||
lena_mesh = next(o for o in new if o.type == "MESH")
|
||||
lena_arm = next((o for o in new if o.type == "ARMATURE"), None)
|
||||
|
||||
# rebind lena mesh to mako armature
|
||||
lena_mesh.parent = mako_arm
|
||||
lena_mesh.matrix_parent_inverse = mako_arm.matrix_world.inverted()
|
||||
for m in list(lena_mesh.modifiers):
|
||||
lena_mesh.modifiers.remove(m)
|
||||
mod = lena_mesh.modifiers.new("Armature", "ARMATURE")
|
||||
mod.object = mako_arm
|
||||
if lena_arm:
|
||||
bpy.data.objects.remove(lena_arm, do_unlink=True)
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
mako_arm.select_set(True)
|
||||
lena_mesh.select_set(True)
|
||||
bpy.context.view_layer.objects.active = mako_arm
|
||||
bpy.ops.export_scene.fbx(filepath=OUT, use_selection=True,
|
||||
add_leaf_bones=False, bake_anim=False)
|
||||
print("EXPORTED", OUT)
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Repose Mako into a strict T-pose for MD's pose detector.
|
||||
|
||||
Measures current limb angles, swings legs outward and levels the arms via the
|
||||
armature, applies the deformation to the mesh, bakes the pose as rest, exports.
|
||||
"""
|
||||
import bpy, json, sys, math
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
SRC = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar_fingers.fbx"
|
||||
OUT = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar_tpose.fbx"
|
||||
LEG_SPREAD_DEG = 6.0
|
||||
|
||||
report = {}
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.fbx(filepath=SRC)
|
||||
arm = next(o for o in bpy.data.objects if o.type == "ARMATURE")
|
||||
mesh = next(o for o in bpy.data.objects if o.type == "MESH")
|
||||
|
||||
def bone_world_dir(pb):
|
||||
m = arm.matrix_world @ pb.matrix
|
||||
head = m.translation
|
||||
tail = m @ Vector((0, pb.length, 0))
|
||||
return (tail - head).normalized(), head
|
||||
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
|
||||
# --- measure current angles ---
|
||||
angles = {}
|
||||
for name in ("thigh_l", "thigh_r", "upperarm_l", "upperarm_r", "lowerarm_l", "lowerarm_r"):
|
||||
pb = arm.pose.bones[name]
|
||||
d, _ = bone_world_dir(pb)
|
||||
angles[name] = {
|
||||
"dir": [round(v, 3) for v in d],
|
||||
"off_vertical_deg": round(math.degrees(d.angle(Vector((0, 0, -1)))), 2),
|
||||
"off_horiz_deg": round(math.degrees(min(d.angle(Vector((1, 0, 0))), d.angle(Vector((-1, 0, 0))))), 2),
|
||||
}
|
||||
report["before"] = angles
|
||||
|
||||
def rotate_world(pb, axis, deg):
|
||||
"""Rotate a pose bone around a world axis through its own head."""
|
||||
d, head = bone_world_dir(pb)
|
||||
R = Matrix.Translation(head) @ Matrix.Rotation(math.radians(deg), 4, axis) @ Matrix.Translation(-head)
|
||||
pb.matrix = arm.matrix_world.inverted() @ R @ arm.matrix_world @ pb.matrix
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# --- legs: swing outward around the forward (Y) axis ---
|
||||
for name, sign in (("thigh_l", 1), ("thigh_r", -1)):
|
||||
pb = arm.pose.bones[name]
|
||||
d, _ = bone_world_dir(pb)
|
||||
side = 1 if d.x >= 0 or "l" in name[-1] else -1
|
||||
# positive X side leg rotates -Y ... determine empirically: we want tail.x to move AWAY from 0
|
||||
for trial in (sign * LEG_SPREAD_DEG, -sign * LEG_SPREAD_DEG):
|
||||
before_x = abs(bone_world_dir(pb)[0].x)
|
||||
rotate_world(pb, "Y", trial)
|
||||
after_x = abs(bone_world_dir(pb)[0].x)
|
||||
if after_x > before_x:
|
||||
break
|
||||
rotate_world(pb, "Y", -trial) # undo, try other sign
|
||||
|
||||
# --- arms: level each segment to exactly horizontal (world XZ -> X) ---
|
||||
for name in ("upperarm_l", "lowerarm_l", "hand_l", "upperarm_r", "lowerarm_r", "hand_r"):
|
||||
pb = arm.pose.bones[name]
|
||||
d, _ = bone_world_dir(pb)
|
||||
target = Vector((1 if d.x >= 0 else -1, 0, 0))
|
||||
flat = Vector((d.x, d.y, d.z))
|
||||
axis = flat.cross(target)
|
||||
if axis.length < 1e-8:
|
||||
continue
|
||||
ang = math.degrees(flat.angle(target))
|
||||
if ang < 0.05:
|
||||
continue
|
||||
R = Matrix.Rotation(math.radians(ang), 4, axis.normalized())
|
||||
head = (arm.matrix_world @ pb.matrix).translation
|
||||
T = Matrix.Translation(head) @ R @ Matrix.Translation(-head)
|
||||
pb.matrix = arm.matrix_world.inverted() @ T @ arm.matrix_world @ pb.matrix
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# --- re-measure ---
|
||||
angles2 = {}
|
||||
for name in ("thigh_l", "thigh_r", "upperarm_l", "upperarm_r", "lowerarm_l", "lowerarm_r"):
|
||||
pb = arm.pose.bones[name]
|
||||
d, _ = bone_world_dir(pb)
|
||||
angles2[name] = {
|
||||
"off_vertical_deg": round(math.degrees(d.angle(Vector((0, 0, -1)))), 2),
|
||||
"off_horiz_deg": round(math.degrees(min(d.angle(Vector((1, 0, 0))), d.angle(Vector((-1, 0, 0))))), 2),
|
||||
}
|
||||
report["after"] = angles2
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# --- bake: apply armature modifier to mesh, then pose as rest ---
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
amod = next(m for m in mesh.modifiers if m.type == "ARMATURE")
|
||||
bpy.ops.object.modifier_apply(modifier=amod.name)
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
bpy.ops.pose.armature_apply(selected=False)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
# re-add armature modifier so the export binds skin
|
||||
mod = mesh.modifiers.new("Armature", "ARMATURE")
|
||||
mod.object = arm
|
||||
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.export_scene.fbx(filepath=OUT, use_selection=True, add_leaf_bones=False, bake_anim=False)
|
||||
report["out"] = OUT
|
||||
with open(sys.argv[-1], "w") as f:
|
||||
json.dump(report, f, indent=1)
|
||||
print("WROTE", sys.argv[-1])
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Rebuild Mako MD avatar with finger weights — v3, UV-space transfer.
|
||||
|
||||
Vertex order differs and the shape was resculpted, but both meshes carry the same
|
||||
Tripo UV atlas. Match target loops to source loops in UV space (KDTree), copy
|
||||
hand+finger weights (CC -> quat names), average per target vertex, normalize.
|
||||
"""
|
||||
import bpy, json, sys
|
||||
from mathutils import kdtree, Vector
|
||||
|
||||
TARGET_FBX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar.fbx"
|
||||
SOURCE_FBX = r"C:\Users\CAN\tinqs-ltd\animation\characters\rig-work\mako_fullhead_rigged.fbx"
|
||||
OUT_FBX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar_fingers.fbx"
|
||||
|
||||
XFER_MAP = {}
|
||||
for S, T in (("L", "l"), ("R", "r")):
|
||||
XFER_MAP[f"CC_Base_{S}_Hand"] = f"hand_{T}"
|
||||
for F, Q in (("Thumb", "thumb"), ("Index", "index"), ("Mid", "middle"),
|
||||
("Ring", "ring"), ("Pinky", "pinky")):
|
||||
for i in (1, 2, 3):
|
||||
XFER_MAP[f"CC_Base_{S}_{F}{i}"] = f"{Q}_0{i}_{T}"
|
||||
|
||||
report = {}
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.fbx(filepath=TARGET_FBX)
|
||||
tgt = next(o for o in bpy.data.objects if o.type == "MESH")
|
||||
pre = {o.name for o in bpy.data.objects}
|
||||
bpy.ops.import_scene.fbx(filepath=SOURCE_FBX)
|
||||
new = [o for o in bpy.data.objects if o.name not in pre]
|
||||
src = max((o for o in new if o.type == "MESH"), key=lambda o: len(o.data.vertices))
|
||||
|
||||
# --- source loop UV KDTree -> vertex index ---
|
||||
s_layer = src.data.uv_layers.active.data
|
||||
s_loops = src.data.loops
|
||||
kd = kdtree.KDTree(len(s_loops))
|
||||
loop_vert = [0] * len(s_loops)
|
||||
for lp in s_loops:
|
||||
uv = s_layer[lp.index].uv
|
||||
kd.insert(Vector((uv.x, uv.y, 0.0)), lp.index)
|
||||
loop_vert[lp.index] = lp.vertex_index
|
||||
kd.balance()
|
||||
|
||||
# --- source per-vertex weights for transfer groups ---
|
||||
src_gidx = {g.index: XFER_MAP[g.name] for g in src.vertex_groups if g.name in XFER_MAP}
|
||||
report["src_transfer_groups"] = len(src_gidx)
|
||||
src_w = [None] * len(src.data.vertices)
|
||||
for i, v in enumerate(src.data.vertices):
|
||||
d = {}
|
||||
for g in v.groups:
|
||||
q = src_gidx.get(g.group)
|
||||
if q and g.weight > 0.0001:
|
||||
d[q] = d.get(q, 0.0) + g.weight
|
||||
src_w[i] = d or None
|
||||
|
||||
# --- match target loops by UV, accumulate weights per target vertex ---
|
||||
t_layer = tgt.data.uv_layers.active.data
|
||||
acc = [None] * len(tgt.data.vertices) # vertex -> {qname: (sum, count)}
|
||||
cnt = [0] * len(tgt.data.vertices)
|
||||
dist_sum, dist_n, dist_max = 0.0, 0, 0.0
|
||||
for lp in tgt.data.loops:
|
||||
uv = t_layer[lp.index].uv
|
||||
_, sl, d = kd.find(Vector((uv.x, uv.y, 0.0)))
|
||||
dist_sum += d; dist_n += 1; dist_max = max(dist_max, d)
|
||||
vi = lp.vertex_index
|
||||
cnt[vi] += 1
|
||||
w = src_w[loop_vert[sl]]
|
||||
if w:
|
||||
a = acc[vi] or {}
|
||||
for q, val in w.items():
|
||||
a[q] = a.get(q, 0.0) + val
|
||||
acc[vi] = a
|
||||
report["uv_dist_mean"] = round(dist_sum / dist_n, 6)
|
||||
report["uv_dist_max"] = round(dist_max, 6)
|
||||
|
||||
# --- wipe old hand groups, write averaged weights ---
|
||||
for name in ("hand_l", "hand_r"):
|
||||
g = tgt.vertex_groups.get(name)
|
||||
if g:
|
||||
tgt.vertex_groups.remove(g)
|
||||
dst = {}
|
||||
for qname in set(XFER_MAP.values()):
|
||||
dst[qname] = tgt.vertex_groups.get(qname) or tgt.vertex_groups.new(name=qname)
|
||||
touched = 0
|
||||
for vi, a in enumerate(acc):
|
||||
if not a:
|
||||
continue
|
||||
touched += 1
|
||||
for q, s in a.items():
|
||||
dst[q].add([vi], s / cnt[vi], "REPLACE")
|
||||
report["tgt_verts_touched"] = touched
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
tgt.select_set(True)
|
||||
bpy.context.view_layer.objects.active = tgt
|
||||
bpy.ops.object.vertex_group_normalize_all(lock_active=False)
|
||||
|
||||
weighted = set()
|
||||
for v in tgt.data.vertices:
|
||||
for g in v.groups:
|
||||
if g.weight > 0.0001:
|
||||
weighted.add(tgt.vertex_groups[g.group].name)
|
||||
report["tgt_weighted_groups"] = len(weighted)
|
||||
report["tgt_finger_groups"] = len([x for x in weighted if any(
|
||||
f in x for f in ("index", "middle", "ring", "pinky", "thumb"))])
|
||||
|
||||
for o in new:
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.export_scene.fbx(filepath=OUT_FBX, use_selection=True,
|
||||
add_leaf_bones=False, bake_anim=False)
|
||||
report["out"] = OUT_FBX
|
||||
with open(sys.argv[-1], "w") as f:
|
||||
json.dump(report, f, indent=1)
|
||||
print("WROTE", sys.argv[-1])
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Rope belt for the Mako kilt -- braided torus + knot + two hanging tie ends.
|
||||
|
||||
Rigid geometry, not cloth (piupiu lesson: a waistband that swings reads broken).
|
||||
Flat pattern: rope 150 x 1.5 cm at high hip, knot + ~22 cm ends on the wearer's
|
||||
right hip; tie ends 40 x 1.2 cm beside the front overlap edge.
|
||||
|
||||
Run: blender --background --python build_rope_belt.py
|
||||
Out: tools/tailor/mako_kilt_v2_belt.obj + composite review renders.
|
||||
"""
|
||||
import bpy, math
|
||||
from mathutils import Vector
|
||||
|
||||
OUTD = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor"
|
||||
AVATAR = OUTD + r"\avatar\Mako_Fullhead_QuatSkin_Avatar_fingers.fbx"
|
||||
KILT_OBJ = OUTD + r"\mako_kilt_v2_garment.obj"
|
||||
KILT_TEX = OUTD + r"\textures\kilt_cloth.png"
|
||||
BELT_OBJ = OUTD + r"\mako_kilt_v2_belt.obj"
|
||||
SHOTS = OUTD + r"\screenshots"
|
||||
|
||||
BELT_Z = 1.055 # sits ON the kilt's top edge (QC: top ~1.06)
|
||||
BELT_R = 0.172 # outside the draped cloth, not just the body
|
||||
ROPE_R = 0.0075 # rope radius (1.5 cm dia)
|
||||
KNOT_ANG = math.radians(-38) # wearer's right-front, beside the overlap edge
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
def rope_material():
|
||||
m = bpy.data.materials.new("RopeBrown")
|
||||
m.use_nodes = True
|
||||
b = m.node_tree.nodes["Principled BSDF"]
|
||||
b.inputs["Base Color"].default_value = (0.42, 0.30, 0.17, 1.0)
|
||||
b.inputs["Roughness"].default_value = 0.9
|
||||
return m
|
||||
|
||||
MAT = rope_material()
|
||||
|
||||
def strand_curve(name, pts, bevel, taper_end=False):
|
||||
cu = bpy.data.curves.new(name, "CURVE")
|
||||
cu.dimensions = "3D"
|
||||
cu.bevel_depth = bevel
|
||||
cu.bevel_resolution = 3
|
||||
sp = cu.splines.new("NURBS")
|
||||
sp.points.add(len(pts) - 1)
|
||||
for i, p in enumerate(pts):
|
||||
sp.points[i].co = (p[0], p[1], p[2], 1.0)
|
||||
if taper_end:
|
||||
sp.points[i].radius = 1.0 - 0.85 * (i / (len(pts) - 1)) ** 2
|
||||
sp.use_endpoint_u = True
|
||||
ob = bpy.data.objects.new(name, cu)
|
||||
ob.data.materials.append(MAT)
|
||||
bpy.context.collection.objects.link(ob)
|
||||
return ob
|
||||
|
||||
# --- braided ring: 3 strands weaving around the belt circle -------------------
|
||||
N = 220
|
||||
belt_parts = []
|
||||
for k in range(3):
|
||||
phase = k * 2.0 * math.pi / 3.0
|
||||
pts = []
|
||||
for i in range(N + 1):
|
||||
t = 2.0 * math.pi * i / N
|
||||
wob = 0.55 * ROPE_R
|
||||
r = BELT_R + wob * math.cos(9 * t + phase)
|
||||
z = BELT_Z + wob * math.sin(9 * t + phase)
|
||||
pts.append((r * math.sin(t), -r * math.cos(t), z))
|
||||
belt_parts.append(strand_curve("belt_strand_%d" % k, pts, ROPE_R * 0.62))
|
||||
|
||||
# --- knot: short fat wrap at the front-right ----------------------------------
|
||||
kx, ky = BELT_R * math.sin(KNOT_ANG), -BELT_R * math.cos(KNOT_ANG)
|
||||
knot_pts = []
|
||||
for i in range(25):
|
||||
t = i / 24.0
|
||||
a = KNOT_ANG + (t - 0.5) * 0.28
|
||||
r = BELT_R + 0.011 * math.sin(t * math.pi)
|
||||
knot_pts.append((r * math.sin(a), -r * math.cos(a),
|
||||
BELT_Z + 0.012 * math.sin(t * 2 * math.pi)))
|
||||
belt_parts.append(strand_curve("belt_knot", knot_pts, ROPE_R * 0.9))
|
||||
|
||||
# --- two hanging tie ends (40 cm, tapered, slight sway) ------------------------
|
||||
out_x, out_y = math.sin(KNOT_ANG), -math.cos(KNOT_ANG)
|
||||
for j, (side, sway) in enumerate(((0.014, 0.035), (-0.012, -0.025))):
|
||||
pts = []
|
||||
L = 0.40
|
||||
for i in range(14):
|
||||
t = i / 13.0
|
||||
# hang OUTSIDE the kilt cloth: push radially out as they fall
|
||||
r_out = 0.012 + 0.010 * t
|
||||
pts.append((kx + out_x * r_out + side + sway * math.sin(t * 2.6),
|
||||
ky + out_y * r_out,
|
||||
BELT_Z - 0.012 - L * t))
|
||||
belt_parts.append(strand_curve("belt_tie_%d" % j, pts, 0.006, taper_end=True))
|
||||
|
||||
# convert curves to mesh and join
|
||||
for ob in belt_parts:
|
||||
ob.select_set(True)
|
||||
bpy.context.view_layer.objects.active = belt_parts[0]
|
||||
bpy.ops.object.convert(target="MESH")
|
||||
bpy.ops.object.join()
|
||||
belt = bpy.context.view_layer.objects.active
|
||||
belt.name = "MakoKiltBelt"
|
||||
bpy.ops.wm.obj_export(filepath=BELT_OBJ, export_selected_objects=True,
|
||||
export_materials=True)
|
||||
print("BELT verts", len(belt.data.vertices))
|
||||
|
||||
# --- composite review render ----------------------------------------------------
|
||||
bpy.ops.import_scene.fbx(filepath=AVATAR)
|
||||
# the avatar's own textures fail to load headless (magenta) -- flat grey body
|
||||
grey = bpy.data.materials.new("BodyGrey")
|
||||
grey.use_nodes = True
|
||||
gb = grey.node_tree.nodes["Principled BSDF"]
|
||||
gb.inputs["Base Color"].default_value = (0.62, 0.60, 0.58, 1.0)
|
||||
gb.inputs["Roughness"].default_value = 0.7
|
||||
for ob in bpy.data.objects:
|
||||
if ob.type == "MESH" and "belt" not in ob.name.lower() and ob.name != "MakoKiltBelt":
|
||||
ob.data.materials.clear()
|
||||
ob.data.materials.append(grey)
|
||||
bpy.ops.wm.obj_import(filepath=KILT_OBJ)
|
||||
kilt = bpy.context.view_layer.objects.active
|
||||
km = bpy.data.materials.new("KiltCloth")
|
||||
km.use_nodes = True
|
||||
bsdf = km.node_tree.nodes["Principled BSDF"]
|
||||
tex = km.node_tree.nodes.new("ShaderNodeTexImage")
|
||||
tex.image = bpy.data.images.load(KILT_TEX)
|
||||
km.node_tree.links.new(tex.outputs["Color"], bsdf.inputs["Base Color"])
|
||||
bsdf.inputs["Roughness"].default_value = 0.85
|
||||
kilt.data.materials.clear()
|
||||
kilt.data.materials.append(km)
|
||||
# MD OBJ exports in mm -- scale to metres if huge
|
||||
if max(kilt.dimensions) > 10:
|
||||
kilt.scale = (0.001, 0.001, 0.001)
|
||||
|
||||
sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
|
||||
sun.data.energy = 1.2
|
||||
sun.rotation_euler = (math.radians(55), 0, math.radians(25))
|
||||
bpy.context.collection.objects.link(sun)
|
||||
fill = bpy.data.objects.new("Fill", bpy.data.lights.new("Fill", "AREA"))
|
||||
fill.data.energy = 220
|
||||
fill.data.size = 3.0
|
||||
fill.location = (-1.2, -2.4, 1.5)
|
||||
fill.rotation_euler = (math.radians(75), 0, math.radians(-25))
|
||||
bpy.context.collection.objects.link(fill)
|
||||
front = bpy.data.objects.new("Front", bpy.data.lights.new("Front", "AREA"))
|
||||
front.data.energy = 160
|
||||
front.data.size = 3.0
|
||||
front.location = (0.8, -2.8, 1.2)
|
||||
front.rotation_euler = (math.radians(80), 0, math.radians(15))
|
||||
bpy.context.collection.objects.link(front)
|
||||
|
||||
cam = bpy.data.objects.new("Cam", bpy.data.cameras.new("Cam"))
|
||||
bpy.context.collection.objects.link(cam)
|
||||
bpy.context.scene.camera = cam
|
||||
sc = bpy.context.scene
|
||||
sc.render.engine = "BLENDER_EEVEE"
|
||||
sc.render.resolution_x = sc.render.resolution_y = 1600
|
||||
sc.render.film_transparent = False
|
||||
for name, loc, rot in (("front", (0, -3.4, 1.05), (math.radians(88), 0, 0)),
|
||||
("threequarter", (2.2, -2.6, 1.1), (math.radians(86), 0, math.radians(40)))):
|
||||
cam.location = loc
|
||||
cam.rotation_euler = rot
|
||||
sc.render.filepath = SHOTS + r"\mako_kilt_v2_belt_%s.png" % name
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print("RENDERS done")
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"name": "mako_kilt_v1",
|
||||
"_worksheet": {
|
||||
"block": "hunter_skirt_v8 topology (12-point shaped A-line, cross-paired sides ONE reversed), renumbered to Mako",
|
||||
"reference": "male-clothing-feather-cape-gpt-piece-kilt{,-front,-back,-pattern-annotated}.png",
|
||||
"flat_pattern_source": "annotated pattern sized to male Lena 1.777 m: worn top 1.03, hem 0.65, panel 118x38 (incl 30 cm wrap overlap), hem ~150. Scaled to Mako 1.818 m (x1.0231): top 1.054, hem 0.665, worn length 39 cm.",
|
||||
"measured_targets": {
|
||||
"waist_top_m": 1.054,
|
||||
"solid_hem_m": 0.665,
|
||||
"note": "Mako card: waist 88.7 @ 1.065, hip 101.4 @ 0.932. Body at worn top (z=1.054, interp) = 89.8 cm."
|
||||
},
|
||||
"ease_profile_cm": {
|
||||
"top_z1.054": 3.2,
|
||||
"z0.990": 6.0,
|
||||
"hip_z0.926": 6.1
|
||||
},
|
||||
"tension_note": "Garment waist 93 cm vs 89.8 body (+3.2, same margin that held v8) and 8.4 cm smaller than the 101.4 hip below it -- cannot slide down. No elastic, no strengthen, per v8 findings.",
|
||||
"silhouette_note": "hem 130 cm/waist 93 = 1.40 vs reference's ~1.27 worn. Mako's gentle 8 cm waist-to-hip drop needs far less flare than Lena's 42 cm; extra 10 cm is walk clearance.",
|
||||
"not_built_v1": [
|
||||
"30 cm wrap overlap flap (reference front-left diagonal edge)",
|
||||
"rope belt + 2 tie ends (geometry; candidate for a downstream prop)",
|
||||
"geometric fringe teeth (drawn in the texture for now, piupiu technique later)"
|
||||
],
|
||||
"texture_note": "kilt_cloth.png spans the 390 mm panel height exactly once (dpi 66.69): fringe 0-30 mm, black triangle band 30-90 mm, plain beige above. PROBE-VERIFIED anchor: MD maps pattern y=0 (hem) to texture v=0.5 and WRAPS -- see mm_from_row() in make_kilt_cloth.py.",
|
||||
"SHIPPED_BY": "md_script_swap.py, NOT the generated md_script.py -- MD's body analyzer rejects every Mako-derived mesh (0 arrangement points, 'avatar must be in T-pose or A-pose' despite a verified perfect T), so the kilt is arranged/sewn/elastic-locked on a 1.07x-scaled LENA donor, then avatars are swapped and the fitted garment re-settles onto Mako. Tried and ruled out: finger weights, decimation, head/hand chops, leg spread, mesh rename, OBJ import.",
|
||||
"v6_findings": {
|
||||
"swap_drop": "the avatar swap drops loose arranged panels ~10 cm; a sewn+cinched garment survives it (drops ~5 cm, countered by the 1.07 donor scale)",
|
||||
"elastic": "waist line 0, total 450 mm/panel, strength 60 -- DEFAULT STRENGTH NEVER REACHES TARGET; strength 60 moved the top from 1.019 to 1.043",
|
||||
"qc_pass": "top 1.043 (target 1.054 +/-0.03), hem 0.662 (target 0.665 +/-0.04), span 0.381 vs 0.390 drafted; measured on mako_kilt_v1_front_hd.png (turntable render, alpha mask, warm-pixel classifier, --height 1.818)",
|
||||
"known_flaw": "slightly scalloped top edge at center front from the stiff elastic; the reference's rope belt would cover it -- candidate for the belt prop pass"
|
||||
}
|
||||
},
|
||||
"md": {
|
||||
"reset": "new_project",
|
||||
"avatar_fbx": "C:/Users/CAN/tinqs-ltd/animation/tools/tailor/avatar/Mako_Fullhead_QuatSkin_Avatar_fingers.fbx",
|
||||
"avatar_scale": 10.0,
|
||||
"add_arrangement_points": true,
|
||||
"auto_translate": true,
|
||||
"zfab": "C:/Users/Public/Documents/MarvelousDesigner/New Assets/Fabric/(Default for Simulation).zfab",
|
||||
"texture": "C:/Users/CAN/tinqs-ltd/animation/tools/tailor/textures/kilt_cloth.png",
|
||||
"texture_dpi": 66.69,
|
||||
"panels": [
|
||||
{
|
||||
"name": "front",
|
||||
"dx": 0.0,
|
||||
"note": "Widths vs Mako body interp +ease: y390/465 (body 89.8+3.2), y325/510 (95.9+6), y260.5/537 (hip 101.4+6), y215/555, y135/600 (thigh hull guess ~108, single thigh 67.3), y0/650. Lines: 0 waist | 1-5 side_R | 6 hem | 7-11 side_L.",
|
||||
"points": [
|
||||
[92.5, 390.0],
|
||||
[557.5, 390.0],
|
||||
[580.0, 325.0],
|
||||
[593.5, 260.5],
|
||||
[602.5, 215.0],
|
||||
[625.0, 135.0],
|
||||
[650.0, 0.0],
|
||||
[0.0, 0.0],
|
||||
[25.0, 135.0],
|
||||
[47.5, 215.0],
|
||||
[56.5, 260.5],
|
||||
[70.0, 325.0]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "back",
|
||||
"dx": 900.0,
|
||||
"note": "identical panel offset by dx (NOT mirrored) -> sides CROSS-pair, ONE reversed (v8 discovery)",
|
||||
"points": [
|
||||
[92.5, 390.0],
|
||||
[557.5, 390.0],
|
||||
[580.0, 325.0],
|
||||
[593.5, 260.5],
|
||||
[602.5, 215.0],
|
||||
[625.0, 135.0],
|
||||
[650.0, 0.0],
|
||||
[0.0, 0.0],
|
||||
[25.0, 135.0],
|
||||
[47.5, 215.0],
|
||||
[56.5, 260.5],
|
||||
[70.0, 325.0]
|
||||
]
|
||||
}
|
||||
],
|
||||
"seams": [
|
||||
{"a": "front", "a_line": 1, "b": "back", "b_line": 11, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 2, "b": "back", "b_line": 10, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 3, "b": "back", "b_line": 9, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 4, "b": "back", "b_line": 8, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 5, "b": "back", "b_line": 7, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 11, "b": "back", "b_line": 1, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 10, "b": "back", "b_line": 2, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 9, "b": "back", "b_line": 3, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 8, "b": "back", "b_line": 4, "reverse_a": false, "reverse_b": true},
|
||||
{"a": "front", "a_line": 7, "b": "back", "b_line": 5, "reverse_a": false, "reverse_b": true}
|
||||
],
|
||||
"arrangements": [
|
||||
{"panel": "front", "point": "Leg_Skirt_Front", "offset": [50, 92, 50]},
|
||||
{"panel": "back", "point": "Leg_Skirt_Back", "offset": [0, 92, 50]}
|
||||
],
|
||||
"sim": {
|
||||
"strengthen": false,
|
||||
"settle_frames": 200,
|
||||
"relax_frames": 60
|
||||
},
|
||||
"cam_viewpoint": 2,
|
||||
"presim_snapshot": "C:/Users/CAN/tinqs-ltd/animation/tools/tailor/screenshots/mako_kilt_v1_presim.png",
|
||||
"snapshot": "C:/Users/CAN/tinqs-ltd/animation/tools/tailor/screenshots/mako_kilt_v1_front.png",
|
||||
"back_snapshot": "C:/Users/CAN/tinqs-ltd/animation/tools/tailor/screenshots/mako_kilt_v1_back.png",
|
||||
"export_dir": "C:/Users/CAN/tinqs-ltd/animation/tools/tailor",
|
||||
"export_basename": "mako_kilt_v1",
|
||||
"exports": ["zprj", "fbx", "obj", "zpac"]
|
||||
},
|
||||
"expect": {
|
||||
"bands": {
|
||||
"mako_kilt_v1": {
|
||||
"top_m": 1.054,
|
||||
"bottom_m": 0.665,
|
||||
"tol_m": 0.03,
|
||||
"bottom_tol_m": 0.04,
|
||||
"from": "flat pattern annotation scaled 1.777 -> 1.818"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
# GENERATED by tools/tailor/draft_garment.py -- DO NOT EDIT.
|
||||
# garment : mako_kilt_v1
|
||||
# stage : all
|
||||
# config : C:\Users\CAN\tinqs-ltd\animation\tools\tailor\work\mako_kilt_v1\config.json
|
||||
# emitted : 2026-08-18T15:14:53
|
||||
# Edit the config's `md` block and re-emit; edits here are lost.
|
||||
#
|
||||
# Run inside an MD bridge session (a human must click Plugin > TinqsMDBridge first):
|
||||
# python tools/md_bridge.py --file <this file> --timeout 250
|
||||
#
|
||||
# HARD-WON MD FACTS (do not rediscover -- see tools/tailor/draft_garment.py):
|
||||
# - 2D pattern space: units are mm, +y is UP in the 3D mapping. Straps/neck at
|
||||
# HIGH y, hem at y=0. Panels built y-down drape upside-down over the head and
|
||||
# tangle -- it looks like a seam bug, it isn't.
|
||||
# - MD's 3D unit is mm (gravity -9800). Blender-exported FBX avatars land 10x
|
||||
# small -- import with op.scale = 10. ImportAvatar() is .avt only (False on FBX).
|
||||
# - op.bAddArrangementPoints = True generates the ~98 named arrangement points;
|
||||
# without it there is nowhere to hang cloth.
|
||||
# - CreatePatternWithPoints takes (x, y, type) triples, type 0 = corner. Returned
|
||||
# id is the pattern index; line i = edge point[i] -> point[i+1].
|
||||
# - Seams are WHOLE-EDGE only: AddSeamlinePairGroup(a, lineA, b, lineB, False,
|
||||
# False) with the SAME line index on mirrored front/back panels. Cross-pairing
|
||||
# sews the garment over the face. Neck gaps and armholes are extra points in
|
||||
# the outline, not partial seams.
|
||||
# - SetArrangement() only ASSIGNS; utility_api.ResetClothArrangement() APPLIES it
|
||||
# (ReDrape3DArrangement only materializes not-yet-draped cloth).
|
||||
# - Arrangement INDICES REGENERATE on every avatar import -- always look up by
|
||||
# NAME from GetArrangementList(). Offsets aren't stable either; verify with a
|
||||
# 0-frame snapshot (the diagnostic that unsticks everything).
|
||||
# - utility_api.NewProject() DELETES the avatar -- re-import after.
|
||||
# - AddFabric() needs a .zfab FILE PATH (a name string silently fails); fabric 0
|
||||
# is the shared default, coloring it dyes every garment. Assign with
|
||||
# SetPatternPieceFabricIndex -- AssignFabricToPattern() never worked.
|
||||
# - SetBaseTextureMapImageGivenFilePath(PATH, fabricIdx) -- path is arg 0.
|
||||
# - PNG DPI sets a texture's physical size in MD; control tiling via dpi in PIL.
|
||||
# - Strengthen through the WHOLE settle, relax only at the end; elastic (if any)
|
||||
# mid-settle, never from frame 0. One garment per scene.
|
||||
# - Simulate(n) is synchronous, ~1 min per 300 frames -- raise the client timeout.
|
||||
# - GetClothPositions() stays empty: no mesh introspection, QC is image-based.
|
||||
|
||||
import os
|
||||
|
||||
report = {"garment": 'mako_kilt_v1', "stage": 'all'}
|
||||
|
||||
# ---- scene ---------------------------------------------------------------
|
||||
utility_api.NewProject() # NOTE: this DELETES the avatar; re-import below
|
||||
AVATAR_FBX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar_fingers.fbx"
|
||||
op = ApiTypes.ImportExportOption()
|
||||
op.scale = 10.0 # Blender-exported FBX lands 10x small
|
||||
op.bAddArrangementPoints = True # ~98 named points to hang cloth on
|
||||
op.bAutoTranslate = True
|
||||
report["avatar"] = import_api.ImportFBX(AVATAR_FBX, op) # ImportAvatar() is .avt only
|
||||
|
||||
# ---- pattern (mm; +y is UP in 3D -- hem at y=0, neck/straps at high y) ----
|
||||
# front: Widths vs Mako body interp +ease: y390/465 (body 89.8+3.2), y325/510 (95.9+6), y260.5/537 (hip 101.4+6), y215/555, y135/600 (thigh hull guess ~108, single thigh 67.3), y0/650. Lines: 0 waist | 1-5 side_R | 6 hem | 7-11 side_L.
|
||||
_pts_p_front = [
|
||||
(92.5, 390.0), (557.5, 390.0), (580.0, 325.0),
|
||||
(593.5, 260.5), (602.5, 215.0), (625.0, 135.0),
|
||||
(650.0, 0.0), (0.0, 0.0), (25.0, 135.0),
|
||||
(47.5, 215.0), (56.5, 260.5), (70.0, 325.0),
|
||||
]
|
||||
p_front = pattern_api.CreatePatternWithPoints([(x + 0.0, y + 0.0, 0) for (x, y) in _pts_p_front])
|
||||
# back: identical panel offset by dx (NOT mirrored) -> sides CROSS-pair, ONE reversed (v8 discovery)
|
||||
_pts_p_back = [
|
||||
(92.5, 390.0), (557.5, 390.0), (580.0, 325.0),
|
||||
(593.5, 260.5), (602.5, 215.0), (625.0, 135.0),
|
||||
(650.0, 0.0), (0.0, 0.0), (25.0, 135.0),
|
||||
(47.5, 215.0), (56.5, 260.5), (70.0, 325.0),
|
||||
]
|
||||
p_back = pattern_api.CreatePatternWithPoints([(x + 900.0, y + 0.0, 0) for (x, y) in _pts_p_back])
|
||||
report["ids"] = [p_front, p_back]
|
||||
|
||||
# ---- seams (WHOLE-EDGE only; same line index on mirrored panels) ---------
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 1, p_back, 11, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 2, p_back, 10, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 3, p_back, 9, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 4, p_back, 8, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 5, p_back, 7, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 11, p_back, 1, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 10, p_back, 2, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 9, p_back, 3, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 8, p_back, 4, False, True)
|
||||
pattern_api.AddSeamlinePairGroup(p_front, 7, p_back, 5, False, True)
|
||||
|
||||
# Paired edges must be EQUAL, not close: a 0.075 mm 'rounding' difference was
|
||||
# a slanted edge that notched a waistband. Fail loudly BEFORE wasting a sim.
|
||||
_pairs = [(p_front, 1, p_back, 11), (p_front, 2, p_back, 10), (p_front, 3, p_back, 9), (p_front, 4, p_back, 8), (p_front, 5, p_back, 7), (p_front, 11, p_back, 1), (p_front, 10, p_back, 2), (p_front, 9, p_back, 3), (p_front, 8, p_back, 4), (p_front, 7, p_back, 5)]
|
||||
report["seam_lengths"] = []
|
||||
_bad = []
|
||||
for _pa, _la, _pb, _lb in _pairs:
|
||||
_fa = pattern_api.GetLineLength(_pa, _la)
|
||||
_fb = pattern_api.GetLineLength(_pb, _lb)
|
||||
report["seam_lengths"].append((_pa, _la, round(_fa, 3), _pb, _lb, round(_fb, 3)))
|
||||
if abs(_fa - _fb) > 0.1:
|
||||
_bad.append("%s.%s=%.3f vs %s.%s=%.3f" % (_pa, _la, _fa, _pb, _lb, _fb))
|
||||
if _bad:
|
||||
raise RuntimeError("seam length mismatch (construction bug, not rounding): "
|
||||
+ "; ".join(_bad))
|
||||
|
||||
# ---- fabric (never touch index 0: it is the scene-wide default) ---------
|
||||
ZFAB = r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab"
|
||||
fab = fabric_api.AddFabric(ZFAB) # a name string silently fails
|
||||
TEX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\textures\kilt_cloth.png" # (dpi 66.69 -> physical size)
|
||||
fabric_api.SetBaseTextureMapImageGivenFilePath(TEX, fab) # path is arg 0
|
||||
for _p in (p_front, p_back,):
|
||||
pattern_api.SetPatternPieceFabricIndex(_p, fab) # AssignFabricToPattern never worked
|
||||
|
||||
# ---- arrangement (BY NAME: indices regenerate on every avatar import) ----
|
||||
arr = {a["ArrangementName"]: int(a["ArrangementIndex"])
|
||||
for a in pattern_api.GetArrangementList()}
|
||||
pattern_api.SetArrangement(p_front, arr['Leg_Skirt_Front'])
|
||||
pattern_api.SetArrangement(p_back, arr['Leg_Skirt_Back'])
|
||||
pattern_api.SetArrangementPosition(p_front, 50, 92, 50)
|
||||
pattern_api.SetArrangementPosition(p_back, 0, 92, 50)
|
||||
|
||||
# Hand the pattern ids to a later --stage drape/publish in the SAME session.
|
||||
TINQS_GARMENT = {"garment": 'mako_kilt_v1', "fabric": fab, "patterns": {'front': p_front, 'back': p_back}}
|
||||
|
||||
# THE diagnostic: apply the arrangement and shoot with ZERO sim frames. It shows
|
||||
# where the panels actually start, before physics muddies it. Reach for this the
|
||||
# moment a drape misbehaves -- upside-down panels look exactly like seam bugs.
|
||||
utility_api.ResetClothArrangement()
|
||||
utility_api.SetCamViewPoint(2) # SetViewPoint() does nothing; 2 = front
|
||||
presim_snap = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\screenshots\mako_kilt_v1_presim.png"
|
||||
os.makedirs(os.path.dirname(presim_snap), exist_ok=True)
|
||||
export_api.ExportSnapshot3D(presim_snap)
|
||||
report['presim_snap'] = presim_snap
|
||||
|
||||
# ---- drape ---------------------------------------------------------------
|
||||
utility_api.ResetClothArrangement() # SetArrangement only assigns; THIS applies it
|
||||
report["sim1"] = utility_api.Simulate(200) # synchronous, ~1 min / 300 frames
|
||||
# Relax only at the end, so the settled shape falls into natural folds.
|
||||
report["sim2"] = utility_api.Simulate(60)
|
||||
utility_api.Refresh3DWindow()
|
||||
utility_api.SetCamViewPoint(2) # SetViewPoint() does nothing; 2 = front
|
||||
snap = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\screenshots\mako_kilt_v1_front.png"
|
||||
os.makedirs(os.path.dirname(snap), exist_ok=True)
|
||||
export_api.ExportSnapshot3D(snap)
|
||||
report['snap'] = snap
|
||||
# ExportSnapshot3D has NO rear view (SetCamViewPoint 0-7: none is the back).
|
||||
# Turntable ignores any path arg and reuses the same output names -- copy now.
|
||||
import shutil
|
||||
_tt = export_api.ExportTurntableImages(4) # 0 front, 1 side, 2 BACK, 3 side
|
||||
if _tt and len(_tt) > 2:
|
||||
back_snap = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\screenshots\mako_kilt_v1_back.png"
|
||||
os.makedirs(os.path.dirname(back_snap), exist_ok=True)
|
||||
shutil.copyfile(_tt[2], back_snap)
|
||||
report["back_snapshot"] = back_snap
|
||||
else:
|
||||
report["back_snapshot"] = "TURNTABLE RETURNED NOTHING -- rear unverified"
|
||||
|
||||
# ---- publish (all scriptable; the FBX/OBJ flags are the load-bearing part) -
|
||||
os.makedirs(r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor", exist_ok=True)
|
||||
xop = ApiTypes.ImportExportOption()
|
||||
xop.bExportGarment = True
|
||||
xop.bExportAvatar = False # the avatar is the game body; ship cloth only
|
||||
report["zprj"] = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\mako_kilt_v1.zprj"
|
||||
export_api.ExportZPrj(report["zprj"]) # editable source of truth; no SaveProjectFile exists
|
||||
report["fbx"] = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\mako_kilt_v1_garment.fbx"
|
||||
export_api.ExportFBX(report["fbx"], xop) # -> clothing/garment_pipeline.py
|
||||
report["obj"] = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\mako_kilt_v1_garment.obj"
|
||||
export_api.ExportOBJ(report["obj"], xop)
|
||||
report["zpac"] = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\mako_kilt_v1.zpac"
|
||||
export_api.ExportZPac(report["zpac"]) # for outfit assembly (ImportZpac op.bAdd=True)
|
||||
|
||||
result = report
|
||||
@@ -0,0 +1,148 @@
|
||||
# DERIVED from the generated md_script.py (draft_garment.py cannot express this).
|
||||
# garment : mako_kilt_v1 -- DONOR-ARRANGEMENT VARIANT
|
||||
#
|
||||
# WHY: MD's pose/body analyzer rejects every Mako-derived mesh ("Fitting Suit
|
||||
# cannot be created / avatar must be in T-pose or A-pose") and generates 0
|
||||
# arrangement points, so nothing can be arranged on him directly. Lena's mesh
|
||||
# passes (109 points) and her hips (109.5 cm) are WIDER than Mako's (101.4), so
|
||||
# cloth arranged on her Leg_Skirt shell circles outside his body too.
|
||||
#
|
||||
# FLOW: arrange the kilt on LENA -> add Mako (bAdd, NO arrangement flag, avoids
|
||||
# the failing analyzer) -> DeleteAvatar(Lena) -> simulate: seams contract the
|
||||
# kilt onto Mako. Arrangement points may die with Lena; the cloth is already
|
||||
# positioned and the sim does not need them.
|
||||
#
|
||||
# python tools/md_bridge.py --file tools/tailor/work/mako_kilt_v1/md_script_swap.py --timeout 900
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
report = {"garment": 'mako_kilt_v1', "stage": 'swap'}
|
||||
|
||||
# ---- scene: LENA is the arrangement donor ---------------------------------
|
||||
utility_api.NewProject()
|
||||
LENA_FBX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx"
|
||||
MAKO_FBX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar_fingers.fbx"
|
||||
op = ApiTypes.ImportExportOption()
|
||||
# v4: donor scaled +5% -- the avatar swap consistently drops the fitted kilt
|
||||
# ~8 cm, so fit it ~6 cm higher on a bigger Lena and let the drop land it on
|
||||
# Mako's target (top 1.054). Elastic still cinches to the same 90 cm.
|
||||
op.scale = 10.7 # v5: 10.5 measured top=1.022 (2 mm under tol) -- nudge up
|
||||
op.bAddArrangementPoints = True
|
||||
op.bAutoTranslate = True
|
||||
report["lena"] = import_api.ImportFBX(LENA_FBX, op)
|
||||
|
||||
# ---- pattern (identical to generated script) -------------------------------
|
||||
# v2: waist narrowed 465 -> 455/panel (91 cm total vs 89.8 body at z=1.054).
|
||||
# v1 measured 9.6 cm low: the 93 cm waist slid until Mako's hip bulge caught it
|
||||
# (his 8 cm waist-to-hip drop cannot arrest a slide the way Lena's flare does).
|
||||
_pts = [
|
||||
(97.5, 390.0), (552.5, 390.0), (580.0, 325.0),
|
||||
(593.5, 260.5), (602.5, 215.0), (625.0, 135.0),
|
||||
(650.0, 0.0), (0.0, 0.0), (25.0, 135.0),
|
||||
(47.5, 215.0), (56.5, 260.5), (70.0, 325.0),
|
||||
]
|
||||
p_front = pattern_api.CreatePatternWithPoints([(x + 0.0, y + 0.0, 0) for (x, y) in _pts])
|
||||
p_back = pattern_api.CreatePatternWithPoints([(x + 900.0, y + 0.0, 0) for (x, y) in _pts])
|
||||
report["ids"] = [p_front, p_back]
|
||||
|
||||
for a_l, b_l in ((1, 11), (2, 10), (3, 9), (4, 8), (5, 7),
|
||||
(11, 1), (10, 2), (9, 3), (8, 4), (7, 5)):
|
||||
pattern_api.AddSeamlinePairGroup(p_front, a_l, p_back, b_l, False, True)
|
||||
|
||||
_bad = []
|
||||
for a_l, b_l in ((1, 11), (2, 10), (3, 9), (4, 8), (5, 7),
|
||||
(11, 1), (10, 2), (9, 3), (8, 4), (7, 5)):
|
||||
_fa = pattern_api.GetLineLength(p_front, a_l)
|
||||
_fb = pattern_api.GetLineLength(p_back, b_l)
|
||||
if abs(_fa - _fb) > 0.1:
|
||||
_bad.append("front.%d=%.3f vs back.%d=%.3f" % (a_l, _fa, b_l, _fb))
|
||||
if _bad:
|
||||
raise RuntimeError("seam length mismatch: " + "; ".join(_bad))
|
||||
|
||||
# ---- fabric ----------------------------------------------------------------
|
||||
ZFAB = r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab"
|
||||
fab = fabric_api.AddFabric(ZFAB)
|
||||
TEX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\textures\kilt_cloth.png"
|
||||
fabric_api.SetBaseTextureMapImageGivenFilePath(TEX, fab)
|
||||
for _p in (p_front, p_back):
|
||||
pattern_api.SetPatternPieceFabricIndex(_p, fab)
|
||||
|
||||
# ---- arrangement on Lena ----------------------------------------------------
|
||||
arr = {a["ArrangementName"]: int(a["ArrangementIndex"])
|
||||
for a in pattern_api.GetArrangementList()}
|
||||
report["arrangement_count"] = len(arr)
|
||||
pattern_api.SetArrangement(p_front, arr['Leg_Skirt_Front'])
|
||||
pattern_api.SetArrangement(p_back, arr['Leg_Skirt_Back'])
|
||||
# v2: y raised 92 -> 96 to start the drape higher (v1 settled 9.6 cm low);
|
||||
# verify against the presim snapshot before trusting the sim.
|
||||
pattern_api.SetArrangementPosition(p_front, 50, 96, 50)
|
||||
pattern_api.SetArrangementPosition(p_back, 0, 96, 50)
|
||||
utility_api.ResetClothArrangement()
|
||||
|
||||
SHOTS = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\screenshots"
|
||||
os.makedirs(SHOTS, exist_ok=True)
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "mako_kilt_v1_presim_lena.png"))
|
||||
|
||||
# ---- v3: FIT ON LENA FIRST ---------------------------------------------------
|
||||
# v2 finding: the avatar swap itself drops loose arranged panels ~10 cm before
|
||||
# any sim runs. So sew + settle + elastic-lock the kilt on Lena (known-good
|
||||
# v8-style drape), and only THEN swap bodies -- a fitted, cinched garment
|
||||
# survives the swap and just re-settles onto Mako.
|
||||
report["sim_lena_a"] = utility_api.Simulate(30)
|
||||
for _p in (p_front, p_back):
|
||||
pattern_api.SetPatternPieceElastic(_p, 0, True)
|
||||
pattern_api.SetPatternPieceElasticTotalLength(_p, 0, 450)
|
||||
# v6: default strength never reached the 90 cm target -- the kilt rested
|
||||
# where Mako's body fills ~93 cm (z=1.02). Stiffen the cinch.
|
||||
pattern_api.SetPatternPieceElasticStrength(_p, 0, 60)
|
||||
report["sim_lena_b"] = utility_api.Simulate(120)
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "mako_kilt_v1_fitted_lena.png"))
|
||||
|
||||
# ---- THE SWAP ---------------------------------------------------------------
|
||||
op2 = ApiTypes.ImportExportOption()
|
||||
op2.scale = 10.0
|
||||
op2.bAdd = True # add alongside Lena
|
||||
op2.bAutoTranslate = True
|
||||
op2.bAddArrangementPoints = False # skip the analyzer that fails on Mako
|
||||
report["mako"] = import_api.ImportFBX(MAKO_FBX, op2)
|
||||
names = [str(n) for n in export_api.GetAvatarNameList()]
|
||||
report["avatars_before_delete"] = names
|
||||
lena_idx = next(i for i, n in enumerate(names) if "Lena" in n)
|
||||
report["deleted_lena"] = utility_api.DeleteAvatar([lena_idx])
|
||||
report["avatars_after_delete"] = [str(n) for n in export_api.GetAvatarNameList()]
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "mako_kilt_v1_presim.png"))
|
||||
|
||||
# ---- re-settle onto Mako ------------------------------------------------------
|
||||
report["sim1"] = utility_api.Simulate(120)
|
||||
report["sim2"] = utility_api.Simulate(40)
|
||||
utility_api.Refresh3DWindow()
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "mako_kilt_v1_front.png"))
|
||||
_tt = export_api.ExportTurntableImages(4) # 0 front, 1 side, 2 BACK, 3 side
|
||||
if _tt and len(_tt) > 2:
|
||||
shutil.copyfile(_tt[0], os.path.join(SHOTS, "mako_kilt_v1_front_hd.png"))
|
||||
shutil.copyfile(_tt[1], os.path.join(SHOTS, "mako_kilt_v1_side_hd.png"))
|
||||
shutil.copyfile(_tt[2], os.path.join(SHOTS, "mako_kilt_v1_back.png"))
|
||||
report["back_snapshot"] = "ok"
|
||||
else:
|
||||
report["back_snapshot"] = "TURNTABLE RETURNED NOTHING"
|
||||
|
||||
# ---- publish -----------------------------------------------------------------
|
||||
OUTD = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor"
|
||||
xop = ApiTypes.ImportExportOption()
|
||||
xop.bExportGarment = True
|
||||
xop.bExportAvatar = False
|
||||
report["zprj"] = os.path.join(OUTD, "mako_kilt_v1.zprj")
|
||||
export_api.ExportZPrj(report["zprj"])
|
||||
report["fbx"] = os.path.join(OUTD, "mako_kilt_v1_garment.fbx")
|
||||
export_api.ExportFBX(report["fbx"], xop)
|
||||
report["obj"] = os.path.join(OUTD, "mako_kilt_v1_garment.obj")
|
||||
export_api.ExportOBJ(report["obj"], xop)
|
||||
report["zpac"] = os.path.join(OUTD, "mako_kilt_v1.zpac")
|
||||
export_api.ExportZPac(report["zpac"])
|
||||
|
||||
result = report
|
||||
@@ -0,0 +1,168 @@
|
||||
# mako_kilt_v2b -- sewn tube (v6-proven) + comb-teeth fringe + overlap flap.
|
||||
#
|
||||
# The seamless single-wrap (true flat pattern) slides off Mako's legs during the
|
||||
# avatar swap -- only a SEWN garment survives it (v1-v6 evidence). So: front/back
|
||||
# panels cross-pair-seamed as in v6, teeth carved INTO the bottom 30 mm (solid
|
||||
# hem 0.695, tips 0.665 -- exactly the flat pattern's "3 cm fringe at bottom
|
||||
# edge"), and the wrap slit faked with a flap panel sewn to a 300 mm segment of
|
||||
# the front waist, hanging free over the front-right.
|
||||
#
|
||||
# Run: python tools/md_bridge.py --file tools/tailor/work/mako_kilt_v1/md_script_v2b.py --timeout 900
|
||||
import os
|
||||
import shutil
|
||||
|
||||
report = {"garment": "mako_kilt_v2b"}
|
||||
|
||||
utility_api.NewProject()
|
||||
op = ApiTypes.ImportExportOption()
|
||||
op.scale = 10.7 # donor upscale counters the ~5 cm swap drop
|
||||
op.bAddArrangementPoints = True
|
||||
op.bAutoTranslate = True
|
||||
report["lena"] = import_api.ImportFBX(
|
||||
r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx", op)
|
||||
|
||||
# ---- panel builder: v6 shape, teeth carved into the hem ----------------------
|
||||
PITCH, STRAND, TEETH_D = 24.0, 17.0, 30.0
|
||||
|
||||
def teeth(x_from, x_to, y_solid):
|
||||
"""Comb teeth from x_from rightward-edge to x_to (x_from > x_to)."""
|
||||
pts = []
|
||||
x = x_from
|
||||
while x - PITCH >= x_to - 0.5:
|
||||
pts.append((x, 0.0, 0))
|
||||
pts.append((x - STRAND, 0.0, 0))
|
||||
pts.append((x - STRAND, y_solid, 0))
|
||||
pts.append((x - PITCH, y_solid, 0))
|
||||
x -= PITCH
|
||||
return pts
|
||||
|
||||
def kilt_panel(dx, split_waist):
|
||||
"""v6 outline with teeth. Returns (points, line_index_map)."""
|
||||
pts = []
|
||||
if split_waist:
|
||||
pts += [(97.5, 390.0, 0), (252.5, 390.0, 0), (552.5, 390.0, 0)]
|
||||
waist_lines = [0, 1] # 155 mm + 300 mm (flap sews to line 1)
|
||||
else:
|
||||
pts += [(97.5, 390.0, 0), (552.5, 390.0, 0)]
|
||||
waist_lines = [0]
|
||||
right_start = len(pts) - 1
|
||||
pts += [(580.0, 325.0, 0), (593.5, 260.5, 0), (602.5, 215.0, 0),
|
||||
(625.0, 135.0, 0), (650.0, TEETH_D, 0)]
|
||||
right_lines = list(range(right_start, right_start + 5))
|
||||
pts += teeth(650.0, 0.0, TEETH_D)
|
||||
pts.append((0.0, TEETH_D, 0))
|
||||
n = len(pts) # lines: n total (closing edge = last)
|
||||
pts_final = [(x + dx, y, t) for (x, y, t) in pts]
|
||||
left_lines = [n - 1, n - 2, n - 3, n - 4]
|
||||
# left side has 4 shaping pts + closing edge = 5 segments (mirror of right)
|
||||
pts_left = [(25.0 + dx, 135.0, 0), (47.5 + dx, 215.0, 0),
|
||||
(56.5 + dx, 260.5, 0), (70.0 + dx, 325.0, 0)]
|
||||
pts_final += pts_left
|
||||
total = len(pts_final)
|
||||
left_lines = [total - 5, total - 4, total - 3, total - 2, total - 1]
|
||||
return pts_final, waist_lines, right_lines, left_lines
|
||||
|
||||
f_pts, f_waist, f_right, f_left = kilt_panel(0.0, split_waist=True)
|
||||
b_pts, b_waist, b_right, b_left = kilt_panel(900.0, split_waist=False)
|
||||
p_front = pattern_api.CreatePatternWithPoints(f_pts)
|
||||
p_back = pattern_api.CreatePatternWithPoints(b_pts)
|
||||
report["front_lines"] = {"waist": f_waist, "right": f_right, "left_last5": f_left}
|
||||
report["back_lines"] = {"waist": b_waist, "right": b_right, "left_last5": b_left}
|
||||
|
||||
# ---- overlap flap: 300 top / 340 hem trapezoid, same teeth + band ------------
|
||||
fl = [(0.0, 390.0, 0), (300.0, 390.0, 0), (320.0, TEETH_D, 0)]
|
||||
fl += teeth(320.0, -20.0, TEETH_D)
|
||||
fl.append((-20.0, TEETH_D, 0))
|
||||
fl = [(x + 1800.0, y, t) for (x, y, t) in fl]
|
||||
p_flap = pattern_api.CreatePatternWithPoints(fl)
|
||||
report["ids"] = [p_front, p_back, p_flap]
|
||||
|
||||
# ---- seams -------------------------------------------------------------------
|
||||
# sides: cross-paired, ONE reversed (v8 discovery). right side of front pairs
|
||||
# with the left side of back, top-to-top.
|
||||
_bad = []
|
||||
for i in range(5):
|
||||
a_l, b_l = f_right[i], b_left[4 - i]
|
||||
pattern_api.AddSeamlinePairGroup(p_front, a_l, p_back, b_l, False, True)
|
||||
if abs(pattern_api.GetLineLength(p_front, a_l) - pattern_api.GetLineLength(p_back, b_l)) > 0.1:
|
||||
_bad.append("R%d" % i)
|
||||
for i in range(5):
|
||||
a_l, b_l = f_left[4 - i], b_right[i]
|
||||
pattern_api.AddSeamlinePairGroup(p_front, a_l, p_back, b_l, False, True)
|
||||
if abs(pattern_api.GetLineLength(p_front, a_l) - pattern_api.GetLineLength(p_back, b_l)) > 0.1:
|
||||
_bad.append("L%d" % i)
|
||||
# flap top (line 0, 300 mm) onto front waist segment (line 1, 300 mm)
|
||||
pattern_api.AddSeamlinePairGroup(p_flap, 0, p_front, 1, False, False)
|
||||
if abs(pattern_api.GetLineLength(p_flap, 0) - pattern_api.GetLineLength(p_front, 1)) > 0.1:
|
||||
_bad.append("FLAP")
|
||||
if _bad:
|
||||
raise RuntimeError("seam length mismatch: " + ",".join(_bad))
|
||||
|
||||
# ---- fabric -------------------------------------------------------------------
|
||||
fab = fabric_api.AddFabric(r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab")
|
||||
fabric_api.SetBaseTextureMapImageGivenFilePath(
|
||||
r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\textures\kilt_cloth.png", fab)
|
||||
for _p in (p_front, p_back, p_flap):
|
||||
pattern_api.SetPatternPieceFabricIndex(_p, fab)
|
||||
|
||||
# ---- arrangement ---------------------------------------------------------------
|
||||
arr = {a["ArrangementName"]: int(a["ArrangementIndex"])
|
||||
for a in pattern_api.GetArrangementList()}
|
||||
pattern_api.SetArrangement(p_front, arr["Leg_Skirt_Front"])
|
||||
pattern_api.SetArrangement(p_back, arr["Leg_Skirt_Back"])
|
||||
pattern_api.SetArrangement(p_flap, arr["Leg_Skirt_Front"])
|
||||
pattern_api.SetArrangementPosition(p_front, 50, 96, 50)
|
||||
pattern_api.SetArrangementPosition(p_back, 0, 96, 50)
|
||||
pattern_api.SetArrangementPosition(p_flap, 65, 96, 70) # right of front, farther out
|
||||
utility_api.ResetClothArrangement()
|
||||
|
||||
SHOTS = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\screenshots"
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "kilt_v2b_presim_lena.png"))
|
||||
|
||||
# ---- fit on donor: settle then elastic (v6 recipe, strength 60) ---------------
|
||||
report["sim_a"] = utility_api.Simulate(30)
|
||||
for _p, lines_totals in ((p_front, ((0, 152), (1, 298))), (p_back, ((0, 450),))):
|
||||
for ln, total in lines_totals:
|
||||
pattern_api.SetPatternPieceElastic(_p, ln, True)
|
||||
pattern_api.SetPatternPieceElasticTotalLength(_p, ln, total)
|
||||
pattern_api.SetPatternPieceElasticStrength(_p, ln, 60)
|
||||
report["sim_b"] = utility_api.Simulate(120)
|
||||
|
||||
# ---- swap ----------------------------------------------------------------------
|
||||
op2 = ApiTypes.ImportExportOption()
|
||||
op2.scale = 10.0
|
||||
op2.bAdd = True
|
||||
op2.bAutoTranslate = True
|
||||
op2.bAddArrangementPoints = False
|
||||
report["mako"] = import_api.ImportFBX(
|
||||
r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar_fingers.fbx", op2)
|
||||
names = [str(n) for n in export_api.GetAvatarNameList()]
|
||||
lena_idx = next(i for i, n in enumerate(names) if "Lena" in n)
|
||||
report["deleted_lena"] = utility_api.DeleteAvatar([lena_idx])
|
||||
report["sim1"] = utility_api.Simulate(120)
|
||||
report["sim2"] = utility_api.Simulate(40)
|
||||
utility_api.Refresh3DWindow()
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "mako_kilt_v2_front.png"))
|
||||
_tt = export_api.ExportTurntableImages(4)
|
||||
if _tt and len(_tt) > 2:
|
||||
shutil.copyfile(_tt[0], os.path.join(SHOTS, "mako_kilt_v2_front_hd.png"))
|
||||
shutil.copyfile(_tt[1], os.path.join(SHOTS, "mako_kilt_v2_side_hd.png"))
|
||||
shutil.copyfile(_tt[2], os.path.join(SHOTS, "mako_kilt_v2_back.png"))
|
||||
report["back_snapshot"] = "ok"
|
||||
|
||||
OUTD = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor"
|
||||
xop = ApiTypes.ImportExportOption()
|
||||
xop.bExportGarment = True
|
||||
xop.bExportAvatar = False
|
||||
report["zprj"] = os.path.join(OUTD, "mako_kilt_v2.zprj")
|
||||
export_api.ExportZPrj(report["zprj"])
|
||||
report["fbx"] = os.path.join(OUTD, "mako_kilt_v2_garment.fbx")
|
||||
export_api.ExportFBX(report["fbx"], xop)
|
||||
report["obj"] = os.path.join(OUTD, "mako_kilt_v2_garment.obj")
|
||||
export_api.ExportOBJ(report["obj"], xop)
|
||||
report["zpac"] = os.path.join(OUTD, "mako_kilt_v2.zpac")
|
||||
export_api.ExportZPac(report["zpac"])
|
||||
|
||||
result = report
|
||||
@@ -0,0 +1,80 @@
|
||||
# mako_kilt_v2 STAGE 2: continue the SAME session after wrap_probe.py (exec
|
||||
# namespace persists -- p_wrap and fab come from the probe). Elastic-lock the
|
||||
# waist, settle on the Lena donor, swap to Mako, re-settle, QC shots, export.
|
||||
# Run: python tools/md_bridge.py --file tools/tailor/work/mako_kilt_v1/wrap_finish.py --timeout 900
|
||||
import os
|
||||
import shutil
|
||||
|
||||
report = {"stage": "wrap_finish", "pattern": p_wrap}
|
||||
|
||||
# finer sim mesh so the 17 mm teeth actually simulate (default 20 mm skips them)
|
||||
for fname in ("SetPatternParticleDistance", "SetParticleDistance"):
|
||||
f = getattr(pattern_api, fname, None)
|
||||
if f:
|
||||
try:
|
||||
f(p_wrap, 10.0)
|
||||
report["particle_distance"] = fname
|
||||
break
|
||||
except Exception as e:
|
||||
report["particle_distance_error"] = "%s: %s" % (fname, e)
|
||||
|
||||
# elastic waist: single wrap panel, line 0 = full top edge.
|
||||
# total 910 mm = 91 cm target waist; strength 60 (v6 finding: default never gets there)
|
||||
pattern_api.SetPatternPieceElastic(p_wrap, 0, True)
|
||||
pattern_api.SetPatternPieceElasticTotalLength(p_wrap, 0, 910)
|
||||
pattern_api.SetPatternPieceElasticStrength(p_wrap, 0, 60)
|
||||
|
||||
report["sim_lena_a"] = utility_api.Simulate(40)
|
||||
report["sim_lena_b"] = utility_api.Simulate(120)
|
||||
SHOTS = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\screenshots"
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "kilt_v2_fitted_lena.png"))
|
||||
|
||||
# ---- swap to Mako ------------------------------------------------------------
|
||||
MAKO_FBX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Mako_Fullhead_QuatSkin_Avatar_fingers.fbx"
|
||||
op2 = ApiTypes.ImportExportOption()
|
||||
op2.scale = 10.0
|
||||
op2.bAdd = True
|
||||
op2.bAutoTranslate = True
|
||||
op2.bAddArrangementPoints = False
|
||||
report["mako"] = import_api.ImportFBX(MAKO_FBX, op2)
|
||||
names = [str(n) for n in export_api.GetAvatarNameList()]
|
||||
lena_name = next(n for n in names if "Lena" in n)
|
||||
# v2b: DEACTIVATE the donor instead of deleting her. DeleteAvatar drops the
|
||||
# cloth into free fall and the seamless wrap slid off Mako's legs to the floor.
|
||||
# Deactivation should kill her collision in place, letting the cinched ring
|
||||
# contract through her ghost body onto Mako with no fall.
|
||||
report["deactivated_lena"] = utility_api.SetAvatarActivation(lena_name, False)
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "kilt_v2_presim.png"))
|
||||
|
||||
report["sim1"] = utility_api.Simulate(120)
|
||||
report["sim2"] = utility_api.Simulate(60)
|
||||
# only now remove her from the scene for clean QC renders
|
||||
lena_idx = next(i for i, n in enumerate([str(n) for n in export_api.GetAvatarNameList()]) if "Lena" in n)
|
||||
report["deleted_lena"] = utility_api.DeleteAvatar([lena_idx])
|
||||
report["sim3"] = utility_api.Simulate(30)
|
||||
utility_api.Refresh3DWindow()
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "mako_kilt_v2_front.png"))
|
||||
_tt = export_api.ExportTurntableImages(4)
|
||||
if _tt and len(_tt) > 2:
|
||||
shutil.copyfile(_tt[0], os.path.join(SHOTS, "mako_kilt_v2_front_hd.png"))
|
||||
shutil.copyfile(_tt[1], os.path.join(SHOTS, "mako_kilt_v2_side_hd.png"))
|
||||
shutil.copyfile(_tt[2], os.path.join(SHOTS, "mako_kilt_v2_back.png"))
|
||||
report["back_snapshot"] = "ok"
|
||||
|
||||
OUTD = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor"
|
||||
xop = ApiTypes.ImportExportOption()
|
||||
xop.bExportGarment = True
|
||||
xop.bExportAvatar = False
|
||||
report["zprj"] = os.path.join(OUTD, "mako_kilt_v2.zprj")
|
||||
export_api.ExportZPrj(report["zprj"])
|
||||
report["fbx"] = os.path.join(OUTD, "mako_kilt_v2_garment.fbx")
|
||||
export_api.ExportFBX(report["fbx"], xop)
|
||||
report["obj"] = os.path.join(OUTD, "mako_kilt_v2_garment.obj")
|
||||
export_api.ExportOBJ(report["obj"], xop)
|
||||
report["zpac"] = os.path.join(OUTD, "mako_kilt_v2.zpac")
|
||||
export_api.ExportZPac(report["zpac"])
|
||||
|
||||
result = report
|
||||
@@ -0,0 +1,67 @@
|
||||
# mako_kilt_v2 STAGE 1: single-wrap-panel probe (TRUE flat-pattern construction).
|
||||
# One 1210 mm panel (91 cm waist + 30 cm overlap), A-line to 1550 mm, comb-teeth
|
||||
# fringe hem (piupiu technique). Arrange on the Lena donor, ZERO sim frames,
|
||||
# snapshot front + top -- the top view shows how far the arrangement wraps.
|
||||
# Run: python tools/md_bridge.py --file tools/tailor/work/mako_kilt_v1/wrap_probe.py --timeout 600
|
||||
import os
|
||||
|
||||
report = {"stage": "wrap_probe"}
|
||||
|
||||
utility_api.NewProject()
|
||||
LENA_FBX = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\avatar\Lena_QuatSkin_Avatar.fbx"
|
||||
op = ApiTypes.ImportExportOption()
|
||||
op.scale = 10.7 # donor upscale (v6 finding: swap drops ~5 cm)
|
||||
op.bAddArrangementPoints = True
|
||||
op.bAutoTranslate = True
|
||||
report["lena"] = import_api.ImportFBX(LENA_FBX, op)
|
||||
|
||||
# ---- single wrap panel with comb-teeth hem ---------------------------------
|
||||
W_TOP, W_HEM, H, TEETH_D = 1210.0, 1550.0, 390.0, 30.0
|
||||
PITCH, STRAND = 24.0, 17.0 # tooth pitch / strand width (gap 7 mm)
|
||||
flare = (W_HEM - W_TOP) / 2.0
|
||||
|
||||
pts = [(0.0, H, 0), (W_TOP, H, 0)] # line 0 = waist (top edge)
|
||||
pts.append((W_TOP + flare, TEETH_D, 0)) # right edge down to teeth top
|
||||
# teeth right -> left along the hem
|
||||
x = W_TOP + flare
|
||||
left_end = -flare
|
||||
n_teeth = 0
|
||||
while x - PITCH >= left_end - 0.5:
|
||||
pts.append((x, 0.0, 0)) # strand outer-bottom
|
||||
pts.append((x - STRAND, 0.0, 0)) # strand inner-bottom
|
||||
pts.append((x - STRAND, TEETH_D, 0)) # back up
|
||||
pts.append((x - PITCH, TEETH_D, 0)) # gap to next strand
|
||||
x -= PITCH
|
||||
n_teeth += 1
|
||||
pts.append((left_end, TEETH_D, 0)) # ensure left hem corner
|
||||
# left edge back up to the waist closes the polygon automatically
|
||||
report["n_teeth"] = n_teeth
|
||||
report["n_points"] = len(pts)
|
||||
|
||||
p_wrap = pattern_api.CreatePatternWithPoints(pts)
|
||||
report["pattern_id"] = p_wrap
|
||||
|
||||
# fabric + v2 texture
|
||||
fab = fabric_api.AddFabric(r"C:\Users\Public\Documents\MarvelousDesigner\New Assets\Fabric\(Default for Simulation).zfab")
|
||||
fabric_api.SetBaseTextureMapImageGivenFilePath(
|
||||
r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\textures\kilt_cloth.png", fab)
|
||||
pattern_api.SetPatternPieceFabricIndex(p_wrap, fab)
|
||||
|
||||
# arrange -- x=50 centres the panel on the front; overlap ends land at the back.
|
||||
# If the top view shows a half-shell or wrong overlap angle, sweep x next.
|
||||
arr = {a["ArrangementName"]: int(a["ArrangementIndex"])
|
||||
for a in pattern_api.GetArrangementList()}
|
||||
report["arrangement_count"] = len(arr)
|
||||
pattern_api.SetArrangement(p_wrap, arr["Leg_Skirt_Front"])
|
||||
pattern_api.SetArrangementPosition(p_wrap, 50, 96, 50)
|
||||
utility_api.ResetClothArrangement()
|
||||
|
||||
SHOTS = r"C:\Users\CAN\tinqs-ltd\animation\tools\tailor\screenshots"
|
||||
os.makedirs(SHOTS, exist_ok=True)
|
||||
utility_api.SetCamViewPoint(2)
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "kilt_v2_probe_front.png"))
|
||||
utility_api.SetCamViewPoint(5) # top-down: the ONLY view that shows wrap
|
||||
export_api.ExportSnapshot3D(os.path.join(SHOTS, "kilt_v2_probe_top.png"))
|
||||
utility_api.SetCamViewPoint(2)
|
||||
|
||||
result = report
|
||||