diff --git a/.gitignore b/.gitignore index d295cbf50..d8af5a631 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,9 @@ spikes/**/*.blend spikes/**/*.exr spikes/**/*.hdr spikes/**/*.zip +# Extracted vendor payload (reproducible from docs/assets/downloads/ archives +# via the spike's own extraction script) — includes Unity .asset/.sk metadata. +spikes/**/raw/ # Planet generator intermediates *.tmp.npz diff --git a/spikes/synty-intake/out/bodies_fit_log.json b/spikes/synty-intake/out/bodies_fit_log.json new file mode 100644 index 000000000..d6cc9fa06 --- /dev/null +++ b/spikes/synty-intake/out/bodies_fit_log.json @@ -0,0 +1,793 @@ +{ + "garment_glb": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/SK_SCFI_CIVL_09_10TORS_HU01_quaternius.glb", + "bodies": { + "average_m": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_hips", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 1.0, + "scale_z": 1.0, + "translate": [ + -0.0, + -0.0, + -0.0 + ] + }, + "aabb_post_affine": { + "min": [ + -0.2082, + -0.1677, + 0.8251 + ], + "max": [ + 0.2305, + 0.1739, + 1.59 + ] + }, + "warp": { + "max_displacement_mm": 13.2, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 1.058, + "max_shrink": 0.919, + "edges_gt_2x": 0, + "edges_lt_0.5x": 0, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.2083, + -0.1707, + 0.8236 + ], + "max": [ + 0.2337, + 0.1739, + 1.59 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -74.86, + "p05_signed_mm": -6.44, + "median_signed_mm": 36.03, + "max_signed_mm": 96.8, + "verts_inside_gt_3mm": 135, + "pct_inside": 6.26 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_average_m.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "average_f": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_hips", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 0.79, + "scale_z": 0.96031, + "translate": [ + 0.0, + 0.01173, + 0.02037 + ] + }, + "aabb_post_affine": { + "min": [ + -0.1645, + -0.1207, + 0.8127 + ], + "max": [ + 0.1821, + 0.1491, + 1.5473 + ] + }, + "warp": { + "max_displacement_mm": 65.3, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 1.712, + "max_shrink": 0.762, + "edges_gt_2x": 0, + "edges_lt_0.5x": 0, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.169, + -0.1566, + 0.8021 + ], + "max": [ + 0.2341, + 0.1404, + 1.5467 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -67.38, + "p05_signed_mm": -12.82, + "median_signed_mm": 26.3, + "max_signed_mm": 85.13, + "verts_inside_gt_3mm": 206, + "pct_inside": 9.56 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_average_f.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "muscular_m": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_hips", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 1.1048, + "scale_z": 1.0, + "translate": [ + 0.0, + -0.00451, + 0.0 + ] + }, + "aabb_post_affine": { + "min": [ + -0.23, + -0.1898, + 0.8251 + ], + "max": [ + 0.2546, + 0.1876, + 1.59 + ] + }, + "warp": { + "max_displacement_mm": 54.7, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 1.489, + "max_shrink": 0.638, + "edges_gt_2x": 0, + "edges_lt_0.5x": 0, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.239, + -0.2016, + 0.8217 + ], + "max": [ + 0.2662, + 0.2002, + 1.5928 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -67.99, + "p05_signed_mm": -9.31, + "median_signed_mm": 38.97, + "max_signed_mm": 110.81, + "verts_inside_gt_3mm": 177, + "pct_inside": 8.21 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_muscular_m.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "muscular_f": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_hips", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 0.79, + "scale_z": 0.96031, + "translate": [ + 0.0, + 0.01173, + 0.02037 + ] + }, + "aabb_post_affine": { + "min": [ + -0.1645, + -0.1207, + 0.8127 + ], + "max": [ + 0.1821, + 0.1491, + 1.5473 + ] + }, + "warp": { + "max_displacement_mm": 89.1, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 1.815, + "max_shrink": 0.744, + "edges_gt_2x": 0, + "edges_lt_0.5x": 0, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.1747, + -0.1679, + 0.8022 + ], + "max": [ + 0.237, + 0.1415, + 1.5473 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -72.04, + "p05_signed_mm": -12.99, + "median_signed_mm": 26.65, + "max_signed_mm": 85.26, + "verts_inside_gt_3mm": 189, + "pct_inside": 8.77 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_muscular_f.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "thin_m": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 1.0, + "scale_z": 1.0, + "translate": [ + -0.0, + -0.0, + -0.0 + ] + }, + "aabb_post_affine": { + "min": [ + -0.2082, + -0.1677, + 0.8251 + ], + "max": [ + 0.2305, + 0.1739, + 1.59 + ] + }, + "warp": { + "max_displacement_mm": 148.8, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 2.091, + "max_shrink": 0.232, + "edges_gt_2x": 1, + "edges_lt_0.5x": 14, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.1968, + -0.1693, + 0.8205 + ], + "max": [ + 0.212, + 0.1959, + 1.5056 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -63.51, + "p05_signed_mm": -8.58, + "median_signed_mm": 40.36, + "max_signed_mm": 124.52, + "verts_inside_gt_3mm": 126, + "pct_inside": 5.85 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_thin_m.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "thin_f": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 0.79, + "scale_z": 0.96031, + "translate": [ + 0.0, + 0.01173, + 0.02037 + ] + }, + "aabb_post_affine": { + "min": [ + -0.1645, + -0.1207, + 0.8127 + ], + "max": [ + 0.1821, + 0.1491, + 1.5473 + ] + }, + "warp": { + "max_displacement_mm": 105.2, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 2.589, + "max_shrink": 0.409, + "edges_gt_2x": 3, + "edges_lt_0.5x": 7, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.1598, + -0.1652, + 0.824 + ], + "max": [ + 0.223, + 0.1782, + 1.4946 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -50.52, + "p05_signed_mm": -18.1, + "median_signed_mm": 25.69, + "max_signed_mm": 85.58, + "verts_inside_gt_3mm": 286, + "pct_inside": 13.27 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_thin_f.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "heavy_m": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 1.0, + "scale_z": 1.0, + "translate": [ + -0.0, + -0.0, + -0.0 + ] + }, + "aabb_post_affine": { + "min": [ + -0.2082, + -0.1677, + 0.8251 + ], + "max": [ + 0.2305, + 0.1739, + 1.59 + ] + }, + "warp": { + "max_displacement_mm": 34.6, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 1.236, + "max_shrink": 0.839, + "edges_gt_2x": 0, + "edges_lt_0.5x": 0, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.22, + -0.1776, + 0.8241 + ], + "max": [ + 0.253, + 0.1837, + 1.5938 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -88.67, + "p05_signed_mm": -6.24, + "median_signed_mm": 31.44, + "max_signed_mm": 103.76, + "verts_inside_gt_3mm": 148, + "pct_inside": 6.87 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_heavy_m.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "heavy_f": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 0.79, + "scale_z": 0.96031, + "translate": [ + 0.0, + 0.01173, + 0.02037 + ] + }, + "aabb_post_affine": { + "min": [ + -0.1645, + -0.1207, + 0.8127 + ], + "max": [ + 0.1821, + 0.1491, + 1.5473 + ] + }, + "warp": { + "max_displacement_mm": 104.0, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 1.839, + "max_shrink": 0.678, + "edges_gt_2x": 0, + "edges_lt_0.5x": 0, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.1891, + -0.1631, + 0.8005 + ], + "max": [ + 0.1965, + 0.1545, + 1.549 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -69.85, + "p05_signed_mm": -15.38, + "median_signed_mm": 21.36, + "max_signed_mm": 87.94, + "verts_inside_gt_3mm": 275, + "pct_inside": 12.76 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_heavy_f.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "teen_m": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_hips", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 0.73705, + "scale_z": 0.84515, + "translate": [ + 0.0, + 0.00491, + 0.00582 + ] + }, + "aabb_post_affine": { + "min": [ + -0.1534, + -0.1187, + 0.7031 + ], + "max": [ + 0.1699, + 0.1331, + 1.3496 + ] + }, + "warp": { + "max_displacement_mm": 42.4, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 1.747, + "max_shrink": 0.819, + "edges_gt_2x": 0, + "edges_lt_0.5x": 0, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.1578, + -0.1319, + 0.7008 + ], + "max": [ + 0.1859, + 0.1363, + 1.3515 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -63.32, + "p05_signed_mm": -7.51, + "median_signed_mm": 25.93, + "max_signed_mm": 74.03, + "verts_inside_gt_3mm": 170, + "pct_inside": 7.89 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_teen_m.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "teen_f": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_hips", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 0.68165, + "scale_z": 0.82861, + "translate": [ + 0.0, + 0.01012, + 0.01758 + ] + }, + "aabb_post_affine": { + "min": [ + -0.1419, + -0.1042, + 0.7013 + ], + "max": [ + 0.1571, + 0.1286, + 1.3351 + ] + }, + "warp": { + "max_displacement_mm": 46.5, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 1.72, + "max_shrink": 0.759, + "edges_gt_2x": 0, + "edges_lt_0.5x": 0, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.1403, + -0.1322, + 0.684 + ], + "max": [ + 0.1987, + 0.1183, + 1.3355 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -41.05, + "p05_signed_mm": -6.08, + "median_signed_mm": 24.43, + "max_signed_mm": 70.84, + "verts_inside_gt_3mm": 120, + "pct_inside": 5.57 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_teen_f.glb", + "size_kb": 653 + }, + "result": "OK" + }, + "child": { + "garment_verts": 2155, + "segments_used": [ + "seg_torso", + "seg_hips", + "seg_neck", + "seg_arm_upper_l", + "seg_arm_upper_r", + "seg_leg_upper_l", + "seg_leg_upper_r" + ], + "affine": { + "scale_xy": 0.73705, + "scale_z": 0.84515, + "translate": [ + 0.0, + 0.00491, + 0.00582 + ] + }, + "aabb_post_affine": { + "min": [ + -0.1534, + -0.1187, + 0.7031 + ], + "max": [ + 0.1699, + 0.1331, + 1.3496 + ] + }, + "warp": { + "max_displacement_mm": 302.7, + "smooth_iterations": 20 + }, + "zero_weight_verts": 0, + "vgroups": 65, + "edge_distortion": { + "max_stretch": 2.619, + "max_shrink": 0.055, + "edges_gt_2x": 3, + "edges_lt_0.5x": 84, + "edges_total": 3799 + }, + "aabb_post_fit": { + "min": [ + -0.1403, + -0.1072, + 0.6974 + ], + "max": [ + 0.1622, + 0.1086, + 1.1344 + ] + }, + "signed_distance": { + "verts_sampled": 2155, + "min_signed_mm": -54.44, + "p05_signed_mm": -13.32, + "median_signed_mm": 26.92, + "max_signed_mm": 79.3, + "verts_inside_gt_3mm": 187, + "pct_inside": 8.68 + }, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_child.glb", + "size_kb": 653 + }, + "result": "OK" + } + } +} \ No newline at end of file diff --git a/spikes/synty-intake/out/bodies_recon.json b/spikes/synty-intake/out/bodies_recon.json new file mode 100644 index 000000000..68426d72d --- /dev/null +++ b/spikes/synty-intake/out/bodies_recon.json @@ -0,0 +1,1058 @@ +{ + "average_f": { + "segments": { + "seg_torso": { + "verts": 359, + "polys": 596, + "vgroups": 65 + }, + "seg_hips": { + "verts": 318, + "polys": 432, + "vgroups": 65 + }, + "seg_neck": { + "verts": 205, + "polys": 306, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 174, + "polys": 285, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 174, + "polys": 285, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 264, + "polys": 447, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 264, + "polys": 447, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + 0.0, + 0.0457, + 0.9318 + ], + "spine_01": [ + -0.0, + 0.0151, + 1.0546 + ], + "spine_02": [ + -0.0, + 0.0023, + 1.1642 + ], + "spine_03": [ + -0.0, + 0.0017, + 1.2906 + ], + "clavicle_l": [ + 0.0293, + -0.016, + 1.4305 + ], + "upperarm_l": [ + 0.1516, + 0.0543, + 1.4181 + ], + "thigh_l": [ + 0.1114, + 0.0518, + 0.9441 + ], + "neck_01": [ + -0.0, + 0.0406, + 1.4849 + ], + "Head": [ + -0.0, + 0.0109, + 1.5496 + ] + } + } + }, + "average_m": { + "segments": { + "seg_torso": { + "verts": 375, + "polys": 631, + "vgroups": 65 + }, + "seg_hips": { + "verts": 255, + "polys": 314, + "vgroups": 65 + }, + "seg_neck": { + "verts": 144, + "polys": 200, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 183, + "polys": 274, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 183, + "polys": 274, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 240, + "polys": 406, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 241, + "polys": 409, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + -0.0, + 0.043, + 0.9491 + ], + "spine_01": [ + -0.0, + 0.0069, + 1.072 + ], + "spine_02": [ + -0.0, + -0.0039, + 1.1778 + ], + "spine_03": [ + -0.0, + -0.0069, + 1.3109 + ], + "clavicle_l": [ + 0.0314, + -0.0335, + 1.4953 + ], + "upperarm_l": [ + 0.1919, + 0.0654, + 1.4555 + ], + "thigh_l": [ + 0.0906, + 0.036, + 0.9712 + ], + "neck_01": [ + -0.0, + 0.0414, + 1.5205 + ], + "Head": [ + -0.0, + 0.0174, + 1.5998 + ] + } + } + }, + "child": { + "segments": { + "seg_torso": { + "verts": 368, + "polys": 554, + "vgroups": 65 + }, + "seg_hips": { + "verts": 232, + "polys": 288, + "vgroups": 65 + }, + "seg_neck": { + "verts": 216, + "polys": 324, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 183, + "polys": 301, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 183, + "polys": 301, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 233, + "polys": 397, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 233, + "polys": 397, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + -0.0, + 0.0366, + 0.80795 + ], + "spine_01": [ + 0.0, + 0.00587, + 0.91257 + ], + "spine_02": [ + 0.0, + -0.00332, + 1.00264 + ], + "spine_03": [ + 0.0, + -0.00587, + 1.11594 + ], + "clavicle_l": [ + 0.02673, + -0.02852, + 1.27292 + ], + "upperarm_l": [ + 0.14144, + 0.05565, + 1.23593 + ], + "thigh_l": [ + 0.07713, + 0.03065, + 0.82676 + ], + "neck_01": [ + 0.0, + 0.03524, + 1.29437 + ], + "Head": [ + 0.0, + 0.01481, + 1.36479 + ] + } + } + }, + "heavy_f": { + "segments": { + "seg_torso": { + "verts": 1704, + "polys": 2912, + "vgroups": 65 + }, + "seg_hips": null, + "seg_neck": { + "verts": 1428, + "polys": 2478, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 667, + "polys": 1092, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 667, + "polys": 1092, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 606, + "polys": 1015, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 606, + "polys": 1015, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + 0.0, + 0.0457, + 0.9318 + ], + "spine_01": [ + -0.0, + 0.0151, + 1.0546 + ], + "spine_02": [ + -0.0, + 0.0023, + 1.1642 + ], + "spine_03": [ + -0.0, + 0.0017, + 1.2906 + ], + "clavicle_l": [ + 0.0293, + -0.016, + 1.4305 + ], + "upperarm_l": [ + 0.1516, + 0.0543, + 1.4181 + ], + "thigh_l": [ + 0.1114, + 0.0518, + 0.9441 + ], + "neck_01": [ + -0.0, + 0.0406, + 1.4849 + ], + "Head": [ + -0.0, + 0.0109, + 1.5496 + ] + } + } + }, + "heavy_m": { + "segments": { + "seg_torso": { + "verts": 1419, + "polys": 2394, + "vgroups": 65 + }, + "seg_hips": null, + "seg_neck": { + "verts": 532, + "polys": 842, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 671, + "polys": 1111, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 671, + "polys": 1111, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 425, + "polys": 718, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 442, + "polys": 735, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + -0.0, + 0.043, + 0.9491 + ], + "spine_01": [ + -0.0, + 0.0069, + 1.072 + ], + "spine_02": [ + -0.0, + -0.0039, + 1.1778 + ], + "spine_03": [ + -0.0, + -0.0069, + 1.3109 + ], + "clavicle_l": [ + 0.0314, + -0.0335, + 1.4953 + ], + "upperarm_l": [ + 0.1919, + 0.0654, + 1.4555 + ], + "thigh_l": [ + 0.0906, + 0.036, + 0.9712 + ], + "neck_01": [ + -0.0, + 0.0414, + 1.5205 + ], + "Head": [ + -0.0, + 0.0174, + 1.5998 + ] + } + } + }, + "muscular_f": { + "segments": { + "seg_torso": { + "verts": 393, + "polys": 630, + "vgroups": 65 + }, + "seg_hips": { + "verts": 288, + "polys": 378, + "vgroups": 65 + }, + "seg_neck": { + "verts": 210, + "polys": 324, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 198, + "polys": 319, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 198, + "polys": 319, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 267, + "polys": 452, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 267, + "polys": 452, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + 0.0, + 0.0457, + 0.9318 + ], + "spine_01": [ + -0.0, + 0.0151, + 1.0546 + ], + "spine_02": [ + -0.0, + 0.0023, + 1.1642 + ], + "spine_03": [ + -0.0, + 0.0017, + 1.2906 + ], + "clavicle_l": [ + 0.0293, + -0.016, + 1.4305 + ], + "upperarm_l": [ + 0.1516, + 0.0543, + 1.4181 + ], + "thigh_l": [ + 0.1114, + 0.0518, + 0.9441 + ], + "neck_01": [ + -0.0, + 0.0406, + 1.4849 + ], + "Head": [ + -0.0, + 0.0109, + 1.5496 + ] + } + } + }, + "muscular_m": { + "segments": { + "seg_torso": { + "verts": 395, + "polys": 650, + "vgroups": 65 + }, + "seg_hips": { + "verts": 266, + "polys": 354, + "vgroups": 65 + }, + "seg_neck": { + "verts": 227, + "polys": 346, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 182, + "polys": 286, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 182, + "polys": 286, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 256, + "polys": 425, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 256, + "polys": 425, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + 0.0, + 0.043, + 0.9491 + ], + "spine_01": [ + 0.0, + 0.0069, + 1.072 + ], + "spine_02": [ + 0.0, + -0.0039, + 1.1778 + ], + "spine_03": [ + -0.0, + -0.0069, + 1.3109 + ], + "clavicle_l": [ + 0.0314, + -0.0335, + 1.4953 + ], + "upperarm_l": [ + 0.21201, + 0.0654, + 1.4555 + ], + "thigh_l": [ + 0.11431, + 0.036, + 0.9712 + ], + "neck_01": [ + -0.0, + 0.0414, + 1.5205 + ], + "Head": [ + -0.0, + 0.0174, + 1.5998 + ] + } + } + }, + "teen_f": { + "segments": { + "seg_torso": { + "verts": 400, + "polys": 666, + "vgroups": 65 + }, + "seg_hips": { + "verts": 279, + "polys": 386, + "vgroups": 65 + }, + "seg_neck": { + "verts": 210, + "polys": 328, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 204, + "polys": 342, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 204, + "polys": 342, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 254, + "polys": 438, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 254, + "polys": 438, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + 0.0, + 0.03943, + 0.80401 + ], + "spine_01": [ + -0.0, + 0.01303, + 0.90997 + ], + "spine_02": [ + 0.0, + 0.00198, + 1.00454 + ], + "spine_03": [ + -0.0, + 0.00147, + 1.1136 + ], + "clavicle_l": [ + 0.02528, + -0.01381, + 1.23432 + ], + "upperarm_l": [ + 0.13081, + 0.04685, + 1.22362 + ], + "thigh_l": [ + 0.09612, + 0.0447, + 0.81462 + ], + "neck_01": [ + -0.0, + 0.03503, + 1.28126 + ], + "Head": [ + -0.0, + 0.00941, + 1.33708 + ] + } + } + }, + "teen_m": { + "segments": { + "seg_torso": { + "verts": 368, + "polys": 554, + "vgroups": 65 + }, + "seg_hips": { + "verts": 232, + "polys": 288, + "vgroups": 65 + }, + "seg_neck": { + "verts": 216, + "polys": 324, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 183, + "polys": 301, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 183, + "polys": 301, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 233, + "polys": 397, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 233, + "polys": 397, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + -0.0, + 0.0366, + 0.80795 + ], + "spine_01": [ + 0.0, + 0.00587, + 0.91257 + ], + "spine_02": [ + 0.0, + -0.00332, + 1.00264 + ], + "spine_03": [ + 0.0, + -0.00587, + 1.11594 + ], + "clavicle_l": [ + 0.02673, + -0.02852, + 1.27292 + ], + "upperarm_l": [ + 0.14144, + 0.05565, + 1.23593 + ], + "thigh_l": [ + 0.07713, + 0.03065, + 0.82676 + ], + "neck_01": [ + 0.0, + 0.03524, + 1.29437 + ], + "Head": [ + 0.0, + 0.01481, + 1.36479 + ] + } + } + }, + "thin_f": { + "segments": { + "seg_torso": { + "verts": 1704, + "polys": 2912, + "vgroups": 65 + }, + "seg_hips": null, + "seg_neck": { + "verts": 1428, + "polys": 2478, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 667, + "polys": 1092, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 667, + "polys": 1092, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 606, + "polys": 1015, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 606, + "polys": 1015, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + 0.0, + 0.0457, + 0.9318 + ], + "spine_01": [ + -0.0, + 0.0151, + 1.0546 + ], + "spine_02": [ + -0.0, + 0.0023, + 1.1642 + ], + "spine_03": [ + -0.0, + 0.0017, + 1.2906 + ], + "clavicle_l": [ + 0.0293, + -0.016, + 1.4305 + ], + "upperarm_l": [ + 0.1516, + 0.0543, + 1.4181 + ], + "thigh_l": [ + 0.1114, + 0.0518, + 0.9441 + ], + "neck_01": [ + -0.0, + 0.0406, + 1.4849 + ], + "Head": [ + -0.0, + 0.0109, + 1.5496 + ] + } + } + }, + "thin_m": { + "segments": { + "seg_torso": { + "verts": 1419, + "polys": 2394, + "vgroups": 65 + }, + "seg_hips": null, + "seg_neck": { + "verts": 532, + "polys": 842, + "vgroups": 65 + }, + "seg_arm_upper_l": { + "verts": 671, + "polys": 1111, + "vgroups": 65 + }, + "seg_arm_upper_r": { + "verts": 671, + "polys": 1111, + "vgroups": 65 + }, + "seg_leg_upper_l": { + "verts": 425, + "polys": 718, + "vgroups": 65 + }, + "seg_leg_upper_r": { + "verts": 442, + "polys": 735, + "vgroups": 65 + } + }, + "armature": { + "bones": 65, + "landmarks": { + "pelvis": [ + -0.0, + 0.043, + 0.9491 + ], + "spine_01": [ + -0.0, + 0.0069, + 1.072 + ], + "spine_02": [ + -0.0, + -0.0039, + 1.1778 + ], + "spine_03": [ + -0.0, + -0.0069, + 1.3109 + ], + "clavicle_l": [ + 0.0314, + -0.0335, + 1.4953 + ], + "upperarm_l": [ + 0.1919, + 0.0654, + 1.4555 + ], + "thigh_l": [ + 0.0906, + 0.036, + 0.9712 + ], + "neck_01": [ + -0.0, + 0.0414, + 1.5205 + ], + "Head": [ + -0.0, + 0.0174, + 1.5998 + ] + } + } + }, + "_topology_vs_average_m": { + "average_f": [ + "seg_torso: 359v/596p vs ref 375v/631p", + "seg_hips: 318v/432p vs ref 255v/314p", + "seg_neck: 205v/306p vs ref 144v/200p", + "seg_arm_upper_l: 174v/285p vs ref 183v/274p", + "seg_arm_upper_r: 174v/285p vs ref 183v/274p", + "seg_leg_upper_l: 264v/447p vs ref 240v/406p", + "seg_leg_upper_r: 264v/447p vs ref 241v/409p" + ], + "average_m": "MATCH", + "child": [ + "seg_torso: 368v/554p vs ref 375v/631p", + "seg_hips: 232v/288p vs ref 255v/314p", + "seg_neck: 216v/324p vs ref 144v/200p", + "seg_arm_upper_l: 183v/301p vs ref 183v/274p", + "seg_arm_upper_r: 183v/301p vs ref 183v/274p", + "seg_leg_upper_l: 233v/397p vs ref 240v/406p", + "seg_leg_upper_r: 233v/397p vs ref 241v/409p" + ], + "heavy_f": [ + "seg_torso: 1704v/2912p vs ref 375v/631p", + "seg_hips: missing", + "seg_neck: 1428v/2478p vs ref 144v/200p", + "seg_arm_upper_l: 667v/1092p vs ref 183v/274p", + "seg_arm_upper_r: 667v/1092p vs ref 183v/274p", + "seg_leg_upper_l: 606v/1015p vs ref 240v/406p", + "seg_leg_upper_r: 606v/1015p vs ref 241v/409p" + ], + "heavy_m": [ + "seg_torso: 1419v/2394p vs ref 375v/631p", + "seg_hips: missing", + "seg_neck: 532v/842p vs ref 144v/200p", + "seg_arm_upper_l: 671v/1111p vs ref 183v/274p", + "seg_arm_upper_r: 671v/1111p vs ref 183v/274p", + "seg_leg_upper_l: 425v/718p vs ref 240v/406p", + "seg_leg_upper_r: 442v/735p vs ref 241v/409p" + ], + "muscular_f": [ + "seg_torso: 393v/630p vs ref 375v/631p", + "seg_hips: 288v/378p vs ref 255v/314p", + "seg_neck: 210v/324p vs ref 144v/200p", + "seg_arm_upper_l: 198v/319p vs ref 183v/274p", + "seg_arm_upper_r: 198v/319p vs ref 183v/274p", + "seg_leg_upper_l: 267v/452p vs ref 240v/406p", + "seg_leg_upper_r: 267v/452p vs ref 241v/409p" + ], + "muscular_m": [ + "seg_torso: 395v/650p vs ref 375v/631p", + "seg_hips: 266v/354p vs ref 255v/314p", + "seg_neck: 227v/346p vs ref 144v/200p", + "seg_arm_upper_l: 182v/286p vs ref 183v/274p", + "seg_arm_upper_r: 182v/286p vs ref 183v/274p", + "seg_leg_upper_l: 256v/425p vs ref 240v/406p", + "seg_leg_upper_r: 256v/425p vs ref 241v/409p" + ], + "teen_f": [ + "seg_torso: 400v/666p vs ref 375v/631p", + "seg_hips: 279v/386p vs ref 255v/314p", + "seg_neck: 210v/328p vs ref 144v/200p", + "seg_arm_upper_l: 204v/342p vs ref 183v/274p", + "seg_arm_upper_r: 204v/342p vs ref 183v/274p", + "seg_leg_upper_l: 254v/438p vs ref 240v/406p", + "seg_leg_upper_r: 254v/438p vs ref 241v/409p" + ], + "teen_m": [ + "seg_torso: 368v/554p vs ref 375v/631p", + "seg_hips: 232v/288p vs ref 255v/314p", + "seg_neck: 216v/324p vs ref 144v/200p", + "seg_arm_upper_l: 183v/301p vs ref 183v/274p", + "seg_arm_upper_r: 183v/301p vs ref 183v/274p", + "seg_leg_upper_l: 233v/397p vs ref 240v/406p", + "seg_leg_upper_r: 233v/397p vs ref 241v/409p" + ], + "thin_f": [ + "seg_torso: 1704v/2912p vs ref 375v/631p", + "seg_hips: missing", + "seg_neck: 1428v/2478p vs ref 144v/200p", + "seg_arm_upper_l: 667v/1092p vs ref 183v/274p", + "seg_arm_upper_r: 667v/1092p vs ref 183v/274p", + "seg_leg_upper_l: 606v/1015p vs ref 240v/406p", + "seg_leg_upper_r: 606v/1015p vs ref 241v/409p" + ], + "thin_m": [ + "seg_torso: 1419v/2394p vs ref 375v/631p", + "seg_hips: missing", + "seg_neck: 532v/842p vs ref 144v/200p", + "seg_arm_upper_l: 671v/1111p vs ref 183v/274p", + "seg_arm_upper_r: 671v/1111p vs ref 183v/274p", + "seg_leg_upper_l: 425v/718p vs ref 240v/406p", + "seg_leg_upper_r: 442v/735p vs ref 241v/409p" + ] + } +} \ No newline at end of file diff --git a/spikes/synty-intake/out/recon_rest_pose.json b/spikes/synty-intake/out/recon_rest_pose.json new file mode 100644 index 000000000..b9492a41c --- /dev/null +++ b/spikes/synty-intake/out/recon_rest_pose.json @@ -0,0 +1,771 @@ +{ + "garment_objects": [ + { + "name": "Armature", + "type": "ARMATURE", + "scale": [ + 0.01, + 0.01, + 0.01 + ], + "location": [ + 0.0, + 0.0, + 0.0 + ] + }, + { + "name": "SK_SCFI_CIVL_09_10TORS_HU01", + "type": "MESH", + "scale": [ + 1.0, + 1.0, + 1.0 + ], + "location": [ + 0.0, + 0.0, + 0.0 + ] + } + ], + "sidekick_bones": { + "pelvis": { + "head": [ + 0.0, + 0.02817, + 0.91021 + ], + "tail": [ + 0.0, + -0.0946, + 0.91021 + ] + }, + "thigh_l": { + "head": [ + 0.09613, + 0.03111, + 0.883 + ], + "tail": [ + 0.09622, + 0.36502, + 0.87676 + ] + }, + "thigh_r": { + "head": [ + -0.09613, + 0.03111, + 0.883 + ], + "tail": [ + -0.09604, + -0.30279, + 0.88923 + ] + }, + "spine_01": { + "head": [ + 0.0, + 0.02533, + 0.9497 + ], + "tail": [ + 0.0, + 0.09286, + 0.96049 + ] + }, + "spine_02": { + "head": [ + 0.0, + 0.01595, + 1.01744 + ], + "tail": [ + 0.0, + 0.1409, + 1.03122 + ] + }, + "spine_03": { + "head": [ + 0.0, + 0.0085, + 1.14293 + ], + "tail": [ + 1e-05, + 0.26263, + 1.14473 + ] + }, + "neck_01": { + "head": [ + 0.0, + 0.04285, + 1.46301 + ], + "tail": [ + 0.0, + 0.13851, + 1.49219 + ] + }, + "head": { + "head": [ + 0.0, + 0.01367, + 1.55867 + ], + "tail": [ + 0.0, + 0.13291, + 1.55867 + ] + }, + "clavicle_l": { + "head": [ + 0.0198, + 0.0427, + 1.36663 + ], + "tail": [ + 0.01704, + 0.22638, + 1.37059 + ] + }, + "upperarm_l": { + "head": [ + 0.19434, + 0.04519, + 1.37329 + ], + "tail": [ + 0.18605, + 0.25779, + 1.37358 + ] + }, + "clavicle_r": { + "head": [ + -0.0198, + 0.0427, + 1.36663 + ], + "tail": [ + -0.02256, + -0.14098, + 1.36267 + ] + }, + "upperarm_r": { + "head": [ + -0.19434, + 0.04519, + 1.37329 + ], + "tail": [ + -0.18605, + 0.25779, + 1.37358 + ] + } + }, + "garment_meshes": { + "SK_SCFI_CIVL_09_10TORS_HU01": { + "verts": 919, + "bbox": { + "min": [ + -0.21081, + -0.18519, + 0.79682 + ], + "max": [ + 0.23342, + 0.1607, + 1.49631 + ], + "dims": [ + 0.44423, + 0.34589, + 0.69949 + ] + }, + "vgroups": [ + "pelvis", + "thigh_l", + "thigh_r", + "spine_01", + "spine_02", + "spine_03", + "neck_01", + "head", + "clavicle_l", + "upperarm_l", + "clavicle_r", + "upperarm_r" + ], + "shape_keys": [ + "Basis", + "defaultBuff", + "masculineFeminine", + "defaultHeavy", + "defaultSkinny" + ] + } + }, + "our_armature_bone_count": 65, + "our_bones": { + "pelvis": { + "head": [ + -0.0, + 0.043, + 0.9491 + ], + "tail": [ + -0.0, + 0.01664, + 1.03883 + ] + }, + "thigh_l": { + "head": [ + 0.0906, + 0.036, + 0.9712 + ], + "tail": [ + 0.0906, + 0.0361, + 0.5424 + ] + }, + "thigh_r": { + "head": [ + -0.0906, + 0.036, + 0.9712 + ], + "tail": [ + -0.0906, + 0.0361, + 0.5424 + ] + }, + "spine_01": { + "head": [ + -0.0, + 0.0069, + 1.072 + ], + "tail": [ + -0.0, + -0.0039, + 1.1778 + ] + }, + "spine_02": { + "head": [ + -0.0, + -0.0039, + 1.1778 + ], + "tail": [ + -0.0, + -0.0069, + 1.3109 + ] + }, + "spine_03": { + "head": [ + -0.0, + -0.0069, + 1.3109 + ], + "tail": [ + -0.0, + 0.03553, + 1.49501 + ] + }, + "neck_01": { + "head": [ + -0.0, + 0.0414, + 1.5205 + ], + "tail": [ + -0.0, + 0.0174, + 1.5998 + ] + }, + "Head": { + "head": [ + -0.0, + 0.0174, + 1.5998 + ], + "tail": [ + -0.0, + 0.015, + 1.68262 + ] + }, + "clavicle_l": { + "head": [ + 0.0314, + -0.0335, + 1.4953 + ], + "tail": [ + 0.20039, + 0.05906, + 1.49517 + ] + }, + "upperarm_l": { + "head": [ + 0.1919, + 0.0654, + 1.4555 + ], + "tail": [ + 0.4429, + 0.073, + 1.4555 + ] + }, + "clavicle_r": { + "head": [ + -0.0314, + -0.0335, + 1.4953 + ], + "tail": [ + -0.20039, + 0.05906, + 1.49517 + ] + }, + "upperarm_r": { + "head": [ + -0.1919, + 0.0654, + 1.4555 + ], + "tail": [ + -0.4429, + 0.073, + 1.4555 + ] + } + }, + "bone_head_deltas": { + "pelvis": { + "delta": [ + 0.0, + -0.01483, + -0.03889 + ], + "magnitude_m": 0.04162 + }, + "thigh_l": { + "delta": [ + 0.00553, + -0.00489, + -0.0882 + ], + "magnitude_m": 0.08851 + }, + "thigh_r": { + "delta": [ + -0.00553, + -0.00489, + -0.0882 + ], + "magnitude_m": 0.08851 + }, + "spine_01": { + "delta": [ + 0.0, + 0.01843, + -0.1223 + ], + "magnitude_m": 0.12368 + }, + "spine_02": { + "delta": [ + 0.0, + 0.01985, + -0.16036 + ], + "magnitude_m": 0.16158 + }, + "spine_03": { + "delta": [ + 0.0, + 0.0154, + -0.16797 + ], + "magnitude_m": 0.16867 + }, + "neck_01": { + "delta": [ + 0.0, + 0.00145, + -0.05749 + ], + "magnitude_m": 0.05751 + }, + "head": { + "delta": [ + 0.0, + -0.00373, + -0.04113 + ], + "magnitude_m": 0.0413 + }, + "clavicle_l": { + "delta": [ + -0.0116, + 0.0762, + -0.12867 + ], + "magnitude_m": 0.14999 + }, + "upperarm_l": { + "delta": [ + 0.00244, + -0.02021, + -0.08221 + ], + "magnitude_m": 0.08469 + }, + "clavicle_r": { + "delta": [ + 0.0116, + 0.0762, + -0.12867 + ], + "magnitude_m": 0.14999 + }, + "upperarm_r": { + "delta": [ + -0.00244, + -0.02021, + -0.08221 + ], + "magnitude_m": 0.08469 + } + }, + "our_body_segments": { + "seg_torso": { + "verts": 375, + "bbox": { + "min": [ + -0.15826, + -0.10161, + 1.03118 + ], + "max": [ + 0.15826, + 0.12067, + 1.35323 + ], + "dims": [ + 0.31652, + 0.22228, + 0.32205 + ] + }, + "vgroups": [ + "root", + "pelvis", + "spine_01", + "spine_02", + "spine_03", + "neck_01", + "Head", + "clavicle_l", + "upperarm_l", + "lowerarm_l", + "hand_l", + "index_01_l", + "index_02_l", + "index_03_l", + "index_04_leaf_l", + "middle_01_l", + "middle_02_l", + "middle_03_l", + "middle_04_leaf_l", + "pinky_01_l", + "pinky_02_l", + "pinky_03_l", + "pinky_04_leaf_l", + "ring_01_l", + "ring_02_l", + "ring_03_l", + "ring_04_leaf_l", + "thumb_01_l", + "thumb_02_l", + "thumb_03_l", + "thumb_04_leaf_l", + "clavicle_r", + "upperarm_r", + "lowerarm_r", + "hand_r", + "index_01_r", + "index_02_r", + "index_03_r", + "index_04_leaf_r", + "middle_01_r", + "middle_02_r", + "middle_03_r", + "middle_04_leaf_r", + "pinky_01_r", + "pinky_02_r", + "pinky_03_r", + "pinky_04_leaf_r", + "ring_01_r", + "ring_02_r", + "ring_03_r", + "ring_04_leaf_r", + "thumb_01_r", + "thumb_02_r", + "thumb_03_r", + "thumb_04_leaf_r", + "thigh_l", + "calf_l", + "foot_l", + "ball_l", + "ball_leaf_l", + "thigh_r", + "calf_r", + "foot_r", + "ball_r", + "ball_leaf_r" + ] + }, + "seg_hips": { + "verts": 42, + "bbox": { + "min": [ + -0.95106, + -1.0, + -1.0 + ], + "max": [ + 0.95106, + 1.0, + 1.0 + ], + "dims": [ + 1.90212, + 2.0, + 2.0 + ] + }, + "vgroups": [] + }, + "seg_neck": { + "verts": 144, + "bbox": { + "min": [ + -0.07635, + -0.02606, + 1.53878 + ], + "max": [ + 0.07635, + 0.10504, + 1.61028 + ], + "dims": [ + 0.1527, + 0.1311, + 0.0715 + ] + }, + "vgroups": [ + "root", + "pelvis", + "spine_01", + "spine_02", + "spine_03", + "neck_01", + "Head", + "clavicle_l", + "upperarm_l", + "lowerarm_l", + "hand_l", + "index_01_l", + "index_02_l", + "index_03_l", + "index_04_leaf_l", + "middle_01_l", + "middle_02_l", + "middle_03_l", + "middle_04_leaf_l", + "pinky_01_l", + "pinky_02_l", + "pinky_03_l", + "pinky_04_leaf_l", + "ring_01_l", + "ring_02_l", + "ring_03_l", + "ring_04_leaf_l", + "thumb_01_l", + "thumb_02_l", + "thumb_03_l", + "thumb_04_leaf_l", + "clavicle_r", + "upperarm_r", + "lowerarm_r", + "hand_r", + "index_01_r", + "index_02_r", + "index_03_r", + "index_04_leaf_r", + "middle_01_r", + "middle_02_r", + "middle_03_r", + "middle_04_leaf_r", + "pinky_01_r", + "pinky_02_r", + "pinky_03_r", + "pinky_04_leaf_r", + "ring_01_r", + "ring_02_r", + "ring_03_r", + "ring_04_leaf_r", + "thumb_01_r", + "thumb_02_r", + "thumb_03_r", + "thumb_04_leaf_r", + "thigh_l", + "calf_l", + "foot_l", + "ball_l", + "ball_leaf_l", + "thigh_r", + "calf_r", + "foot_r", + "ball_r", + "ball_leaf_r" + ] + }, + "seg_arm_upper_l": { + "verts": 183, + "bbox": { + "min": [ + 0.15183, + -0.00827, + 1.38075 + ], + "max": [ + 0.44693, + 0.13217, + 1.51998 + ], + "dims": [ + 0.2951, + 0.14044, + 0.13923 + ] + }, + "vgroups": [ + "root", + "pelvis", + "spine_01", + "spine_02", + "spine_03", + "neck_01", + "Head", + "clavicle_l", + "upperarm_l", + "lowerarm_l", + "hand_l", + "index_01_l", + "index_02_l", + "index_03_l", + "index_04_leaf_l", + "middle_01_l", + "middle_02_l", + "middle_03_l", + "middle_04_leaf_l", + "pinky_01_l", + "pinky_02_l", + "pinky_03_l", + "pinky_04_leaf_l", + "ring_01_l", + "ring_02_l", + "ring_03_l", + "ring_04_leaf_l", + "thumb_01_l", + "thumb_02_l", + "thumb_03_l", + "thumb_04_leaf_l", + "clavicle_r", + "upperarm_r", + "lowerarm_r", + "hand_r", + "index_01_r", + "index_02_r", + "index_03_r", + "index_04_leaf_r", + "middle_01_r", + "middle_02_r", + "middle_03_r", + "middle_04_leaf_r", + "pinky_01_r", + "pinky_02_r", + "pinky_03_r", + "pinky_04_leaf_r", + "ring_01_r", + "ring_02_r", + "ring_03_r", + "ring_04_leaf_r", + "thumb_01_r", + "thumb_02_r", + "thumb_03_r", + "thumb_04_leaf_r", + "thigh_l", + "calf_l", + "foot_l", + "ball_l", + "ball_leaf_l", + "thigh_r", + "calf_r", + "foot_r", + "ball_r", + "ball_leaf_r" + ] + }, + "seg_arm_upper_r": { + "verts": 42, + "bbox": { + "min": [ + -0.95106, + -1.0, + -1.0 + ], + "max": [ + 0.95106, + 1.0, + 1.0 + ], + "dims": [ + 1.90212, + 2.0, + 2.0 + ] + }, + "vgroups": [] + } + } +} \ No newline at end of file diff --git a/spikes/synty-intake/out/transplant_log.json b/spikes/synty-intake/out/transplant_log.json new file mode 100644 index 000000000..40f80c17d --- /dev/null +++ b/spikes/synty-intake/out/transplant_log.json @@ -0,0 +1,248 @@ +{ + "garment": { + "verts": 919, + "sidekick_vgroups": [ + "pelvis", + "thigh_l", + "thigh_r", + "spine_01", + "spine_02", + "spine_03", + "neck_01", + "head", + "clavicle_l", + "upperarm_l", + "clavicle_r", + "upperarm_r" + ] + }, + "alignment": { + "scale_xy": 0.98744, + "scale_z": 1.09356, + "translate": [ + -0.0, + 0.01518, + -0.04628 + ], + "sk_pelvis": [ + 0.0, + 0.02817, + 0.91021 + ], + "our_pelvis": [ + -0.0, + 0.043, + 0.9491 + ], + "sk_upperarm_l": [ + 0.19434, + 0.04519, + 1.37329 + ], + "our_upperarm_l": [ + 0.1919, + 0.0654, + 1.4555 + ] + }, + "post_affine_bone_residuals": { + "pelvis": { + "residual_m": 0.0, + "delta": [ + 0.0, + 0.0, + 0.0 + ] + }, + "thigh_l": { + "residual_m": 0.05298, + "delta": [ + 0.00432, + 0.00991, + -0.05186 + ] + }, + "thigh_r": { + "residual_m": 0.05298, + "delta": [ + -0.00433, + 0.00991, + -0.05186 + ] + }, + "spine_01": { + "residual_m": 0.08639, + "delta": [ + -0.0, + 0.03329, + -0.07972 + ] + }, + "spine_02": { + "residual_m": 0.11676, + "delta": [ + -0.0, + 0.03483, + -0.11144 + ] + }, + "spine_03": { + "residual_m": 0.11156, + "delta": [ + -0.0, + 0.03047, + -0.10732 + ] + }, + "neck_01": { + "residual_m": 0.03682, + "delta": [ + -0.0, + 0.0161, + 0.03311 + ] + }, + "head": { + "residual_m": 0.0595, + "delta": [ + -0.0, + 0.01128, + 0.05843 + ] + }, + "clavicle_l": { + "residual_m": 0.10301, + "delta": [ + -0.01185, + 0.09085, + -0.04708 + ] + }, + "upperarm_l": { + "residual_m": 0.00559, + "delta": [ + -0.0, + -0.00559, + 0.0 + ] + }, + "clavicle_r": { + "residual_m": 0.10301, + "delta": [ + 0.01185, + 0.09085, + -0.04708 + ] + }, + "upperarm_r": { + "residual_m": 0.00559, + "delta": [ + -0.0, + -0.00559, + 0.0 + ] + } + }, + "weight_source_segments": [ + { + "name": "RegularMale.001", + "verts": 375 + }, + { + "name": "RegularMale.002", + "verts": 255 + }, + { + "name": "RegularMale.003", + "verts": 144 + }, + { + "name": "RegularMale.004", + "verts": 183 + }, + { + "name": "RegularMale.005", + "verts": 183 + }, + { + "name": "RegularMale.006", + "verts": 240 + }, + { + "name": "RegularMale.007", + "verts": 241 + } + ], + "transferred_vgroups": [ + "root", + "pelvis", + "spine_01", + "spine_02", + "spine_03", + "neck_01", + "Head", + "clavicle_l", + "upperarm_l", + "lowerarm_l", + "hand_l", + "index_01_l", + "index_02_l", + "index_03_l", + "index_04_leaf_l", + "middle_01_l", + "middle_02_l", + "middle_03_l", + "middle_04_leaf_l", + "pinky_01_l", + "pinky_02_l", + "pinky_03_l", + "pinky_04_leaf_l", + "ring_01_l", + "ring_02_l", + "ring_03_l", + "ring_04_leaf_l", + "thumb_01_l", + "thumb_02_l", + "thumb_03_l", + "thumb_04_leaf_l", + "clavicle_r", + "upperarm_r", + "lowerarm_r", + "hand_r", + "index_01_r", + "index_02_r", + "index_03_r", + "index_04_leaf_r", + "middle_01_r", + "middle_02_r", + "middle_03_r", + "middle_04_leaf_r", + "pinky_01_r", + "pinky_02_r", + "pinky_03_r", + "pinky_04_leaf_r", + "ring_01_r", + "ring_02_r", + "ring_03_r", + "ring_04_leaf_r", + "thumb_01_r", + "thumb_02_r", + "thumb_03_r", + "thumb_04_leaf_r", + "thigh_l", + "calf_l", + "foot_l", + "ball_l", + "ball_leaf_l", + "thigh_r", + "calf_r", + "foot_r", + "ball_r", + "ball_leaf_r" + ], + "zero_weight_verts": 0, + "export": { + "path": "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/SK_SCFI_CIVL_09_10TORS_HU01_quaternius.glb", + "size_kb": 653 + } +} \ No newline at end of file diff --git a/spikes/synty-intake/scripts/01_recon_rest_pose.py b/spikes/synty-intake/scripts/01_recon_rest_pose.py new file mode 100644 index 000000000..69a503c6c --- /dev/null +++ b/spikes/synty-intake/scripts/01_recon_rest_pose.py @@ -0,0 +1,137 @@ +""" +Spike T-1089 / Synty Sidekick intake — step 1: rest-pose recon. + +Imports the Sidekick garment FBX (with its armature) and our Quaternius +armature.glb + average_m body segments, then prints: + - world-space bone head/tail positions for the 12 vertex-group bones + in BOTH armatures + per-bone delta + - bounding boxes of the garment vs our torso-region body segments + - object transforms as-imported (scale factors etc.) + +Run: + tooling/blender --background --python \ + spikes/synty-intake/scripts/01_recon_rest_pose.py +""" + +import bpy +import json +import os + +REPO = "/var/mnt/data/projects/settled-reach" +GARMENT_FBX = os.path.join( + REPO, "spikes/synty-intake/raw/Assets/Synty/SidekickCharacters/Resources", + "Meshes/Outfits/Starter/SK_SCFI_CIVL_09_10TORS_HU01.fbx") +OUR_ARMATURE_GLB = os.path.join(REPO, "client/assets/characters/skeleton/armature.glb") +BODY_DIR = os.path.join(REPO, "client/assets/characters/bodies/average_m") +BODY_SEGS = ["seg_torso", "seg_hips", "seg_neck", "seg_arm_upper_l", "seg_arm_upper_r"] + +BOUND_BONES = ["pelvis", "thigh_l", "thigh_r", "spine_01", "spine_02", "spine_03", + "neck_01", "head", "clavicle_l", "upperarm_l", "clavicle_r", + "upperarm_r"] + +OUT_JSON = os.path.join(REPO, "spikes/synty-intake/out/recon_rest_pose.json") + + +def clear_scene(): + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + + +def import_new(op, path, **kw): + before = set(bpy.data.objects) + op(filepath=path, **kw) + return list(set(bpy.data.objects) - before) + + +def find_armature(objs): + for o in objs: + if o.type == 'ARMATURE': + return o + return None + + +def bone_world(arm_obj, name): + b = arm_obj.data.bones.get(name) + if b is None: + return None + mw = arm_obj.matrix_world + h = mw @ b.head_local + t = mw @ b.tail_local + return {"head": [round(v, 5) for v in h], "tail": [round(v, 5) for v in t]} + + +def mesh_world_bbox(obj): + import mathutils + pts = [obj.matrix_world @ mathutils.Vector(c) for c in obj.bound_box] + lo = [round(min(p[i] for p in pts), 5) for i in range(3)] + hi = [round(max(p[i] for p in pts), 5) for i in range(3)] + return {"min": lo, "max": hi, + "dims": [round(hi[i] - lo[i], 5) for i in range(3)]} + + +clear_scene() + +report = {} + +# --- garment FBX --- +g_objs = import_new(bpy.ops.import_scene.fbx, GARMENT_FBX) +g_arm = find_armature(g_objs) +g_meshes = [o for o in g_objs if o.type == 'MESH'] +report["garment_objects"] = [ + {"name": o.name, "type": o.type, + "scale": [round(s, 6) for s in o.scale], + "location": [round(v, 6) for v in o.location]} for o in g_objs] +report["sidekick_bones"] = {} +for bn in BOUND_BONES: + report["sidekick_bones"][bn] = bone_world(g_arm, bn) +report["garment_meshes"] = {} +for m in g_meshes: + report["garment_meshes"][m.name] = { + "verts": len(m.data.vertices), + "bbox": mesh_world_bbox(m), + "vgroups": [vg.name for vg in m.vertex_groups], + "shape_keys": ([kb.name for kb in m.data.shape_keys.key_blocks] + if m.data.shape_keys else []), + } + +# --- our armature --- +o_objs = import_new(bpy.ops.import_scene.gltf, OUR_ARMATURE_GLB) +o_arm = find_armature(o_objs) +report["our_armature_bone_count"] = len(o_arm.data.bones) +report["our_bones"] = {} +deltas = {} +for bn in BOUND_BONES: + ours = bn if bn != "head" else "Head" + info = bone_world(o_arm, ours) + report["our_bones"][ours] = info + sk = report["sidekick_bones"][bn] + if info and sk: + d = [round(sk["head"][i] - info["head"][i], 5) for i in range(3)] + mag = round(sum(x * x for x in d) ** 0.5, 5) + deltas[bn] = {"delta": d, "magnitude_m": mag} +report["bone_head_deltas"] = deltas + +# --- our body segments (proportion reference) --- +report["our_body_segments"] = {} +for seg in BODY_SEGS: + path = os.path.join(BODY_DIR, seg + ".glb") + objs = import_new(bpy.ops.import_scene.gltf, path) + for m in objs: + if m.type == 'MESH': + report["our_body_segments"][seg] = { + "verts": len(m.data.vertices), + "bbox": mesh_world_bbox(m), + "vgroups": [vg.name for vg in m.vertex_groups], + } + +os.makedirs(os.path.dirname(OUT_JSON), exist_ok=True) +with open(OUT_JSON, "w") as f: + json.dump(report, f, indent=2) + +print("\n=== RECON SUMMARY ===") +print(json.dumps(deltas, indent=2)) +print("Garment bbox:", json.dumps( + {k: v["bbox"]["dims"] for k, v in report["garment_meshes"].items()})) +print("Body seg bbox:", json.dumps( + {k: v["bbox"]["dims"] for k, v in report["our_body_segments"].items()})) +print("Report written:", OUT_JSON) diff --git a/spikes/synty-intake/scripts/02_transplant_garment.py b/spikes/synty-intake/scripts/02_transplant_garment.py new file mode 100644 index 000000000..f5cc08fdc --- /dev/null +++ b/spikes/synty-intake/scripts/02_transplant_garment.py @@ -0,0 +1,263 @@ +""" +Spike T-1089 / Synty Sidekick intake — step 2: transplant. + +Route decision (from 01_recon_rest_pose.py, out/recon_rest_pose.json): + Route 1 (rename-and-rebind, keep Sidekick weights verbatim) REJECTED on + measured evidence: spine_02/spine_03 joint heads sit 0.160/0.168 m lower in + the Sidekick rig than ours, shoulders 0.082 m lower, clavicles 0.150 m off. + Rest pose would render fine (inverse binds absorb it) but every spine/shoulder + rotation would pivot the garment around joints up to 17 cm away from where the + body underneath pivots -> swim/clip under animation. + Route 2 (proven pipeline): affine-align garment onto our average_m body, + then Data Transfer POLYINTERP_NEAREST weights from OUR body mesh, normalize, + bind to OUR armature. + +Alignment transform (documented, measured from bone landmarks): + scale XY by ours_shoulder_x / sk_shoulder_x + scale Z by (ours_upperarm_z - ours_pelvis_z) / (sk_upperarm_z - sk_pelvis_z) + then translate so the pelvis landmarks coincide. + +Run: + tooling/blender --background --python \ + spikes/synty-intake/scripts/02_transplant_garment.py +""" + +import bpy +import json +import os +from mathutils import Matrix, Vector + +REPO = "/var/mnt/data/projects/settled-reach" +GARMENT_FBX = os.path.join( + REPO, "spikes/synty-intake/raw/Assets/Synty/SidekickCharacters/Resources", + "Meshes/Outfits/Starter/SK_SCFI_CIVL_09_10TORS_HU01.fbx") +OUR_ARMATURE_GLB = os.path.join(REPO, "client/assets/characters/skeleton/armature.glb") +BODY_DIR = os.path.join(REPO, "client/assets/characters/bodies/average_m") +# Segments covered by / adjacent to the torso garment — the weight-transfer source. +BODY_SEGS = ["seg_torso", "seg_hips", "seg_neck", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_leg_upper_l", "seg_leg_upper_r"] + +OUT_GLB = os.path.join(REPO, "spikes/synty-intake/out", + "SK_SCFI_CIVL_09_10TORS_HU01_quaternius.glb") +OUT_LOG = os.path.join(REPO, "spikes/synty-intake/out", "transplant_log.json") + +log = {} + + +def import_new(op, path, **kw): + before = set(bpy.data.objects) + op(filepath=path, **kw) + return list(set(bpy.data.objects) - before) + + +def find_armature(objs): + for o in objs: + if o.type == 'ARMATURE': + return o + return None + + +def bone_head_world(arm, name): + b = arm.data.bones.get(name) + return (arm.matrix_world @ b.head_local) if b else None + + +def deselect_all(): + bpy.ops.object.select_all(action='DESELECT') + + +def set_active(obj): + bpy.context.view_layer.objects.active = obj + + +# --- fresh scene --- +bpy.ops.object.select_all(action='SELECT') +bpy.ops.object.delete(use_global=False) + +# --- 1. import garment --- +g_objs = import_new(bpy.ops.import_scene.fbx, GARMENT_FBX) +g_arm = find_armature(g_objs) +garment = next(o for o in g_objs if o.type == 'MESH') +log["garment"] = {"verts": len(garment.data.vertices), + "sidekick_vgroups": [vg.name for vg in garment.vertex_groups]} + +# landmarks in Sidekick space (world) +sk_pelvis = bone_head_world(g_arm, "pelvis").copy() +sk_upperarm = bone_head_world(g_arm, "upperarm_l").copy() + +# --- 2. import our armature --- +o_objs = import_new(bpy.ops.import_scene.gltf, OUR_ARMATURE_GLB) +our_arm = find_armature(o_objs) +our_pelvis = bone_head_world(our_arm, "pelvis").copy() +our_upperarm = bone_head_world(our_arm, "upperarm_l").copy() + +# --- 3. detach garment from Sidekick armature, freeze world coords --- +deselect_all() +set_active(garment) +garment.select_set(True) +mw = garment.matrix_world.copy() +garment.parent = None +garment.matrix_world = mw +for mod in list(garment.modifiers): + garment.modifiers.remove(mod) +# drop Sidekick body-blend shape keys (not part of our body-type system) +if garment.data.shape_keys: + set_active(garment) + bpy.ops.object.shape_key_remove(all=True) +# bake object transform into mesh data so object space == world space +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + +# --- 4. affine alignment garment -> our proportions --- +sxy = our_upperarm.x / sk_upperarm.x +sz = (our_upperarm.z - our_pelvis.z) / (sk_upperarm.z - sk_pelvis.z) +S = Matrix.Diagonal(Vector((sxy, sxy, sz, 1.0))) +scaled_pelvis = Vector((sk_pelvis.x * sxy, sk_pelvis.y * sxy, sk_pelvis.z * sz)) +t = our_pelvis - scaled_pelvis +M = Matrix.Translation(t) @ S +garment.data.transform(M) +garment.data.update() + +log["alignment"] = { + "scale_xy": round(sxy, 5), "scale_z": round(sz, 5), + "translate": [round(v, 5) for v in t], + "sk_pelvis": [round(v, 5) for v in sk_pelvis], + "our_pelvis": [round(v, 5) for v in our_pelvis], + "sk_upperarm_l": [round(v, 5) for v in sk_upperarm], + "our_upperarm_l": [round(v, 5) for v in our_upperarm], +} + +# residual landmark error after affine, for all 12 bound bones +residuals = {} +for bn in ["pelvis", "thigh_l", "thigh_r", "spine_01", "spine_02", "spine_03", + "neck_01", "head", "clavicle_l", "upperarm_l", "clavicle_r", + "upperarm_r"]: + sk = bone_head_world(g_arm, bn) + ours = bone_head_world(our_arm, bn if bn != "head" else "Head") + sk_t = M @ sk + d = sk_t - ours + residuals[bn] = {"residual_m": round(d.length, 5), + "delta": [round(v, 5) for v in d]} +log["post_affine_bone_residuals"] = residuals + +# --- 5. delete Sidekick armature --- +deselect_all() +g_arm.select_set(True) +set_active(g_arm) +bpy.ops.object.delete(use_global=False) + +# --- 6. import + join our body segments as the weight-transfer source --- +seg_meshes = [] +for seg in BODY_SEGS: + objs = import_new(bpy.ops.import_scene.gltf, + os.path.join(BODY_DIR, seg + ".glb")) + picked = None + for o in objs: + if o.type == 'MESH' and len(o.vertex_groups) > 0: + picked = o + elif o.type == 'MESH': + # debris (bounds icosphere etc.) — delete + deselect_all() + o.select_set(True) + set_active(o) + bpy.ops.object.delete(use_global=False) + # delete the segment's own armature copy + arm = find_armature(objs) + if arm and picked is not None: + # unparent mesh first, keep transform + pmw = picked.matrix_world.copy() + picked.parent = None + picked.matrix_world = pmw + if arm: + deselect_all() + arm.select_set(True) + set_active(arm) + bpy.ops.object.delete(use_global=False) + if picked is None: + print(f"WARNING: no weighted mesh in {seg}") + continue + for mod in list(picked.modifiers): + picked.modifiers.remove(mod) + seg_meshes.append(picked) + +log["weight_source_segments"] = [ + {"name": m.name, "verts": len(m.data.vertices)} for m in seg_meshes] + +deselect_all() +for m in seg_meshes: + m.select_set(True) +set_active(seg_meshes[0]) +if len(seg_meshes) > 1: + bpy.ops.object.join() +body = bpy.context.view_layer.objects.active +body.name = "weight_source_body" +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + +# --- 7. weight transfer: our body -> garment --- +# wipe Sidekick vertex groups first so only OUR weights remain +for vg in list(garment.vertex_groups): + garment.vertex_groups.remove(vg) + +deselect_all() +set_active(garment) +garment.select_set(True) + +dt = garment.modifiers.new(name="WeightTransfer", type='DATA_TRANSFER') +dt.object = body +dt.use_vert_data = True +dt.data_types_verts = {'VGROUP_WEIGHTS'} +dt.vert_mapping = 'POLYINTERP_NEAREST' +dt.layers_vgroup_select_src = 'ALL' +dt.layers_vgroup_select_dst = 'NAME' +bpy.ops.object.datalayout_transfer(modifier=dt.name) +bpy.ops.object.modifier_apply(modifier=dt.name) + +# normalize +bpy.ops.object.mode_set(mode='WEIGHT_PAINT') +bpy.ops.object.vertex_group_normalize_all(lock_active=False) +bpy.ops.object.mode_set(mode='OBJECT') + +log["transferred_vgroups"] = [vg.name for vg in garment.vertex_groups] + +# sanity: count verts with (near-)zero total weight +zero = 0 +for v in garment.data.vertices: + tot = sum(g.weight for g in v.groups) + if tot < 1e-4: + zero += 1 +log["zero_weight_verts"] = zero + +# --- 8. bind garment to OUR armature --- +garment.parent = our_arm +garment.matrix_parent_inverse = our_arm.matrix_world.inverted() +am = garment.modifiers.new(name="Armature", type='ARMATURE') +am.object = our_arm + +# --- 9. delete the body source, export armature + garment --- +deselect_all() +body.select_set(True) +set_active(body) +bpy.ops.object.delete(use_global=False) + +os.makedirs(os.path.dirname(OUT_GLB), exist_ok=True) +deselect_all() +our_arm.select_set(True) +garment.select_set(True) +set_active(our_arm) +bpy.ops.export_scene.gltf( + filepath=OUT_GLB, + export_format='GLB', + use_selection=True, + export_apply=False, + export_animations=False, + export_skins=True, + export_morph=False, + export_extras=False, +) +log["export"] = {"path": OUT_GLB, "size_kb": os.path.getsize(OUT_GLB) // 1024} + +with open(OUT_LOG, "w") as f: + json.dump(log, f, indent=2) + +print("\n=== TRANSPLANT SUMMARY ===") +print(json.dumps(log, indent=2)) diff --git a/spikes/synty-intake/scripts/03_recon_bodies.py b/spikes/synty-intake/scripts/03_recon_bodies.py new file mode 100644 index 000000000..72ca71bb4 --- /dev/null +++ b/spikes/synty-intake/scripts/03_recon_bodies.py @@ -0,0 +1,116 @@ +""" +Spike T-1089 / Synty Sidekick intake — step 3 recon: the 11 body types. + +Questions this answers (feeds 04_batch_fit_bodies.py): + 1. Do the garment-adjacent segments (torso/hips/neck/upper arms/upper legs) + share topology (vert/poly counts) across all 11 body types? If yes, the + Surface-Deform refit can be driven by direct vertex correspondence + (bind SD on average_m, write target coords into the reference mesh). + 2. Do the per-segment embedded armatures differ across bodies (rest-pose + bake), or is it the shared skeleton everywhere? + +Run: + tooling/blender --background --python \ + spikes/synty-intake/scripts/03_recon_bodies.py +""" + +import bpy +import json +import os + +REPO = "/var/mnt/data/projects/settled-reach" +BODIES_DIR = os.path.join(REPO, "client/assets/characters/bodies") +OUT_JSON = os.path.join(REPO, "spikes/synty-intake/out/bodies_recon.json") + +BODY_TYPES = ["average_f", "average_m", "child", "heavy_f", "heavy_m", + "muscular_f", "muscular_m", "teen_f", "teen_m", + "thin_f", "thin_m"] + +SEGS = ["seg_torso", "seg_hips", "seg_neck", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_leg_upper_l", "seg_leg_upper_r"] + +LANDMARK_BONES = ["pelvis", "spine_01", "spine_02", "spine_03", + "clavicle_l", "upperarm_l", "thigh_l", "neck_01", "Head"] + + +def clear_scene(): + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + for block_list in (bpy.data.meshes, bpy.data.armatures, bpy.data.images, + bpy.data.materials): + for block in list(block_list): + if block.users == 0: + block_list.remove(block) + + +def import_new(path): + before = set(bpy.data.objects) + bpy.ops.import_scene.gltf(filepath=path) + return list(set(bpy.data.objects) - before) + + +report = {} + +for body in BODY_TYPES: + body_dir = os.path.join(BODIES_DIR, body) + entry = {"segments": {}, "armature": None} + for seg in SEGS: + path = os.path.join(body_dir, seg + ".glb") + if not os.path.exists(path): + entry["segments"][seg] = None + continue + clear_scene() + objs = import_new(path) + mesh = None + for o in objs: + if o.type == 'MESH' and len(o.vertex_groups) > 0: + mesh = o + arm = None + for o in objs: + if o.type == 'ARMATURE': + arm = o + seg_info = {} + if mesh is not None: + seg_info["verts"] = len(mesh.data.vertices) + seg_info["polys"] = len(mesh.data.polygons) + seg_info["vgroups"] = len(mesh.vertex_groups) + entry["segments"][seg] = seg_info + # record armature landmarks once per body (from seg_torso) + if seg == "seg_torso" and arm is not None: + lm = {} + for bn in LANDMARK_BONES: + b = arm.data.bones.get(bn) + if b is None: + b = arm.data.bones.get(bn.lower()) + if b is not None: + w = arm.matrix_world @ b.head_local + lm[bn] = [round(v, 5) for v in w] + entry["armature"] = { + "bones": len(arm.data.bones), + "landmarks": lm, + } + report[body] = entry + +# cross-body topology comparison vs average_m +ref = report["average_m"]["segments"] +topo = {} +for body in BODY_TYPES: + diffs = [] + for seg in SEGS: + a = ref.get(seg) + b = report[body]["segments"].get(seg) + if not a or not b: + diffs.append(f"{seg}: missing") + elif (a["verts"], a["polys"]) != (b["verts"], b["polys"]): + diffs.append(f"{seg}: {b['verts']}v/{b['polys']}p vs " + f"ref {a['verts']}v/{a['polys']}p") + topo[body] = diffs if diffs else "MATCH" +report["_topology_vs_average_m"] = topo + +os.makedirs(os.path.dirname(OUT_JSON), exist_ok=True) +with open(OUT_JSON, "w") as f: + json.dump(report, f, indent=2) + +print("\n=== BODIES RECON ===") +print(json.dumps(report, indent=2)) diff --git a/spikes/synty-intake/scripts/04_batch_fit_bodies.py b/spikes/synty-intake/scripts/04_batch_fit_bodies.py new file mode 100644 index 000000000..f5898a7cc --- /dev/null +++ b/spikes/synty-intake/scripts/04_batch_fit_bodies.py @@ -0,0 +1,420 @@ +""" +Spike T-1089 / Synty Sidekick intake — step 4: 11-body batch fit. + +Takes the transplanted torso garment (fitted to average_m, bound to our shared +armature = the average_m bind skeleton) through a multi-body refit, following +the fit_outfits_to_bodies.py Surface-Deform approach, adapted for the measured +reality that the 11 bodies do NOT share topology (03_recon_bodies.py: +every body is independently authored; thin_*/heavy_* even lack seg_hips). + +Per target body: + 1. Import the transplanted garment GLB; capture reference (average_m) + landmarks from its armature; detach + freeze the garment mesh. + 2. Import the average_m reference segments, join -> ref_body. + 3. Import the target body segments (skipping missing ones), keep the + seg_torso armature as the target bind skeleton, join -> tgt_body. + 4. Landmark affine (same machinery as 02): scale XY by shoulder ratio, + scale Z by pelvis->upperarm span ratio, translate pelvis->pelvis. + Applied to BOTH the garment and ref_body. + 5. BVH nearest-surface warp (replaces Surface Deform + Shrinkwrap — + first attempt showed SD driven by a shrinkwrapped driver mesh + spikes 14-28x on fold discontinuities, and SD bind itself is + unreliable against the multi-shell joined segment mesh): + for each garment vertex, find the nearest point on ref_body, + decompose the offset into (height along surface normal + + tangential residual), re-evaluate the same surface point on + tgt_body via its own nearest-surface lookup, and rebuild the + vertex at the target surface with the offset preserved. The + per-vertex displacement field is then Laplacian-smoothed over + the garment mesh connectivity to kill nearest-point-map + discontinuities while keeping rigid details (back device, + plating) coherent. + 6. Wipe vgroups, Data Transfer POLYINTERP_NEAREST weights from tgt_body, + normalize, bind to the target body's embedded armature (same bind pose + as the body segments -> garment deforms identically to the body under + any runtime skeleton pose). + 7. Export armature + garment GLB (export_skins=True) to + out/bodies/SK_SCFI_CIVL_09_10TORS_HU01_.glb + 8. Metrics (out/bodies_fit_log.json): zero-weight verts, AABB, + edge-length distortion vs post-affine baseline (collapse/fold + detector), signed-distance stats garment vs body surface + (penetration detector). + +Run: + tooling/blender --background --python \ + spikes/synty-intake/scripts/04_batch_fit_bodies.py +""" + +import bpy +import json +import math +import os +from mathutils import Matrix, Vector +from mathutils.bvhtree import BVHTree + +REPO = "/var/mnt/data/projects/settled-reach" +GARMENT_GLB = os.path.join(REPO, "spikes/synty-intake/out", + "SK_SCFI_CIVL_09_10TORS_HU01_quaternius.glb") +BODIES_DIR = os.path.join(REPO, "client/assets/characters/bodies") +OUT_DIR = os.path.join(REPO, "spikes/synty-intake/out/bodies") +OUT_LOG = os.path.join(REPO, "spikes/synty-intake/out/bodies_fit_log.json") + +BODY_TYPES = ["average_m", "average_f", "muscular_m", "muscular_f", + "thin_m", "thin_f", "heavy_m", "heavy_f", + "teen_m", "teen_f", "child"] + +SEGS = ["seg_torso", "seg_hips", "seg_neck", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_leg_upper_l", "seg_leg_upper_r"] + +PENETRATION_MM = 3.0 # garment vertex deeper than this inside the body -> flag + + +def deselect_all(): + bpy.ops.object.select_all(action='DESELECT') + + +def set_active(obj): + bpy.context.view_layer.objects.active = obj + + +def clear_scene(): + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + for block_list in (bpy.data.meshes, bpy.data.armatures, bpy.data.images, + bpy.data.materials): + for block in list(block_list): + if block.users == 0: + block_list.remove(block) + + +def import_new(op, path, **kw): + before = set(bpy.data.objects) + op(filepath=path, **kw) + return list(set(bpy.data.objects) - before) + + +def find_armature(objs): + for o in objs: + if o.type == 'ARMATURE': + return o + return None + + +def bone_head_world(arm, name): + b = arm.data.bones.get(name) + return (arm.matrix_world @ b.head_local).copy() if b else None + + +def delete_obj(obj): + deselect_all() + obj.select_set(True) + set_active(obj) + bpy.ops.object.delete(use_global=False) + + +def freeze(obj): + """Unparent keeping transform, strip modifiers, bake transform to data.""" + deselect_all() + set_active(obj) + obj.select_set(True) + mw = obj.matrix_world.copy() + obj.parent = None + obj.matrix_world = mw + for mod in list(obj.modifiers): + obj.modifiers.remove(mod) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + +def import_body(body, keep_armature): + """Import + join a body's garment-adjacent segments. + + Returns (joined_mesh, armature_or_None, segments_used). + """ + seg_meshes = [] + kept_arm = None + used = [] + for seg in SEGS: + path = os.path.join(BODIES_DIR, body, seg + ".glb") + if not os.path.exists(path): + continue + objs = import_new(bpy.ops.import_scene.gltf, path) + picked = None + for o in objs: + if o.type == 'MESH' and len(o.vertex_groups) > 0: + picked = o + elif o.type == 'MESH': + delete_obj(o) # debris (bounds helpers etc.) + arm = find_armature(objs) + if picked is not None: + pmw = picked.matrix_world.copy() + picked.parent = None + picked.matrix_world = pmw + if arm is not None: + if keep_armature and kept_arm is None: + kept_arm = arm + else: + delete_obj(arm) + if picked is None: + print(f" WARNING: no weighted mesh in {body}/{seg}") + continue + for mod in list(picked.modifiers): + picked.modifiers.remove(mod) + seg_meshes.append(picked) + used.append(seg) + deselect_all() + for m in seg_meshes: + m.select_set(True) + set_active(seg_meshes[0]) + if len(seg_meshes) > 1: + bpy.ops.object.join() + joined = bpy.context.view_layer.objects.active + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + return joined, kept_arm, used + + +def edge_lengths(mesh_obj): + verts = mesh_obj.data.vertices + return [(verts[e.vertices[0]].co - verts[e.vertices[1]].co).length + for e in mesh_obj.data.edges] + + +def aabb(mesh_obj): + xs = [v.co for v in mesh_obj.data.vertices] + lo = Vector((min(v.x for v in xs), min(v.y for v in xs), + min(v.z for v in xs))) + hi = Vector((max(v.x for v in xs), max(v.y for v in xs), + max(v.z for v in xs))) + return lo, hi + + +def warp_garment(garment, ref_body, tgt_body, depsgraph, + smooth_iterations=20, smooth_lambda=0.5): + """Nearest-surface warp: move each garment vertex from its offset + relative to ref_body onto the equivalent offset relative to tgt_body, + then Laplacian-smooth the displacement field over the garment mesh.""" + bvh_ref = BVHTree.FromObject(ref_body, depsgraph) + bvh_tgt = BVHTree.FromObject(tgt_body, depsgraph) + + verts = garment.data.vertices + n = len(verts) + disp = [None] * n + for i, v in enumerate(verts): + co_r, n_r, _idx, _d = bvh_ref.find_nearest(v.co) + if co_r is None: + disp[i] = Vector((0, 0, 0)) + continue + delta = v.co - co_r + h = delta.dot(n_r) + tang = delta - h * n_r + co_t, n_t, _idx2, _d2 = bvh_tgt.find_nearest(co_r) + if co_t is None: + disp[i] = Vector((0, 0, 0)) + continue + new_co = co_t + h * n_t + tang + disp[i] = new_co - v.co + + # adjacency from edges + adj = [[] for _ in range(n)] + for e in garment.data.edges: + a, b = e.vertices + adj[a].append(b) + adj[b].append(a) + + # Laplacian smoothing of the displacement field (not the geometry) + for _ in range(smooth_iterations): + new_disp = [None] * n + for i in range(n): + if not adj[i]: + new_disp[i] = disp[i] + continue + avg = Vector((0, 0, 0)) + for j in adj[i]: + avg += disp[j] + avg /= len(adj[i]) + new_disp[i] = disp[i].lerp(avg, smooth_lambda) + disp = new_disp + + max_disp = 0.0 + for i, v in enumerate(verts): + v.co = v.co + disp[i] + if disp[i].length > max_disp: + max_disp = disp[i].length + garment.data.update() + return {"max_displacement_mm": round(max_disp * 1000, 1), + "smooth_iterations": smooth_iterations} + + +def signed_distance_stats(garment, body_obj, depsgraph): + """Signed distance of each garment vertex to the body surface. + + Positive = outside the body (along surface normal), negative = inside. + """ + bvh = BVHTree.FromObject(body_obj, depsgraph) + dists = [] + for v in garment.data.vertices: + co, normal, _idx, _d = bvh.find_nearest(v.co) + if co is None: + continue + dists.append((v.co - co).dot(normal)) + dists.sort() + n = len(dists) + inside = [d for d in dists if d < -PENETRATION_MM / 1000.0] + return { + "verts_sampled": n, + "min_signed_mm": round(dists[0] * 1000, 2), + "p05_signed_mm": round(dists[max(0, int(n * 0.05) - 1)] * 1000, 2), + "median_signed_mm": round(dists[n // 2] * 1000, 2), + "max_signed_mm": round(dists[-1] * 1000, 2), + f"verts_inside_gt_{PENETRATION_MM:g}mm": len(inside), + "pct_inside": round(100.0 * len(inside) / n, 2), + } + + +log = {"garment_glb": GARMENT_GLB, "bodies": {}} +os.makedirs(OUT_DIR, exist_ok=True) + +for body in BODY_TYPES: + print(f"\n{'='*60}\n BODY: {body}\n{'='*60}") + entry = {} + clear_scene() + + # --- 1. garment + reference landmarks --- + g_objs = import_new(bpy.ops.import_scene.gltf, GARMENT_GLB) + ref_arm = find_armature(g_objs) + # the GLB also carries a bounds icosphere — the garment is the skinned + # mesh (has vertex groups); the set-diff order is nondeterministic, so + # pick explicitly and delete the rest + g_meshes = [o for o in g_objs if o.type == 'MESH'] + garment = max((m for m in g_meshes if len(m.vertex_groups) > 0), + key=lambda m: len(m.data.vertices)) + ref_pelvis = bone_head_world(ref_arm, "pelvis") + ref_upperarm = bone_head_world(ref_arm, "upperarm_l") + freeze(garment) + for m in g_meshes: + if m is not garment: + delete_obj(m) + delete_obj(ref_arm) + entry["garment_verts"] = len(garment.data.vertices) + + # --- 2. reference body (average_m) --- + ref_body, _, _ = import_body("average_m", keep_armature=False) + ref_body.name = "ref_body" + + # --- 3. target body --- + tgt_body, tgt_arm, used = import_body(body, keep_armature=True) + tgt_body.name = "tgt_body" + entry["segments_used"] = used + if tgt_arm is None: + entry["result"] = "FAIL: no target armature" + log["bodies"][body] = entry + continue + tgt_pelvis = bone_head_world(tgt_arm, "pelvis") + tgt_upperarm = bone_head_world(tgt_arm, "upperarm_l") + + # --- 4. landmark affine ref -> tgt --- + sxy = tgt_upperarm.x / ref_upperarm.x + sz = (tgt_upperarm.z - tgt_pelvis.z) / (ref_upperarm.z - ref_pelvis.z) + S = Matrix.Diagonal(Vector((sxy, sxy, sz, 1.0))) + scaled_pelvis = Vector((ref_pelvis.x * sxy, ref_pelvis.y * sxy, + ref_pelvis.z * sz)) + t = tgt_pelvis - scaled_pelvis + M = Matrix.Translation(t) @ S + garment.data.transform(M) + garment.data.update() + ref_body.data.transform(M) + ref_body.data.update() + entry["affine"] = {"scale_xy": round(sxy, 5), "scale_z": round(sz, 5), + "translate": [round(v, 5) for v in t]} + + # post-affine baseline for distortion metrics + base_edges = edge_lengths(garment) + lo, hi = aabb(garment) + entry["aabb_post_affine"] = {"min": [round(v, 4) for v in lo], + "max": [round(v, 4) for v in hi]} + + # --- 5. BVH nearest-surface warp ref_body -> tgt_body --- + depsgraph = bpy.context.evaluated_depsgraph_get() + entry["warp"] = warp_garment(garment, ref_body, tgt_body, depsgraph) + + delete_obj(ref_body) + + # --- 7. weights from the target body --- + for vg in list(garment.vertex_groups): + garment.vertex_groups.remove(vg) + deselect_all() + set_active(garment) + garment.select_set(True) + dt = garment.modifiers.new(name="WeightTransfer", type='DATA_TRANSFER') + dt.object = tgt_body + dt.use_vert_data = True + dt.data_types_verts = {'VGROUP_WEIGHTS'} + dt.vert_mapping = 'POLYINTERP_NEAREST' + dt.layers_vgroup_select_src = 'ALL' + dt.layers_vgroup_select_dst = 'NAME' + bpy.ops.object.datalayout_transfer(modifier=dt.name) + bpy.ops.object.modifier_apply(modifier=dt.name) + bpy.ops.object.mode_set(mode='WEIGHT_PAINT') + bpy.ops.object.vertex_group_normalize_all(lock_active=False) + bpy.ops.object.mode_set(mode='OBJECT') + + zero = sum(1 for v in garment.data.vertices + if sum(g.weight for g in v.groups) < 1e-4) + entry["zero_weight_verts"] = zero + entry["vgroups"] = len(garment.vertex_groups) + + # --- 8. metrics --- + post_edges = edge_lengths(garment) + ratios = [p / b for p, b in zip(post_edges, base_edges) if b > 1e-9] + entry["edge_distortion"] = { + "max_stretch": round(max(ratios), 3), + "max_shrink": round(min(ratios), 3), + "edges_gt_2x": sum(1 for r in ratios if r > 2.0), + "edges_lt_0.5x": sum(1 for r in ratios if r < 0.5), + "edges_total": len(ratios), + } + lo, hi = aabb(garment) + entry["aabb_post_fit"] = {"min": [round(v, 4) for v in lo], + "max": [round(v, 4) for v in hi]} + depsgraph = bpy.context.evaluated_depsgraph_get() + entry["signed_distance"] = signed_distance_stats(garment, tgt_body, + depsgraph) + + # --- 9. bind + export --- + garment.parent = tgt_arm + garment.matrix_parent_inverse = tgt_arm.matrix_world.inverted() + am = garment.modifiers.new(name="Armature", type='ARMATURE') + am.object = tgt_arm + delete_obj(tgt_body) + + out_glb = os.path.join(OUT_DIR, + f"SK_SCFI_CIVL_09_10TORS_HU01_{body}.glb") + deselect_all() + tgt_arm.select_set(True) + garment.select_set(True) + set_active(tgt_arm) + bpy.ops.export_scene.gltf( + filepath=out_glb, + export_format='GLB', + use_selection=True, + export_apply=False, + export_animations=False, + export_skins=True, + export_morph=False, + export_extras=False, + ) + entry["export"] = {"path": out_glb, + "size_kb": os.path.getsize(out_glb) // 1024} + entry["result"] = "OK" + log["bodies"][body] = entry + +with open(OUT_LOG, "w") as f: + json.dump(log, f, indent=2) + +print("\n=== BATCH FIT SUMMARY ===") +for body, e in log["bodies"].items(): + sd_stats = e.get("signed_distance", {}) + print(f" {body:12s} {e.get('result','?'):14s} " + f"zero_w={e.get('zero_weight_verts','-')} " + f"inside%={sd_stats.get('pct_inside','-')} " + f"stretch={e.get('edge_distortion',{}).get('max_stretch','-')}") diff --git a/spikes/synty-intake/scripts/extract_unitypackage.py b/spikes/synty-intake/scripts/extract_unitypackage.py new file mode 100644 index 000000000..8f7e5d6d0 --- /dev/null +++ b/spikes/synty-intake/scripts/extract_unitypackage.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Extract a .unitypackage (gzipped tar of GUID dirs) into real paths. + +Copies the raw `asset` bytes of every FBX/texture/metadata entry to +raw/. Prints a summary by extension. +""" +import sys +import tarfile +import shutil +from pathlib import Path +from collections import Counter + +KEEP = {".fbx", ".png", ".tga", ".json", ".txt", ".sk", ".asset"} + + +def main(pkg: str, out_root: str) -> None: + out = Path(out_root) + out.mkdir(parents=True, exist_ok=True) + counts: Counter = Counter() + with tarfile.open(pkg, "r:gz") as tar: + members = {m.name: m for m in tar.getmembers()} + guids = sorted({name.split("/")[0] for name in members if "/" in name}) + for guid in guids: + pn = members.get(f"{guid}/pathname") + asset = members.get(f"{guid}/asset") + if pn is None or asset is None: + continue + pathname = tar.extractfile(pn).read().decode("utf-8").splitlines()[0].strip() + ext = Path(pathname).suffix.lower() + if ext not in KEEP: + counts[f"(skipped {ext or 'dir'})"] += 1 + continue + dest = out / pathname + dest.parent.mkdir(parents=True, exist_ok=True) + with tar.extractfile(asset) as src, open(dest, "wb") as dst: + shutil.copyfileobj(src, dst) + counts[ext] += 1 + for ext, n in sorted(counts.items()): + print(f"{ext}\t{n}") + + +if __name__ == "__main__": + main(sys.argv[1], sys.argv[2]) diff --git a/spikes/synty-intake/scripts/qa_bodies.gd b/spikes/synty-intake/scripts/qa_bodies.gd new file mode 100644 index 000000000..8e05f1b17 --- /dev/null +++ b/spikes/synty-intake/scripts/qa_bodies.gd @@ -0,0 +1,193 @@ +extends Node3D +## Spike T-1089 Synty intake — 11-body batch QA scene (THROWAWAY, lives in +## client/spike_synty_qa/ only for the res:// path; source of truth is +## spikes/synty-intake/scripts/qa_bodies.gd). +## +## For each body type on disk: builds a CharacterVisual, reparents that +## body's refitted Synty torso garment onto its skeleton (mirrors +## character_visual.gd:517-535), screenshots idle front, then plays Walk +## and screenshots two stride phases (front + side). Peasant pants/shoes +## are added only for bodies that have them on disk. Saves to +## spikes/synty-intake/out/qa_bodies/, then quits. + +const BODIES_GLB_DIR := "res://spike_synty_qa/bodies/" +const SHOT_DIR := "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/qa_bodies/" +const CLOTHING_DIR := "res://assets/characters/clothing/" + +const BODY_KEYS: Array[String] = [ + "average_m", "average_f", "muscular_m", "muscular_f", + "thin_m", "thin_f", "heavy_m", "heavy_f", + "teen_m", "teen_f", "child", +] + +const KEY_TO_ENUM := { + "average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M, + "average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F, + "muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M, + "muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F, + "thin_m": CharacterVisualDescriptor.BodyType.THIN_M, + "thin_f": CharacterVisualDescriptor.BodyType.THIN_F, + "heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M, + "heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, + "child": CharacterVisualDescriptor.BodyType.CHILD, +} + +var _cam: Camera3D = null + + +func _ready() -> void: + _setup_stage() + _run_qa.call_deferred() + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-45, 30, 0) + light.light_energy = 1.2 + add_child(light) + + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.8, 0.8, 0.85) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.18, 0.19, 0.22) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + + _cam = Camera3D.new() + add_child(_cam) + + +func _cam_front(h: float) -> void: + _cam.position = Vector3(0, h * 0.72, h * 0.85) + _cam.look_at_from_position(_cam.position, Vector3(0, h * 0.62, 0), Vector3.UP) + + +func _cam_side(h: float) -> void: + _cam.position = Vector3(h * 0.85, h * 0.72, 0) + _cam.look_at_from_position(_cam.position, Vector3(0, h * 0.62, 0), Vector3.UP) + + +func _attach_garment(skel: Skeleton3D, glb_path: String) -> Array[MeshInstance3D]: + var out: Array[MeshInstance3D] = [] + var scene := load(glb_path) as PackedScene + if scene == null: + push_error("QA: garment GLB failed to load: " + glb_path) + return out + var inst := scene.instantiate() + var stack: Array[Node] = [inst] + while not stack.is_empty(): + var n: Node = stack.pop_back() + for c in n.get_children(): + stack.append(c) + if n is MeshInstance3D and (n as MeshInstance3D).skin != null: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + out.append(mi) + inst.queue_free() + return out + + +func _shot(fname: String) -> void: + await RenderingServer.frame_post_draw + var img := get_viewport().get_texture().get_image() + img.save_png(SHOT_DIR + fname) + print("QA: saved ", fname) + + +func _wait_frames(n: int) -> void: + for i in n: + await get_tree().process_frame + + +func _run_qa() -> void: + DirAccess.make_dir_recursive_absolute(SHOT_DIR) + for body_key: String in BODY_KEYS: + var glb := BODIES_GLB_DIR + "SK_SCFI_CIVL_09_10TORS_HU01_%s.glb" % body_key + if not ResourceLoader.exists(glb): + print("QA: MISSING garment GLB for ", body_key, " — skipped") + continue + + var cv := CharacterVisual.new() + add_child(cv) + + var desc := CharacterVisualDescriptor.new() + desc.body_type = KEY_TO_ENUM[body_key] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + var slots := {} + if ResourceLoader.exists(CLOTHING_DIR + "peasant_pants/%s.glb" % body_key): + slots["legs"] = "peasant_pants" + if ResourceLoader.exists(CLOTHING_DIR + "peasant_shoes/%s.glb" % body_key): + slots["feet"] = "peasant_shoes" + desc.clothing_slots = slots + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("QA: no skeleton for " + body_key) + cv.queue_free() + continue + print("QA[", body_key, "]: skeleton bones = ", skel.get_bone_count(), + " pants = ", slots.has("legs")) + + # rough height for camera framing (child/teen are shorter) + var h0 := 1.75 + if body_key == "child" or body_key.begins_with("teen"): + h0 = 1.45 + + # body-only control shot BEFORE the garment — separates body-asset + # rendering defects from garment fit defects + await _wait_frames(5) + _cam_front(h0) + await _shot("%s_0_body_only.png" % body_key) + + var meshes := _attach_garment(skel, glb) + for mi in meshes: + print("QA[", body_key, "]: garment '", mi.name, "' skin binds = ", + mi.skin.get_bind_count(), " aabb = ", mi.get_aabb()) + + var h := h0 + + await _wait_frames(5) + _cam_front(h) + await _shot("%s_1_idle_front.png" % body_key) + + var ap := cv.get_animation_player() + var walk := "" + if ap: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + var full := str(lib_name) + "/" + str(a) + if full.to_lower().contains("walk"): + walk = str(a) + if full.to_lower().ends_with("/walk"): + break + if walk != "": + cv.play_animation(walk) + await _wait_frames(24) + _cam_front(h) + await _shot("%s_2_walk_front.png" % body_key) + _cam_side(h) + await _wait_frames(8) + await _shot("%s_3_walk_side.png" % body_key) + else: + push_warning("QA: no walk animation for " + body_key) + + for mi in meshes: + print("QA[", body_key, "]: post-walk garment aabb = ", mi.get_aabb()) + + cv.queue_free() + await _wait_frames(2) + + print("QA: DONE") + get_tree().quit(0) diff --git a/spikes/synty-intake/scripts/qa_bodies.tscn b/spikes/synty-intake/scripts/qa_bodies.tscn new file mode 100644 index 000000000..95df286fc --- /dev/null +++ b/spikes/synty-intake/scripts/qa_bodies.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://synty_qa_bodies1"] + +[ext_resource type="Script" path="res://spike_synty_qa/qa_bodies.gd" id="1"] + +[node name="QABodies" type="Node3D"] +script = ExtResource("1") diff --git a/spikes/synty-intake/scripts/qa_scene.gd b/spikes/synty-intake/scripts/qa_scene.gd new file mode 100644 index 000000000..2e48c0bfa --- /dev/null +++ b/spikes/synty-intake/scripts/qa_scene.gd @@ -0,0 +1,167 @@ +extends Node3D +## Spike T-1089 Synty intake — Godot QA scene (THROWAWAY, lives in +## client/spike_synty_qa/ only for the res:// path; source of truth is +## spikes/synty-intake/scripts/qa_scene.gd). +## +## Builds a CharacterVisual (average_m + peasant pants/shoes), reparents the +## transplanted Synty torso garment onto its skeleton (mirrors +## character_visual.gd:517-535), plays Walk, saves screenshots to +## spikes/synty-intake/out/qa/, then quits. + +const GARMENT_GLB := "res://spike_synty_qa/SK_SCFI_CIVL_09_10TORS_HU01_quaternius.glb" +const SHOT_DIR := "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/qa/" + +var _cv: CharacterVisual = null +var _cam: Camera3D = null +var _garment_meshes: Array[MeshInstance3D] = [] + + +func _ready() -> void: + _setup_stage() + _cv = CharacterVisual.new() + add_child(_cv) + + var desc := CharacterVisualDescriptor.new() + desc.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_M + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + desc.clothing_slots = {"legs": "peasant_pants", "feet": "peasant_shoes"} + _cv.load_descriptor(desc) + + var skel := _cv.get_skeleton() + print("QA: skeleton bones = ", skel.get_bone_count() if skel else -1) + + _attach_garment(skel) + _run_qa.call_deferred() + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-45, 30, 0) + light.light_energy = 1.2 + add_child(light) + + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.8, 0.8, 0.85) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.18, 0.19, 0.22) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + + _cam = Camera3D.new() + add_child(_cam) + _set_cam_front() + + +func _set_cam_front() -> void: + _cam.position = Vector3(0, 1.25, 1.4) + _cam.look_at_from_position(_cam.position, Vector3(0, 1.15, 0), Vector3.UP) + + +func _set_cam_side() -> void: + _cam.position = Vector3(1.4, 1.25, 0) + _cam.look_at_from_position(_cam.position, Vector3(0, 1.15, 0), Vector3.UP) + + +func _set_cam_back() -> void: + _cam.position = Vector3(0, 1.25, -1.4) + _cam.look_at_from_position(_cam.position, Vector3(0, 1.15, 0), Vector3.UP) + + +func _attach_garment(skel: Skeleton3D) -> void: + # Mirror character_visual.gd:517-535 — reparent skinned meshes onto OUR + # skeleton; matching bone names in the Skin resource drive them. + var scene := load(GARMENT_GLB) as PackedScene + if scene == null: + push_error("QA: garment GLB failed to load: " + GARMENT_GLB) + return + var inst := scene.instantiate() + var stack: Array[Node] = [inst] + while not stack.is_empty(): + var n: Node = stack.pop_back() + for c in n.get_children(): + stack.append(c) + if n is MeshInstance3D and (n as MeshInstance3D).skin != null: + var mi := n as MeshInstance3D + mi.get_parent().remove_child(mi) + mi.owner = null + skel.add_child(mi) + _garment_meshes.append(mi) + inst.queue_free() + print("QA: garment skinned meshes attached = ", _garment_meshes.size()) + for mi in _garment_meshes: + print("QA: garment mesh '", mi.name, "' skin binds = ", mi.skin.get_bind_count()) + + +func _shot(fname: String) -> void: + await RenderingServer.frame_post_draw + var img := get_viewport().get_texture().get_image() + img.save_png(SHOT_DIR + fname) + print("QA: saved ", fname) + + +func _wait_frames(n: int) -> void: + for i in n: + await get_tree().process_frame + + +func _run_qa() -> void: + await _wait_frames(5) + + # report garment AABB sanity (explosion check) + for mi in _garment_meshes: + print("QA: garment local AABB = ", mi.get_aabb()) + + # --- idle --- + await _shot("01_idle_front.png") + _set_cam_side() + await _shot("02_idle_side.png") + _set_cam_back() + await _shot("02b_idle_back.png") + + # --- walk --- + var ap := _cv.get_animation_player() + var names: Array[String] = [] + if ap: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + names.append(str(lib_name) + "/" + str(a)) + print("QA: animations = ", names) + + var walk := "" + for n in names: + if n.to_lower().contains("walk"): + walk = n.get_slice("/", 1) + if n.to_lower().ends_with("/walk"): + break + if walk == "": + push_warning("QA: no walk animation found — staying on idle") + else: + print("QA: playing '", walk, "'") + _cv.play_animation(walk) + + # mid-stride frames at two phases, front + side + await _wait_frames(24) + _set_cam_front() + await _shot("03_walk_front_a.png") + _set_cam_side() + await _shot("04_walk_side_a.png") + await _wait_frames(14) + await _shot("05_walk_side_b.png") + _set_cam_front() + await _shot("06_walk_front_b.png") + _set_cam_back() + await _wait_frames(10) + await _shot("07_walk_back.png") + + for mi in _garment_meshes: + print("QA: post-walk garment local AABB = ", mi.get_aabb()) + + print("QA: DONE") + get_tree().quit(0) diff --git a/spikes/synty-intake/scripts/qa_scene.tscn b/spikes/synty-intake/scripts/qa_scene.tscn new file mode 100644 index 000000000..ddde8e6a8 --- /dev/null +++ b/spikes/synty-intake/scripts/qa_scene.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://spike_synty_qa1"] + +[ext_resource type="Script" path="res://spike_synty_qa/qa_scene.gd" id="1"] + +[node name="QARoot" type="Node3D"] +script = ExtResource("1") diff --git a/spikes/synty-intake/scripts/recon_body_aabb.py b/spikes/synty-intake/scripts/recon_body_aabb.py new file mode 100644 index 000000000..9c837bbcd --- /dev/null +++ b/spikes/synty-intake/scripts/recon_body_aabb.py @@ -0,0 +1,37 @@ +"""Quick recon: joined garment-adjacent body mesh AABB per body type.""" +import bpy +import os + +REPO = "/var/mnt/data/projects/settled-reach" +BODIES_DIR = os.path.join(REPO, "client/assets/characters/bodies") +BODY_TYPES = ["average_m", "average_f", "muscular_m", "muscular_f", + "thin_m", "thin_f", "heavy_m", "heavy_f", + "teen_m", "teen_f", "child"] +SEGS = ["seg_torso", "seg_hips", "seg_neck", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_leg_upper_l", "seg_leg_upper_r"] + +for body in BODY_TYPES: + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + xs, ys, zs = [], [], [] + t_xs, t_ys = [], [] + for seg in SEGS: + path = os.path.join(BODIES_DIR, body, seg + ".glb") + if not os.path.exists(path): + continue + before = set(bpy.data.objects) + bpy.ops.import_scene.gltf(filepath=path) + for o in set(bpy.data.objects) - before: + if o.type == 'MESH' and len(o.vertex_groups) > 0: + for v in o.data.vertices: + w = o.matrix_world @ v.co + xs.append(w.x) + ys.append(w.y) + zs.append(w.z) + if seg == "seg_torso": + t_xs.append(w.x) + t_ys.append(w.y) + print(f"AABB {body}: full x={max(xs)-min(xs):.3f} " + f"y={max(ys)-min(ys):.3f} z_top={max(zs):.3f} " + f"| torso x={max(t_xs)-min(t_xs):.3f} depth y={max(t_ys)-min(t_ys):.3f}") diff --git a/spikes/synty-intake/scripts/recon_clothing_bind.py b/spikes/synty-intake/scripts/recon_clothing_bind.py new file mode 100644 index 000000000..e6d1214e3 --- /dev/null +++ b/spikes/synty-intake/scripts/recon_clothing_bind.py @@ -0,0 +1,23 @@ +"""Quick recon: which bind skeleton do production per-body clothing GLBs embed?""" +import bpy +import os + +REPO = "/var/mnt/data/projects/settled-reach" +FILES = [ + "client/assets/characters/clothing/peasant_pants/teen_m.glb", + "client/assets/characters/clothing/peasant_pants/average_f.glb", + "client/assets/characters/clothing/peasant_pants/average_m.glb", +] + +for rel in FILES: + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + bpy.ops.import_scene.gltf(filepath=os.path.join(REPO, rel)) + for o in bpy.data.objects: + if o.type == 'ARMATURE': + for bn in ("pelvis", "upperarm_l"): + b = o.data.bones.get(bn) + if b: + w = o.matrix_world @ b.head_local + print(f"RECON {rel} {bn} " + f"({w.x:.5f}, {w.y:.5f}, {w.z:.5f})") diff --git a/spikes/synty-intake/scripts/recon_dump.py b/spikes/synty-intake/scripts/recon_dump.py new file mode 100644 index 000000000..24728548a --- /dev/null +++ b/spikes/synty-intake/scripts/recon_dump.py @@ -0,0 +1,42 @@ +"""Blender recon: dump skeleton/mesh facts for a Sidekick FBX and our armature. + +Usage: tooling/blender --background --python recon_dump.py -- +""" +import sys + +import bpy + +argv = sys.argv[sys.argv.index("--") + 1:] +fbx_path, glb_path = argv[0], argv[1] + +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.fbx(filepath=fbx_path) +print("=== SIDEKICK FBX:", fbx_path.split("/")[-1]) +sk_bones = [] +for obj in bpy.data.objects: + if obj.type == "ARMATURE": + sk_bones = sorted(b.name for b in obj.data.bones) + print("ARMATURE:", obj.name, "bones:", len(sk_bones)) + elif obj.type == "MESH": + keys = obj.data.shape_keys + print( + "MESH:", obj.name, + "verts:", len(obj.data.vertices), + "dim:", [round(d, 3) for d in obj.dimensions], + "shapekeys:", [k.name for k in keys.key_blocks] if keys else [], + "vgroups:", len(obj.vertex_groups), + ) +print("SK_BONES:", ",".join(sk_bones)) + +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.gltf(filepath=glb_path) +print("=== OURS:", glb_path.split("/")[-1]) +our_bones = [] +for obj in bpy.data.objects: + if obj.type == "ARMATURE": + our_bones = sorted(b.name for b in obj.data.bones) + print("ARMATURE:", obj.name, "bones:", len(our_bones)) +print("OUR_BONES:", ",".join(our_bones)) + +overlap = set(sk_bones) & set(our_bones) +print("OVERLAP:", len(overlap), "of", len(sk_bones), "sidekick /", len(our_bones), "ours") diff --git a/spikes/synty-intake/scripts/vgroup_dump.py b/spikes/synty-intake/scripts/vgroup_dump.py new file mode 100644 index 000000000..432fcf69a --- /dev/null +++ b/spikes/synty-intake/scripts/vgroup_dump.py @@ -0,0 +1,11 @@ +"""Dump vertex-group names of every mesh in an FBX.""" +import sys + +import bpy + +fbx_path = sys.argv[sys.argv.index("--") + 1] +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.fbx(filepath=fbx_path) +for obj in bpy.data.objects: + if obj.type == "MESH": + print("MESH:", obj.name, "VGROUPS:", ",".join(g.name for g in obj.vertex_groups))