diff --git a/client/assets/characters/clothing/buttondown_modern/average_f.glb b/client/assets/characters/clothing/buttondown_modern/average_f.glb new file mode 100644 index 000000000..982c216e8 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/average_f_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/average_f_base_albedo.png new file mode 100644 index 000000000..0348b1f02 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/average_f_mask.png b/client/assets/characters/clothing/buttondown_modern/average_f_mask.png new file mode 100644 index 000000000..62dbafa02 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/average_m.glb b/client/assets/characters/clothing/buttondown_modern/average_m.glb new file mode 100644 index 000000000..7d814a545 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/average_m_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/average_m_base_albedo.png new file mode 100644 index 000000000..d13f4c50e Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/average_m_mask.png b/client/assets/characters/clothing/buttondown_modern/average_m_mask.png new file mode 100644 index 000000000..3c8486ff2 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/base_albedo.png b/client/assets/characters/clothing/buttondown_modern/base_albedo.png new file mode 100644 index 000000000..d13f4c50e Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/child.glb b/client/assets/characters/clothing/buttondown_modern/child.glb new file mode 100644 index 000000000..8d1a40f86 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/child.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/child_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/child_base_albedo.png new file mode 100644 index 000000000..13700bb97 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/child_mask.png b/client/assets/characters/clothing/buttondown_modern/child_mask.png new file mode 100644 index 000000000..7d37bdaee Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/coverage.json b/client/assets/characters/clothing/buttondown_modern/coverage.json new file mode 100644 index 000000000..d72d0c9ac --- /dev/null +++ b/client/assets/characters/clothing/buttondown_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "torso_upper", "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/buttondown_modern/heavy_f.glb b/client/assets/characters/clothing/buttondown_modern/heavy_f.glb new file mode 100644 index 000000000..f6d35491c Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..133f0fe26 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/heavy_f_mask.png b/client/assets/characters/clothing/buttondown_modern/heavy_f_mask.png new file mode 100644 index 000000000..4f7d910ea Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/heavy_m.glb b/client/assets/characters/clothing/buttondown_modern/heavy_m.glb new file mode 100644 index 000000000..0bb032fc4 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..ffa271275 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/heavy_m_mask.png b/client/assets/characters/clothing/buttondown_modern/heavy_m_mask.png new file mode 100644 index 000000000..48bb96b5d Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/muscular_f.glb b/client/assets/characters/clothing/buttondown_modern/muscular_f.glb new file mode 100644 index 000000000..e305f547a Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..05e5ab37a Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/muscular_f_mask.png b/client/assets/characters/clothing/buttondown_modern/muscular_f_mask.png new file mode 100644 index 000000000..6c21992aa Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/muscular_m.glb b/client/assets/characters/clothing/buttondown_modern/muscular_m.glb new file mode 100644 index 000000000..0b0d99490 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..36d29ccdf Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/muscular_m_mask.png b/client/assets/characters/clothing/buttondown_modern/muscular_m_mask.png new file mode 100644 index 000000000..b9c365e07 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/reference_mask.png b/client/assets/characters/clothing/buttondown_modern/reference_mask.png new file mode 100644 index 000000000..3c8486ff2 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/teen_f.glb b/client/assets/characters/clothing/buttondown_modern/teen_f.glb new file mode 100644 index 000000000..92ef3be11 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..864fe4514 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/teen_f_mask.png b/client/assets/characters/clothing/buttondown_modern/teen_f_mask.png new file mode 100644 index 000000000..6dbd75c2b Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/teen_m.glb b/client/assets/characters/clothing/buttondown_modern/teen_m.glb new file mode 100644 index 000000000..c63af8729 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..170cb92a5 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/teen_m_mask.png b/client/assets/characters/clothing/buttondown_modern/teen_m_mask.png new file mode 100644 index 000000000..f6142f7af Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/thin_f.glb b/client/assets/characters/clothing/buttondown_modern/thin_f.glb new file mode 100644 index 000000000..53b16473f Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..1e94c9938 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/thin_f_mask.png b/client/assets/characters/clothing/buttondown_modern/thin_f_mask.png new file mode 100644 index 000000000..6269f2c82 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/thin_m.glb b/client/assets/characters/clothing/buttondown_modern/thin_m.glb new file mode 100644 index 000000000..90a113dc1 Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/buttondown_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/buttondown_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..c28b5342b Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/buttondown_modern/thin_m_mask.png b/client/assets/characters/clothing/buttondown_modern/thin_m_mask.png new file mode 100644 index 000000000..9e479ec3c Binary files /dev/null and b/client/assets/characters/clothing/buttondown_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/average_f.glb b/client/assets/characters/clothing/hoodie_modern/average_f.glb new file mode 100644 index 000000000..41dab2141 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/average_f_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/average_f_base_albedo.png new file mode 100644 index 000000000..618beeb51 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/average_f_mask.png b/client/assets/characters/clothing/hoodie_modern/average_f_mask.png new file mode 100644 index 000000000..e9332d0a8 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/average_m.glb b/client/assets/characters/clothing/hoodie_modern/average_m.glb new file mode 100644 index 000000000..b0224c37b Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/average_m_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/average_m_base_albedo.png new file mode 100644 index 000000000..6d0bf70aa Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/average_m_mask.png b/client/assets/characters/clothing/hoodie_modern/average_m_mask.png new file mode 100644 index 000000000..b0c3bf6ef Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/base_albedo.png b/client/assets/characters/clothing/hoodie_modern/base_albedo.png new file mode 100644 index 000000000..6d0bf70aa Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/child.glb b/client/assets/characters/clothing/hoodie_modern/child.glb new file mode 100644 index 000000000..92cb80a3a Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/child.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/child_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/child_base_albedo.png new file mode 100644 index 000000000..26d59c03b Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/child_mask.png b/client/assets/characters/clothing/hoodie_modern/child_mask.png new file mode 100644 index 000000000..92a28b1f4 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/coverage.json b/client/assets/characters/clothing/hoodie_modern/coverage.json new file mode 100644 index 000000000..9c2d866d2 --- /dev/null +++ b/client/assets/characters/clothing/hoodie_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/hoodie_modern/heavy_f.glb b/client/assets/characters/clothing/hoodie_modern/heavy_f.glb new file mode 100644 index 000000000..eb538c273 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..03b7e5f6b Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/heavy_f_mask.png b/client/assets/characters/clothing/hoodie_modern/heavy_f_mask.png new file mode 100644 index 000000000..a370a4a97 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/heavy_m.glb b/client/assets/characters/clothing/hoodie_modern/heavy_m.glb new file mode 100644 index 000000000..bf775fb15 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..e776b2516 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/heavy_m_mask.png b/client/assets/characters/clothing/hoodie_modern/heavy_m_mask.png new file mode 100644 index 000000000..d1da889cc Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/muscular_f.glb b/client/assets/characters/clothing/hoodie_modern/muscular_f.glb new file mode 100644 index 000000000..efc36d77e Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..71cd037be Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/muscular_f_mask.png b/client/assets/characters/clothing/hoodie_modern/muscular_f_mask.png new file mode 100644 index 000000000..1c88bc0fe Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/muscular_m.glb b/client/assets/characters/clothing/hoodie_modern/muscular_m.glb new file mode 100644 index 000000000..8fa6ec45f Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..11350c39e Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/muscular_m_mask.png b/client/assets/characters/clothing/hoodie_modern/muscular_m_mask.png new file mode 100644 index 000000000..b298dd17c Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/reference_mask.png b/client/assets/characters/clothing/hoodie_modern/reference_mask.png new file mode 100644 index 000000000..b0c3bf6ef Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/teen_f.glb b/client/assets/characters/clothing/hoodie_modern/teen_f.glb new file mode 100644 index 000000000..603136c27 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..a54c8e06f Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/teen_f_mask.png b/client/assets/characters/clothing/hoodie_modern/teen_f_mask.png new file mode 100644 index 000000000..9793ca1f5 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/teen_m.glb b/client/assets/characters/clothing/hoodie_modern/teen_m.glb new file mode 100644 index 000000000..23bc44f83 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..e19f7b4d9 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/teen_m_mask.png b/client/assets/characters/clothing/hoodie_modern/teen_m_mask.png new file mode 100644 index 000000000..ef00693f1 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/thin_f.glb b/client/assets/characters/clothing/hoodie_modern/thin_f.glb new file mode 100644 index 000000000..7f7e98e8d Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..0334c0ca6 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/thin_f_mask.png b/client/assets/characters/clothing/hoodie_modern/thin_f_mask.png new file mode 100644 index 000000000..50a693756 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/thin_m.glb b/client/assets/characters/clothing/hoodie_modern/thin_m.glb new file mode 100644 index 000000000..4abc34103 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/hoodie_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/hoodie_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..3ecdfae89 Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/hoodie_modern/thin_m_mask.png b/client/assets/characters/clothing/hoodie_modern/thin_m_mask.png new file mode 100644 index 000000000..8293f6e0d Binary files /dev/null and b/client/assets/characters/clothing/hoodie_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/average_f.glb b/client/assets/characters/clothing/jacket_modern/average_f.glb new file mode 100644 index 000000000..4cbb40416 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/average_f_base_albedo.png b/client/assets/characters/clothing/jacket_modern/average_f_base_albedo.png new file mode 100644 index 000000000..7dab99feb Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/average_f_mask.png b/client/assets/characters/clothing/jacket_modern/average_f_mask.png new file mode 100644 index 000000000..56d6ea303 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/average_m.glb b/client/assets/characters/clothing/jacket_modern/average_m.glb new file mode 100644 index 000000000..69845e862 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/average_m_base_albedo.png b/client/assets/characters/clothing/jacket_modern/average_m_base_albedo.png new file mode 100644 index 000000000..b7a872f75 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/average_m_mask.png b/client/assets/characters/clothing/jacket_modern/average_m_mask.png new file mode 100644 index 000000000..e980b773c Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/base_albedo.png b/client/assets/characters/clothing/jacket_modern/base_albedo.png new file mode 100644 index 000000000..b7a872f75 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/child.glb b/client/assets/characters/clothing/jacket_modern/child.glb new file mode 100644 index 000000000..4745e46f2 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/child.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/child_base_albedo.png b/client/assets/characters/clothing/jacket_modern/child_base_albedo.png new file mode 100644 index 000000000..c30b324ff Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/child_mask.png b/client/assets/characters/clothing/jacket_modern/child_mask.png new file mode 100644 index 000000000..15f568bd6 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/coverage.json b/client/assets/characters/clothing/jacket_modern/coverage.json new file mode 100644 index 000000000..9c2d866d2 --- /dev/null +++ b/client/assets/characters/clothing/jacket_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/jacket_modern/heavy_f.glb b/client/assets/characters/clothing/jacket_modern/heavy_f.glb new file mode 100644 index 000000000..68efb2486 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/jacket_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..ea3d33e0c Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/heavy_f_mask.png b/client/assets/characters/clothing/jacket_modern/heavy_f_mask.png new file mode 100644 index 000000000..2993abc4b Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/heavy_m.glb b/client/assets/characters/clothing/jacket_modern/heavy_m.glb new file mode 100644 index 000000000..4f067991b Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/jacket_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..28b597ef1 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/heavy_m_mask.png b/client/assets/characters/clothing/jacket_modern/heavy_m_mask.png new file mode 100644 index 000000000..230ce20f8 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/muscular_f.glb b/client/assets/characters/clothing/jacket_modern/muscular_f.glb new file mode 100644 index 000000000..9efb90c5a Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/jacket_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..fb44ee78f Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/muscular_f_mask.png b/client/assets/characters/clothing/jacket_modern/muscular_f_mask.png new file mode 100644 index 000000000..d1af3a256 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/muscular_m.glb b/client/assets/characters/clothing/jacket_modern/muscular_m.glb new file mode 100644 index 000000000..74ef42d25 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/jacket_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..e7d389035 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/muscular_m_mask.png b/client/assets/characters/clothing/jacket_modern/muscular_m_mask.png new file mode 100644 index 000000000..2a0a9bdab Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/reference_mask.png b/client/assets/characters/clothing/jacket_modern/reference_mask.png new file mode 100644 index 000000000..e980b773c Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/teen_f.glb b/client/assets/characters/clothing/jacket_modern/teen_f.glb new file mode 100644 index 000000000..04c38c82f Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/jacket_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..9a27fd7b9 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/teen_f_mask.png b/client/assets/characters/clothing/jacket_modern/teen_f_mask.png new file mode 100644 index 000000000..4f3ffbdae Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/teen_m.glb b/client/assets/characters/clothing/jacket_modern/teen_m.glb new file mode 100644 index 000000000..9be266703 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/jacket_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..2ad9179ed Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/teen_m_mask.png b/client/assets/characters/clothing/jacket_modern/teen_m_mask.png new file mode 100644 index 000000000..2fbcf17e9 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/thin_f.glb b/client/assets/characters/clothing/jacket_modern/thin_f.glb new file mode 100644 index 000000000..c1366b5d3 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/jacket_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..a9df15393 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/thin_f_mask.png b/client/assets/characters/clothing/jacket_modern/thin_f_mask.png new file mode 100644 index 000000000..f58ea6513 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/jacket_modern/thin_m.glb b/client/assets/characters/clothing/jacket_modern/thin_m.glb new file mode 100644 index 000000000..37a60eb83 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/jacket_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/jacket_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..0227038f2 Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jacket_modern/thin_m_mask.png b/client/assets/characters/clothing/jacket_modern/thin_m_mask.png new file mode 100644 index 000000000..d5e447e5a Binary files /dev/null and b/client/assets/characters/clothing/jacket_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/average_f.glb b/client/assets/characters/clothing/jeans_modern/average_f.glb new file mode 100644 index 000000000..edc2c2712 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/average_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/average_f_base_albedo.png new file mode 100644 index 000000000..5dd109790 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/average_f_mask.png b/client/assets/characters/clothing/jeans_modern/average_f_mask.png new file mode 100644 index 000000000..822557a63 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/average_m.glb b/client/assets/characters/clothing/jeans_modern/average_m.glb new file mode 100644 index 000000000..e0a7100bc Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/average_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/average_m_base_albedo.png new file mode 100644 index 000000000..3e79b15b8 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/average_m_mask.png b/client/assets/characters/clothing/jeans_modern/average_m_mask.png new file mode 100644 index 000000000..aaec0fc5c Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/base_albedo.png b/client/assets/characters/clothing/jeans_modern/base_albedo.png new file mode 100644 index 000000000..3e79b15b8 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/child.glb b/client/assets/characters/clothing/jeans_modern/child.glb new file mode 100644 index 000000000..4150c1321 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/child.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/child_base_albedo.png b/client/assets/characters/clothing/jeans_modern/child_base_albedo.png new file mode 100644 index 000000000..66b6d5a4c Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/child_mask.png b/client/assets/characters/clothing/jeans_modern/child_mask.png new file mode 100644 index 000000000..d44abfc56 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/coverage.json b/client/assets/characters/clothing/jeans_modern/coverage.json new file mode 100644 index 000000000..00e827118 --- /dev/null +++ b/client/assets/characters/clothing/jeans_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["hips", "leg_upper_l", "leg_upper_r", "leg_lower_l", "leg_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/jeans_modern/heavy_f.glb b/client/assets/characters/clothing/jeans_modern/heavy_f.glb new file mode 100644 index 000000000..d25a8bd65 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..36519b93b Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/heavy_f_mask.png b/client/assets/characters/clothing/jeans_modern/heavy_f_mask.png new file mode 100644 index 000000000..d16a0b37e Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/heavy_m.glb b/client/assets/characters/clothing/jeans_modern/heavy_m.glb new file mode 100644 index 000000000..b97099056 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..f1b9e1f68 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/heavy_m_mask.png b/client/assets/characters/clothing/jeans_modern/heavy_m_mask.png new file mode 100644 index 000000000..56178c000 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/muscular_f.glb b/client/assets/characters/clothing/jeans_modern/muscular_f.glb new file mode 100644 index 000000000..674875be3 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..07d6aeb46 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/muscular_f_mask.png b/client/assets/characters/clothing/jeans_modern/muscular_f_mask.png new file mode 100644 index 000000000..67c39da83 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/muscular_m.glb b/client/assets/characters/clothing/jeans_modern/muscular_m.glb new file mode 100644 index 000000000..152a54aae Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..17f564e18 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/muscular_m_mask.png b/client/assets/characters/clothing/jeans_modern/muscular_m_mask.png new file mode 100644 index 000000000..9142b14f7 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/reference_mask.png b/client/assets/characters/clothing/jeans_modern/reference_mask.png new file mode 100644 index 000000000..aaec0fc5c Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/teen_f.glb b/client/assets/characters/clothing/jeans_modern/teen_f.glb new file mode 100644 index 000000000..493aa2ecf Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..87566f6d6 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/teen_f_mask.png b/client/assets/characters/clothing/jeans_modern/teen_f_mask.png new file mode 100644 index 000000000..14f729c61 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/teen_m.glb b/client/assets/characters/clothing/jeans_modern/teen_m.glb new file mode 100644 index 000000000..3e67cc360 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..7b5d1c72c Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/teen_m_mask.png b/client/assets/characters/clothing/jeans_modern/teen_m_mask.png new file mode 100644 index 000000000..0b26e8385 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/thin_f.glb b/client/assets/characters/clothing/jeans_modern/thin_f.glb new file mode 100644 index 000000000..2cb611ecc Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/jeans_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..9b08389cc Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/thin_f_mask.png b/client/assets/characters/clothing/jeans_modern/thin_f_mask.png new file mode 100644 index 000000000..c65963304 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/jeans_modern/thin_m.glb b/client/assets/characters/clothing/jeans_modern/thin_m.glb new file mode 100644 index 000000000..06400b64d Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/jeans_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/jeans_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..0bb6b7c05 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/jeans_modern/thin_m_mask.png b/client/assets/characters/clothing/jeans_modern/thin_m_mask.png new file mode 100644 index 000000000..f6e695c23 Binary files /dev/null and b/client/assets/characters/clothing/jeans_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/average_f.glb b/client/assets/characters/clothing/pants_formal/average_f.glb new file mode 100644 index 000000000..fb18e1b09 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/average_f.glb differ diff --git a/client/assets/characters/clothing/pants_formal/average_f_base_albedo.png b/client/assets/characters/clothing/pants_formal/average_f_base_albedo.png new file mode 100644 index 000000000..322c9efff Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/average_f_mask.png b/client/assets/characters/clothing/pants_formal/average_f_mask.png new file mode 100644 index 000000000..431247ec7 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/average_f_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/average_m.glb b/client/assets/characters/clothing/pants_formal/average_m.glb new file mode 100644 index 000000000..2de13f95b Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/average_m.glb differ diff --git a/client/assets/characters/clothing/pants_formal/average_m_base_albedo.png b/client/assets/characters/clothing/pants_formal/average_m_base_albedo.png new file mode 100644 index 000000000..94dd6ced9 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/average_m_mask.png b/client/assets/characters/clothing/pants_formal/average_m_mask.png new file mode 100644 index 000000000..0e5f76d01 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/average_m_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/base_albedo.png b/client/assets/characters/clothing/pants_formal/base_albedo.png new file mode 100644 index 000000000..94dd6ced9 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/child.glb b/client/assets/characters/clothing/pants_formal/child.glb new file mode 100644 index 000000000..1cce694ec Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/child.glb differ diff --git a/client/assets/characters/clothing/pants_formal/child_base_albedo.png b/client/assets/characters/clothing/pants_formal/child_base_albedo.png new file mode 100644 index 000000000..46dd08513 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/child_mask.png b/client/assets/characters/clothing/pants_formal/child_mask.png new file mode 100644 index 000000000..92733ea72 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/child_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/coverage.json b/client/assets/characters/clothing/pants_formal/coverage.json new file mode 100644 index 000000000..00e827118 --- /dev/null +++ b/client/assets/characters/clothing/pants_formal/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["hips", "leg_upper_l", "leg_upper_r", "leg_lower_l", "leg_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/pants_formal/heavy_f.glb b/client/assets/characters/clothing/pants_formal/heavy_f.glb new file mode 100644 index 000000000..0759c03e7 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/heavy_f.glb differ diff --git a/client/assets/characters/clothing/pants_formal/heavy_f_base_albedo.png b/client/assets/characters/clothing/pants_formal/heavy_f_base_albedo.png new file mode 100644 index 000000000..8ceccc273 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/heavy_f_mask.png b/client/assets/characters/clothing/pants_formal/heavy_f_mask.png new file mode 100644 index 000000000..b11f4916e Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/heavy_m.glb b/client/assets/characters/clothing/pants_formal/heavy_m.glb new file mode 100644 index 000000000..a74b4685d Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/heavy_m.glb differ diff --git a/client/assets/characters/clothing/pants_formal/heavy_m_base_albedo.png b/client/assets/characters/clothing/pants_formal/heavy_m_base_albedo.png new file mode 100644 index 000000000..e29900b16 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/heavy_m_mask.png b/client/assets/characters/clothing/pants_formal/heavy_m_mask.png new file mode 100644 index 000000000..134eeb945 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/muscular_f.glb b/client/assets/characters/clothing/pants_formal/muscular_f.glb new file mode 100644 index 000000000..58088a2f9 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/muscular_f.glb differ diff --git a/client/assets/characters/clothing/pants_formal/muscular_f_base_albedo.png b/client/assets/characters/clothing/pants_formal/muscular_f_base_albedo.png new file mode 100644 index 000000000..a5ff16810 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/muscular_f_mask.png b/client/assets/characters/clothing/pants_formal/muscular_f_mask.png new file mode 100644 index 000000000..0d64c78fa Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/muscular_m.glb b/client/assets/characters/clothing/pants_formal/muscular_m.glb new file mode 100644 index 000000000..e385d3d3d Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/muscular_m.glb differ diff --git a/client/assets/characters/clothing/pants_formal/muscular_m_base_albedo.png b/client/assets/characters/clothing/pants_formal/muscular_m_base_albedo.png new file mode 100644 index 000000000..c1c3060ce Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/muscular_m_mask.png b/client/assets/characters/clothing/pants_formal/muscular_m_mask.png new file mode 100644 index 000000000..dbd7f6a66 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/reference_mask.png b/client/assets/characters/clothing/pants_formal/reference_mask.png new file mode 100644 index 000000000..0e5f76d01 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/reference_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/teen_f.glb b/client/assets/characters/clothing/pants_formal/teen_f.glb new file mode 100644 index 000000000..19986b8c9 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/teen_f.glb differ diff --git a/client/assets/characters/clothing/pants_formal/teen_f_base_albedo.png b/client/assets/characters/clothing/pants_formal/teen_f_base_albedo.png new file mode 100644 index 000000000..3ef10c32a Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/teen_f_mask.png b/client/assets/characters/clothing/pants_formal/teen_f_mask.png new file mode 100644 index 000000000..fea3e988f Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/teen_m.glb b/client/assets/characters/clothing/pants_formal/teen_m.glb new file mode 100644 index 000000000..6b44529a4 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/teen_m.glb differ diff --git a/client/assets/characters/clothing/pants_formal/teen_m_base_albedo.png b/client/assets/characters/clothing/pants_formal/teen_m_base_albedo.png new file mode 100644 index 000000000..c30bc98bd Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/teen_m_mask.png b/client/assets/characters/clothing/pants_formal/teen_m_mask.png new file mode 100644 index 000000000..e56909d13 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/thin_f.glb b/client/assets/characters/clothing/pants_formal/thin_f.glb new file mode 100644 index 000000000..3b92d821f Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/thin_f.glb differ diff --git a/client/assets/characters/clothing/pants_formal/thin_f_base_albedo.png b/client/assets/characters/clothing/pants_formal/thin_f_base_albedo.png new file mode 100644 index 000000000..4e93caebc Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/thin_f_mask.png b/client/assets/characters/clothing/pants_formal/thin_f_mask.png new file mode 100644 index 000000000..c4fae8b94 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/pants_formal/thin_m.glb b/client/assets/characters/clothing/pants_formal/thin_m.glb new file mode 100644 index 000000000..90703e186 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/thin_m.glb differ diff --git a/client/assets/characters/clothing/pants_formal/thin_m_base_albedo.png b/client/assets/characters/clothing/pants_formal/thin_m_base_albedo.png new file mode 100644 index 000000000..cf117c1ed Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/pants_formal/thin_m_mask.png b/client/assets/characters/clothing/pants_formal/thin_m_mask.png new file mode 100644 index 000000000..0e5f76d01 Binary files /dev/null and b/client/assets/characters/clothing/pants_formal/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/average_f.glb b/client/assets/characters/clothing/shorts_modern/average_f.glb new file mode 100644 index 000000000..b0304e385 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/average_f_base_albedo.png b/client/assets/characters/clothing/shorts_modern/average_f_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/average_f_mask.png b/client/assets/characters/clothing/shorts_modern/average_f_mask.png new file mode 100644 index 000000000..d8aa65c72 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/average_m.glb b/client/assets/characters/clothing/shorts_modern/average_m.glb new file mode 100644 index 000000000..5a7a427ce Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/average_m_base_albedo.png b/client/assets/characters/clothing/shorts_modern/average_m_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/average_m_mask.png b/client/assets/characters/clothing/shorts_modern/average_m_mask.png new file mode 100644 index 000000000..82e2cdb56 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/base_albedo.png b/client/assets/characters/clothing/shorts_modern/base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/child.glb b/client/assets/characters/clothing/shorts_modern/child.glb new file mode 100644 index 000000000..9eb35af6b Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/child.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/child_base_albedo.png b/client/assets/characters/clothing/shorts_modern/child_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/child_mask.png b/client/assets/characters/clothing/shorts_modern/child_mask.png new file mode 100644 index 000000000..70ef68663 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/coverage.json b/client/assets/characters/clothing/shorts_modern/coverage.json new file mode 100644 index 000000000..aa1db5d24 --- /dev/null +++ b/client/assets/characters/clothing/shorts_modern/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["hips", "leg_upper_l", "leg_upper_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/shorts_modern/heavy_f.glb b/client/assets/characters/clothing/shorts_modern/heavy_f.glb new file mode 100644 index 000000000..6907b1cad Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/shorts_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/heavy_f_mask.png b/client/assets/characters/clothing/shorts_modern/heavy_f_mask.png new file mode 100644 index 000000000..d8aa65c72 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/heavy_m.glb b/client/assets/characters/clothing/shorts_modern/heavy_m.glb new file mode 100644 index 000000000..ed32feb68 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/shorts_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/heavy_m_mask.png b/client/assets/characters/clothing/shorts_modern/heavy_m_mask.png new file mode 100644 index 000000000..82e2cdb56 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/muscular_f.glb b/client/assets/characters/clothing/shorts_modern/muscular_f.glb new file mode 100644 index 000000000..633aa9591 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/muscular_f_base_albedo.png b/client/assets/characters/clothing/shorts_modern/muscular_f_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/muscular_f_mask.png b/client/assets/characters/clothing/shorts_modern/muscular_f_mask.png new file mode 100644 index 000000000..fbac6d67d Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/muscular_m.glb b/client/assets/characters/clothing/shorts_modern/muscular_m.glb new file mode 100644 index 000000000..3a8a812e0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/muscular_m_base_albedo.png b/client/assets/characters/clothing/shorts_modern/muscular_m_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/muscular_m_mask.png b/client/assets/characters/clothing/shorts_modern/muscular_m_mask.png new file mode 100644 index 000000000..40dd39728 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/reference_mask.png b/client/assets/characters/clothing/shorts_modern/reference_mask.png new file mode 100644 index 000000000..82e2cdb56 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/teen_f.glb b/client/assets/characters/clothing/shorts_modern/teen_f.glb new file mode 100644 index 000000000..c4b5e6ae3 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/teen_f_base_albedo.png b/client/assets/characters/clothing/shorts_modern/teen_f_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/teen_f_mask.png b/client/assets/characters/clothing/shorts_modern/teen_f_mask.png new file mode 100644 index 000000000..86eb2401a Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/teen_m.glb b/client/assets/characters/clothing/shorts_modern/teen_m.glb new file mode 100644 index 000000000..da8378289 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/teen_m_base_albedo.png b/client/assets/characters/clothing/shorts_modern/teen_m_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/teen_m_mask.png b/client/assets/characters/clothing/shorts_modern/teen_m_mask.png new file mode 100644 index 000000000..70ef68663 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/thin_f.glb b/client/assets/characters/clothing/shorts_modern/thin_f.glb new file mode 100644 index 000000000..a30174576 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/shorts_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/thin_f_mask.png b/client/assets/characters/clothing/shorts_modern/thin_f_mask.png new file mode 100644 index 000000000..d8aa65c72 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/shorts_modern/thin_m.glb b/client/assets/characters/clothing/shorts_modern/thin_m.glb new file mode 100644 index 000000000..543491e98 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/shorts_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/shorts_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..b40c3cec0 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/shorts_modern/thin_m_mask.png b/client/assets/characters/clothing/shorts_modern/thin_m_mask.png new file mode 100644 index 000000000..82e2cdb56 Binary files /dev/null and b/client/assets/characters/clothing/shorts_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/average_f.glb b/client/assets/characters/clothing/suit_jacket_black/average_f.glb new file mode 100644 index 000000000..518a15cd8 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/average_f.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/average_f_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/average_f_base_albedo.png new file mode 100644 index 000000000..ac40587e7 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/average_f_mask.png b/client/assets/characters/clothing/suit_jacket_black/average_f_mask.png new file mode 100644 index 000000000..4f3b0d490 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/average_f_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/average_m.glb b/client/assets/characters/clothing/suit_jacket_black/average_m.glb new file mode 100644 index 000000000..7b3e99d7f Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/average_m.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/average_m_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/average_m_base_albedo.png new file mode 100644 index 000000000..eda3991f9 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/average_m_mask.png b/client/assets/characters/clothing/suit_jacket_black/average_m_mask.png new file mode 100644 index 000000000..f4fd5bd98 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/average_m_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/base_albedo.png new file mode 100644 index 000000000..eda3991f9 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/child.glb b/client/assets/characters/clothing/suit_jacket_black/child.glb new file mode 100644 index 000000000..1cc7dc08d Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/child.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/child_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/child_base_albedo.png new file mode 100644 index 000000000..aa8fd1260 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/child_mask.png b/client/assets/characters/clothing/suit_jacket_black/child_mask.png new file mode 100644 index 000000000..a830ba68b Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/child_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/coverage.json b/client/assets/characters/clothing/suit_jacket_black/coverage.json new file mode 100644 index 000000000..d72d0c9ac --- /dev/null +++ b/client/assets/characters/clothing/suit_jacket_black/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "torso_upper", "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/suit_jacket_black/heavy_f.glb b/client/assets/characters/clothing/suit_jacket_black/heavy_f.glb new file mode 100644 index 000000000..bca2a6b68 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/heavy_f.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/heavy_f_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/heavy_f_base_albedo.png new file mode 100644 index 000000000..3b32ec227 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/heavy_f_mask.png b/client/assets/characters/clothing/suit_jacket_black/heavy_f_mask.png new file mode 100644 index 000000000..1e6c5463a Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/heavy_m.glb b/client/assets/characters/clothing/suit_jacket_black/heavy_m.glb new file mode 100644 index 000000000..713c60c19 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/heavy_m.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/heavy_m_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/heavy_m_base_albedo.png new file mode 100644 index 000000000..69784a75d Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/heavy_m_mask.png b/client/assets/characters/clothing/suit_jacket_black/heavy_m_mask.png new file mode 100644 index 000000000..e98828f4f Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/muscular_f.glb b/client/assets/characters/clothing/suit_jacket_black/muscular_f.glb new file mode 100644 index 000000000..e7eadea85 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/muscular_f.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/muscular_f_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/muscular_f_base_albedo.png new file mode 100644 index 000000000..1ea0d64b5 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/muscular_f_mask.png b/client/assets/characters/clothing/suit_jacket_black/muscular_f_mask.png new file mode 100644 index 000000000..0082079b5 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/muscular_m.glb b/client/assets/characters/clothing/suit_jacket_black/muscular_m.glb new file mode 100644 index 000000000..98437778b Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/muscular_m.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/muscular_m_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/muscular_m_base_albedo.png new file mode 100644 index 000000000..674ba1f52 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/muscular_m_mask.png b/client/assets/characters/clothing/suit_jacket_black/muscular_m_mask.png new file mode 100644 index 000000000..a3b312d79 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/reference_mask.png b/client/assets/characters/clothing/suit_jacket_black/reference_mask.png new file mode 100644 index 000000000..f4fd5bd98 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/reference_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/teen_f.glb b/client/assets/characters/clothing/suit_jacket_black/teen_f.glb new file mode 100644 index 000000000..291f34b7f Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/teen_f.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/teen_f_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/teen_f_base_albedo.png new file mode 100644 index 000000000..70d84114f Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/teen_f_mask.png b/client/assets/characters/clothing/suit_jacket_black/teen_f_mask.png new file mode 100644 index 000000000..9222403a6 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/teen_m.glb b/client/assets/characters/clothing/suit_jacket_black/teen_m.glb new file mode 100644 index 000000000..30b7009b6 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/teen_m.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/teen_m_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/teen_m_base_albedo.png new file mode 100644 index 000000000..561e2d612 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/teen_m_mask.png b/client/assets/characters/clothing/suit_jacket_black/teen_m_mask.png new file mode 100644 index 000000000..c329274a7 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/thin_f.glb b/client/assets/characters/clothing/suit_jacket_black/thin_f.glb new file mode 100644 index 000000000..fc24e7139 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/thin_f.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/thin_f_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/thin_f_base_albedo.png new file mode 100644 index 000000000..4832f635f Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/thin_f_mask.png b/client/assets/characters/clothing/suit_jacket_black/thin_f_mask.png new file mode 100644 index 000000000..e62214dde Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/thin_m.glb b/client/assets/characters/clothing/suit_jacket_black/thin_m.glb new file mode 100644 index 000000000..2ba81e10b Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/thin_m.glb differ diff --git a/client/assets/characters/clothing/suit_jacket_black/thin_m_base_albedo.png b/client/assets/characters/clothing/suit_jacket_black/thin_m_base_albedo.png new file mode 100644 index 000000000..8df54f74f Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/suit_jacket_black/thin_m_mask.png b/client/assets/characters/clothing/suit_jacket_black/thin_m_mask.png new file mode 100644 index 000000000..65ad2be80 Binary files /dev/null and b/client/assets/characters/clothing/suit_jacket_black/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/average_f.glb b/client/assets/characters/clothing/tshirt_modern/average_f.glb index 88e1c77e6..aeb9da5ec 100644 Binary files a/client/assets/characters/clothing/tshirt_modern/average_f.glb and b/client/assets/characters/clothing/tshirt_modern/average_f.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/average_f_mask.png b/client/assets/characters/clothing/tshirt_modern/average_f_mask.png new file mode 100644 index 000000000..161404470 Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/average_f_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/average_m.glb b/client/assets/characters/clothing/tshirt_modern/average_m.glb index 428b37a64..cd627b4c6 100644 Binary files a/client/assets/characters/clothing/tshirt_modern/average_m.glb and b/client/assets/characters/clothing/tshirt_modern/average_m.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/average_m_mask.png b/client/assets/characters/clothing/tshirt_modern/average_m_mask.png new file mode 100644 index 000000000..b32b44002 Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/average_m_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/base_albedo.png b/client/assets/characters/clothing/tshirt_modern/base_albedo.png new file mode 100644 index 000000000..ee1fb5f5b Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/base_albedo.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/child.glb b/client/assets/characters/clothing/tshirt_modern/child.glb new file mode 100644 index 000000000..1117960e6 Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/child.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/child_base_albedo.png b/client/assets/characters/clothing/tshirt_modern/child_base_albedo.png new file mode 100644 index 000000000..ee1fb5f5b Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/child_mask.png b/client/assets/characters/clothing/tshirt_modern/child_mask.png new file mode 100644 index 000000000..85f3921d3 Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/child_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/heavy_f.glb b/client/assets/characters/clothing/tshirt_modern/heavy_f.glb new file mode 100644 index 000000000..5ee26b131 Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/heavy_f.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/heavy_f_base_albedo.png b/client/assets/characters/clothing/tshirt_modern/heavy_f_base_albedo.png new file mode 100644 index 000000000..ee1fb5f5b Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/heavy_f_mask.png b/client/assets/characters/clothing/tshirt_modern/heavy_f_mask.png new file mode 100644 index 000000000..dc0391984 Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/heavy_m.glb b/client/assets/characters/clothing/tshirt_modern/heavy_m.glb new file mode 100644 index 000000000..af0ebf76a Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/heavy_m.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/heavy_m_base_albedo.png b/client/assets/characters/clothing/tshirt_modern/heavy_m_base_albedo.png new file mode 100644 index 000000000..ee1fb5f5b Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/heavy_m_mask.png b/client/assets/characters/clothing/tshirt_modern/heavy_m_mask.png new file mode 100644 index 000000000..6b46c270a Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/muscular_f.glb b/client/assets/characters/clothing/tshirt_modern/muscular_f.glb index aea595c46..4d5f075af 100644 Binary files a/client/assets/characters/clothing/tshirt_modern/muscular_f.glb and b/client/assets/characters/clothing/tshirt_modern/muscular_f.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/muscular_f_mask.png b/client/assets/characters/clothing/tshirt_modern/muscular_f_mask.png new file mode 100644 index 000000000..439416236 Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/muscular_m.glb b/client/assets/characters/clothing/tshirt_modern/muscular_m.glb index 37912e2cd..6283f7273 100644 Binary files a/client/assets/characters/clothing/tshirt_modern/muscular_m.glb and b/client/assets/characters/clothing/tshirt_modern/muscular_m.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/muscular_m_mask.png b/client/assets/characters/clothing/tshirt_modern/muscular_m_mask.png new file mode 100644 index 000000000..aaf492e5f Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/reference_mask.png b/client/assets/characters/clothing/tshirt_modern/reference_mask.png index 022146ea9..b32b44002 100644 Binary files a/client/assets/characters/clothing/tshirt_modern/reference_mask.png and b/client/assets/characters/clothing/tshirt_modern/reference_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/teen_f.glb b/client/assets/characters/clothing/tshirt_modern/teen_f.glb index 8d17fe59f..0bc75938c 100644 Binary files a/client/assets/characters/clothing/tshirt_modern/teen_f.glb and b/client/assets/characters/clothing/tshirt_modern/teen_f.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/teen_f_mask.png b/client/assets/characters/clothing/tshirt_modern/teen_f_mask.png new file mode 100644 index 000000000..4ed8fbc64 Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/teen_m.glb b/client/assets/characters/clothing/tshirt_modern/teen_m.glb index b69a29e61..580534828 100644 Binary files a/client/assets/characters/clothing/tshirt_modern/teen_m.glb and b/client/assets/characters/clothing/tshirt_modern/teen_m.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/teen_m_mask.png b/client/assets/characters/clothing/tshirt_modern/teen_m_mask.png new file mode 100644 index 000000000..c508818bb Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/thin_f.glb b/client/assets/characters/clothing/tshirt_modern/thin_f.glb new file mode 100644 index 000000000..8d3bc4bac Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/thin_f.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/thin_f_base_albedo.png b/client/assets/characters/clothing/tshirt_modern/thin_f_base_albedo.png new file mode 100644 index 000000000..ee1fb5f5b Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/thin_f_mask.png b/client/assets/characters/clothing/tshirt_modern/thin_f_mask.png new file mode 100644 index 000000000..1e9dd8aca Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/thin_m.glb b/client/assets/characters/clothing/tshirt_modern/thin_m.glb new file mode 100644 index 000000000..f23038f2c Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/thin_m.glb differ diff --git a/client/assets/characters/clothing/tshirt_modern/thin_m_base_albedo.png b/client/assets/characters/clothing/tshirt_modern/thin_m_base_albedo.png new file mode 100644 index 000000000..ee1fb5f5b Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/tshirt_modern/thin_m_mask.png b/client/assets/characters/clothing/tshirt_modern/thin_m_mask.png new file mode 100644 index 000000000..a5c8dc0bf Binary files /dev/null and b/client/assets/characters/clothing/tshirt_modern/thin_m_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/average_f.glb b/client/assets/characters/clothing/uniform_utility/average_f.glb new file mode 100644 index 000000000..d2497fc22 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/average_f.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/average_f_base_albedo.png b/client/assets/characters/clothing/uniform_utility/average_f_base_albedo.png new file mode 100644 index 000000000..75f0b8090 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/average_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/average_f_mask.png b/client/assets/characters/clothing/uniform_utility/average_f_mask.png new file mode 100644 index 000000000..f62a1be1e Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/average_f_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/average_m.glb b/client/assets/characters/clothing/uniform_utility/average_m.glb new file mode 100644 index 000000000..d6e26ae69 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/average_m.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/average_m_base_albedo.png b/client/assets/characters/clothing/uniform_utility/average_m_base_albedo.png new file mode 100644 index 000000000..55efcb31d Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/average_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/average_m_mask.png b/client/assets/characters/clothing/uniform_utility/average_m_mask.png new file mode 100644 index 000000000..f23637d25 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/average_m_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/base_albedo.png b/client/assets/characters/clothing/uniform_utility/base_albedo.png new file mode 100644 index 000000000..55efcb31d Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/child.glb b/client/assets/characters/clothing/uniform_utility/child.glb new file mode 100644 index 000000000..8e13b0f4f Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/child.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/child_base_albedo.png b/client/assets/characters/clothing/uniform_utility/child_base_albedo.png new file mode 100644 index 000000000..26e19d000 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/child_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/child_mask.png b/client/assets/characters/clothing/uniform_utility/child_mask.png new file mode 100644 index 000000000..9869cb87c Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/child_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/coverage.json b/client/assets/characters/clothing/uniform_utility/coverage.json new file mode 100644 index 000000000..8c9167667 --- /dev/null +++ b/client/assets/characters/clothing/uniform_utility/coverage.json @@ -0,0 +1,5 @@ +{ + "hides": ["torso", "torso_upper", "hips", "arm_upper_l", "arm_upper_r", "arm_lower_l", "arm_lower_r", "leg_upper_l", "leg_upper_r", "leg_lower_l", "leg_lower_r"], + "torso_variant": "full", + "multi_region": true +} diff --git a/client/assets/characters/clothing/uniform_utility/heavy_f.glb b/client/assets/characters/clothing/uniform_utility/heavy_f.glb new file mode 100644 index 000000000..5da4cf4db Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/heavy_f.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/heavy_f_base_albedo.png b/client/assets/characters/clothing/uniform_utility/heavy_f_base_albedo.png new file mode 100644 index 000000000..2429a5c91 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/heavy_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/heavy_f_mask.png b/client/assets/characters/clothing/uniform_utility/heavy_f_mask.png new file mode 100644 index 000000000..e2a17638c Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/heavy_f_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/heavy_m.glb b/client/assets/characters/clothing/uniform_utility/heavy_m.glb new file mode 100644 index 000000000..821ee03d9 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/heavy_m.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/heavy_m_base_albedo.png b/client/assets/characters/clothing/uniform_utility/heavy_m_base_albedo.png new file mode 100644 index 000000000..ab8068dc8 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/heavy_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/heavy_m_mask.png b/client/assets/characters/clothing/uniform_utility/heavy_m_mask.png new file mode 100644 index 000000000..db35445bf Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/heavy_m_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/muscular_f.glb b/client/assets/characters/clothing/uniform_utility/muscular_f.glb new file mode 100644 index 000000000..009124127 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/muscular_f.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/muscular_f_base_albedo.png b/client/assets/characters/clothing/uniform_utility/muscular_f_base_albedo.png new file mode 100644 index 000000000..4fe9bc184 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/muscular_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/muscular_f_mask.png b/client/assets/characters/clothing/uniform_utility/muscular_f_mask.png new file mode 100644 index 000000000..39a46d814 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/muscular_f_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/muscular_m.glb b/client/assets/characters/clothing/uniform_utility/muscular_m.glb new file mode 100644 index 000000000..e8e30f9e4 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/muscular_m.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/muscular_m_base_albedo.png b/client/assets/characters/clothing/uniform_utility/muscular_m_base_albedo.png new file mode 100644 index 000000000..5fe12a653 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/muscular_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/muscular_m_mask.png b/client/assets/characters/clothing/uniform_utility/muscular_m_mask.png new file mode 100644 index 000000000..e17c067d6 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/muscular_m_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/reference_mask.png b/client/assets/characters/clothing/uniform_utility/reference_mask.png new file mode 100644 index 000000000..f23637d25 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/reference_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/teen_f.glb b/client/assets/characters/clothing/uniform_utility/teen_f.glb new file mode 100644 index 000000000..5dfee4572 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/teen_f.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/teen_f_base_albedo.png b/client/assets/characters/clothing/uniform_utility/teen_f_base_albedo.png new file mode 100644 index 000000000..0ed3ab4c7 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/teen_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/teen_f_mask.png b/client/assets/characters/clothing/uniform_utility/teen_f_mask.png new file mode 100644 index 000000000..18d9225c3 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/teen_f_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/teen_m.glb b/client/assets/characters/clothing/uniform_utility/teen_m.glb new file mode 100644 index 000000000..55c561671 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/teen_m.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/teen_m_base_albedo.png b/client/assets/characters/clothing/uniform_utility/teen_m_base_albedo.png new file mode 100644 index 000000000..0b28c261a Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/teen_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/teen_m_mask.png b/client/assets/characters/clothing/uniform_utility/teen_m_mask.png new file mode 100644 index 000000000..b267e9020 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/teen_m_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/thin_f.glb b/client/assets/characters/clothing/uniform_utility/thin_f.glb new file mode 100644 index 000000000..d22e91151 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/thin_f.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/thin_f_base_albedo.png b/client/assets/characters/clothing/uniform_utility/thin_f_base_albedo.png new file mode 100644 index 000000000..5dff2e153 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/thin_f_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/thin_f_mask.png b/client/assets/characters/clothing/uniform_utility/thin_f_mask.png new file mode 100644 index 000000000..9d7b16fcb Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/thin_f_mask.png differ diff --git a/client/assets/characters/clothing/uniform_utility/thin_m.glb b/client/assets/characters/clothing/uniform_utility/thin_m.glb new file mode 100644 index 000000000..b110a3a92 Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/thin_m.glb differ diff --git a/client/assets/characters/clothing/uniform_utility/thin_m_base_albedo.png b/client/assets/characters/clothing/uniform_utility/thin_m_base_albedo.png new file mode 100644 index 000000000..70cb2012d Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/thin_m_base_albedo.png differ diff --git a/client/assets/characters/clothing/uniform_utility/thin_m_mask.png b/client/assets/characters/clothing/uniform_utility/thin_m_mask.png new file mode 100644 index 000000000..052301d5c Binary files /dev/null and b/client/assets/characters/clothing/uniform_utility/thin_m_mask.png differ diff --git a/client/assets/characters/manifest.json b/client/assets/characters/manifest.json index 84a0a1947..0ce29da1b 100644 --- a/client/assets/characters/manifest.json +++ b/client/assets/characters/manifest.json @@ -59,6 +59,84 @@ "slot": "torso", "style": "modern", "logo_capable": true + }, + "hoodie_modern": { + "slot": "torso", + "style": "modern", + "logo_capable": true + }, + "buttondown_modern": { + "slot": "torso", + "style": "modern", + "logo_capable": false + }, + "shorts_modern": { + "slot": "legs", + "style": "modern", + "logo_capable": false + }, + "jeans_modern": { + "slot": "legs", + "style": "modern", + "logo_capable": false + }, + "pants_formal": { + "slot": "legs", + "style": "formal", + "logo_capable": false + }, + "jacket_modern": { + "slot": "torso", + "style": "modern", + "logo_capable": true + }, + "suit_jacket_black": { + "slot": "torso", + "style": "formal", + "logo_capable": false, + "regions": { + "R": "lapels_collar", + "G": "jacket_body", + "B": "sleeves", + "A": "shirt_triangle" + }, + "default_tints": [ + [ + 0.13, + 0.13, + 0.16, + 1 + ], + [ + 0.09, + 0.09, + 0.11, + 1 + ], + [ + 0.09, + 0.09, + 0.11, + 1 + ], + [ + 0.93, + 0.93, + 0.96, + 1 + ] + ] + }, + "uniform_utility": { + "slot": "full_body", + "style": "utility", + "logo_capable": true, + "default_tints": [ + "#3b474f", + "#6b8280", + "#d18c2e", + "#d9dee0" + ] } }, "accessories": [] diff --git a/client/scripts/rendering/character_visual.gd b/client/scripts/rendering/character_visual.gd index 26cc19dc0..5b120b109 100644 --- a/client/scripts/rendering/character_visual.gd +++ b/client/scripts/rendering/character_visual.gd @@ -586,8 +586,12 @@ func _load_clothing(desc: CharacterVisualDescriptor) -> void: if coverage.get("torso_variant") == "upper": _active_torso_variant = "upper" - # Load recolor mask: shared across all body-type variants (same UV layout after Surface Deform). - var mask_path := item_dir + "reference_mask.png" + # Load recolor mask: per-body-authored garments (offset-shell route) ship + # one mask per body type; SD-fit garments share reference_mask.png + # (same UV layout across variants after Surface Deform). + var mask_path := item_dir + "%s_mask.png" % desc.body_type_key() + if not ResourceLoader.exists(mask_path): + mask_path = item_dir + "reference_mask.png" var mask_tex: Texture2D = null if ResourceLoader.exists(mask_path): mask_tex = load(mask_path) as Texture2D diff --git a/client/tests/test_garment_engine_t1089.gd b/client/tests/test_garment_engine_t1089.gd index b3ac8ecf5..59c82f5bb 100644 --- a/client/tests/test_garment_engine_t1089.gd +++ b/client/tests/test_garment_engine_t1089.gd @@ -22,8 +22,11 @@ const CLOTHING_DIR := "res://assets/characters/clothing/tshirt_modern/" const LOGO_PATH := "res://assets/characters/logos/thrds.png" const MANIFEST_PATH := "res://assets/characters/manifest.json" +## All 11 body types — tshirt_modern is authored PER BODY (offset-shell mode, +## T-1089): each body gets its own .glb + _mask.png. const FITTED_BODIES := [ - "average_m", "average_f", "muscular_m", "muscular_f", "teen_m", "teen_f", + "average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", + "heavy_m", "heavy_f", "teen_m", "teen_f", "child", ] @@ -155,8 +158,23 @@ func _has_skinned_mesh(root: Node) -> bool: func test_logo_and_mask_textures_load() -> void: assert_bool(ResourceLoader.exists(LOGO_PATH)).is_true() + # Runtime fallback mask (SD-fit garments + safety net) must still exist. assert_bool(ResourceLoader.exists(CLOTHING_DIR + "reference_mask.png")).is_true() var logo := load(LOGO_PATH) as Texture2D assert_object(logo).is_not_null() var mask := load(CLOTHING_DIR + "reference_mask.png") as Texture2D assert_object(mask).is_not_null() + + +func test_per_body_masks_load() -> void: + # Per-body offset-shell authoring: the compositor prefers + # _mask.png over reference_mask.png (character_visual.gd). + for body in FITTED_BODIES: + var mask_path := CLOTHING_DIR + "%s_mask.png" % body + assert_bool(ResourceLoader.exists(mask_path)).override_failure_message( + "per-body region mask missing: %s" % mask_path + ).is_true() + var mask := load(mask_path) as Texture2D + assert_object(mask).override_failure_message( + "per-body region mask failed to load: %s" % mask_path + ).is_not_null() diff --git a/client/tools/garment_preview/preview_buttondown_modern.gd b/client/tools/garment_preview/preview_buttondown_modern.gd new file mode 100644 index 000000000..13da5fb01 --- /dev/null +++ b/client/tools/garment_preview/preview_buttondown_modern.gd @@ -0,0 +1,187 @@ +extends Node3D +## Buttondown preview capture (T-1089 buttondown_modern) — per-garment copy of +## preview_scene.gd (same convention as preview_shorts_modern.gd): oxford +## palette (white collar/cuffs/placket over light-blue body), body-aware mask +## loading (_mask.png first, mirroring runtime), no logo (logo_capable +## OFF), front + 3/4 walk yaws only. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "buttondown_modern" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_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, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, +} + +# Winchester oxford: white collar + white cuffs/placket over light-blue body. +const TINT_COLLAR := Color(0.93, 0.94, 0.96) # R region +const TINT_BODY := Color(0.55, 0.68, 0.82) # G region +const TINT_CUFF := Color(0.90, 0.91, 0.94) # B region (cuffs + placket) + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "buttondown_modern") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame the upper body so placket/buttons/collar/cuffs read clearly. + _cam.position = Vector3(0.0, 1.25, 1.7) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 1.15, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body-authored garment: prefer _mask.png (runtime behaviour). + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a walk pose so the shirt reads in motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task spec: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).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 _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_COLLAR) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", TINT_CUFF) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("logo_enabled", 0.0) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_buttondown_modern.tscn b/client/tools/garment_preview/preview_buttondown_modern.tscn new file mode 100644 index 000000000..5bc4438fd --- /dev/null +++ b/client/tools/garment_preview/preview_buttondown_modern.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_buttondown_modern.gd" id="1_bdprev"] + +[node name="ButtondownPreview" type="Node3D"] +script = ExtResource("1_bdprev") diff --git a/client/tools/garment_preview/preview_shorts_modern.gd b/client/tools/garment_preview/preview_shorts_modern.gd new file mode 100644 index 000000000..e57d54762 --- /dev/null +++ b/client/tools/garment_preview/preview_shorts_modern.gd @@ -0,0 +1,185 @@ +extends Node3D +## shorts_modern preview capture (T-1089 basic set) — parameterized copy of +## preview_scene.gd for a LOWER-BODY 2-region garment: per-body region mask +## (reference fallback), no logo, camera framed on hips + legs, front + 3/4 +## Walk renders. Temporary tool scene — safe to delete after captures. +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "shorts_modern" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/shorts_modern" + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_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, + "teen_m": CharacterVisualDescriptor.BodyType.TEEN_M, + "teen_f": CharacterVisualDescriptor.BodyType.TEEN_F, +} + +# Modern casual palette: charcoal waistband, muted olive body. tint_2/3 unused. +const TINT_WAISTBAND := Color(0.13, 0.14, 0.16) # R region +const TINT_BODY := Color(0.42, 0.47, 0.38) # G region + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "shorts_modern") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/shorts_modern") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame hips + legs so the waistband and hem read clearly. + _cam.position = Vector3(0.0, 0.82, 1.7) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.72, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body region mask (offset-shell route); reference fallback mirrors + # the runtime compositor. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + for mi in meshes: + _apply_garment_shader(mi, shader, mask) + + # Freeze on a mid-stride Walk pose so hem + waistband read under motion. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 (task deliverable: two preview renders). + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).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 _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + mat.set_shader_parameter("tint_0", TINT_WAISTBAND) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", Color.WHITE) + mat.set_shader_parameter("tint_3", Color.WHITE) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_shorts_modern.tscn b/client/tools/garment_preview/preview_shorts_modern.tscn new file mode 100644 index 000000000..8cb35a8cf --- /dev/null +++ b/client/tools/garment_preview/preview_shorts_modern.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_shorts_modern.gd" id="1_prevs"] + +[node name="GarmentPreviewShorts" type="Node3D"] +script = ExtResource("1_prevs") diff --git a/client/tools/garment_preview/preview_uniform_utility.gd b/client/tools/garment_preview/preview_uniform_utility.gd new file mode 100644 index 000000000..6d415f740 --- /dev/null +++ b/client/tools/garment_preview/preview_uniform_utility.gd @@ -0,0 +1,208 @@ +extends Node3D +## uniform_utility preview capture (T-1089 route (c) colorable-uniform proof). +## +## Full-body variant of preview_scene.gd for the utility coverall: whole-figure +## camera framing (the garment runs collar to ankle), per-body region mask +## preference (_mask.png, the per-body offset-shell convention) and the +## default utility palette on the four recolor zones: +## R trim (collar + cuffs) = dark slate +## G body fabric = utility teal-grey +## B belt + patches = safety amber +## A shoulder marks = pale high-vis +## The chest patch carries the thrds logo (logo_capable proof). +## +## Config via env vars (all optional): +## GARMENT_PREVIEW_ITEM default "uniform_utility" +## GARMENT_PREVIEW_BODY default "average_m" +## GARMENT_PREVIEW_OUT default "/.cache/garment-preview/uniform_utility" +## GARMENT_PREVIEW_LOGO default thrds +## +## Run: godot4 --path client --rendering-driver opengl3 \ +## res://tools/garment_preview/preview_uniform_utility.tscn + +const CLOTHING_DIR := "res://assets/characters/clothing/" +const LOGO_PATH := "res://assets/characters/logos/thrds.png" +const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader" +const VIEWPORT := Vector2i(768, 1024) + +const BODY_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, +} + +# Default utility palette — four cleanly recolorable zones. +const TINT_TRIM := Color(0.23, 0.28, 0.31) # R: collar + cuffs, dark slate +const TINT_BODY := Color(0.42, 0.51, 0.50) # G: main fabric, teal-grey +const TINT_BELT := Color(0.82, 0.55, 0.18) # B: belt + patches, safety amber +const TINT_MARKS := Color(0.85, 0.87, 0.88) # A: shoulder marks, pale high-vis + +var _out_dir := "" +var _cam: Camera3D = null + + +func _ready() -> void: + var item := _env("GARMENT_PREVIEW_ITEM", "uniform_utility") + var body := _env("GARMENT_PREVIEW_BODY", "average_m") + _out_dir = _env("GARMENT_PREVIEW_OUT", + "/var/mnt/data/projects/settled-reach/.cache/garment-preview/uniform_utility") + get_window().size = VIEWPORT + _setup_stage() + _run.call_deferred(item, body) + + +func _env(key: String, fallback: String) -> String: + var v := OS.get_environment(key) + return v if not v.is_empty() else fallback + + +func _setup_stage() -> void: + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-38, 28, 0) + light.light_energy = 1.3 + add_child(light) + var env := Environment.new() + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = Color(0.82, 0.82, 0.86) + env.ambient_light_energy = 0.9 + env.background_mode = Environment.BG_COLOR + env.background_color = Color(0.16, 0.17, 0.20) + var we := WorldEnvironment.new() + we.environment = env + add_child(we) + _cam = Camera3D.new() + _cam.fov = 45.0 + add_child(_cam) + # Frame the WHOLE figure — the coverall runs collar to ankle. + _cam.position = Vector3(0.0, 1.05, 3.1) + _cam.look_at_from_position(_cam.position, Vector3(0.0, 0.92, 0.0), Vector3.UP) + + +func _run(item: String, body: String) -> void: + DirAccess.make_dir_recursive_absolute(_out_dir) + if not BODY_KEY_TO_ENUM.has(body): + push_error("preview: unknown body %s" % body) + get_tree().quit(1) + return + + var cv := CharacterVisual.new() + add_child(cv) + var desc := CharacterVisualDescriptor.new() + desc.body_type = BODY_KEY_TO_ENUM[body] + desc.head_id = "head_001" + desc.hair_id = "buzzed" + desc.eyebrow_id = "regular" + desc.skin_tone = 3 + cv.load_descriptor(desc) + + var skel := cv.get_skeleton() + if skel == null: + push_error("preview: no skeleton") + get_tree().quit(1) + return + + var meshes := _attach_garment(skel, item, body) + print("preview: attached %d garment meshes" % meshes.size()) + var shader := load(SHADER_PATH) as Shader + # Per-body mask first (offset-shell per-body convention), then fallback. + var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body]) + if mask == null: + mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item) + var logo_path := _env("GARMENT_PREVIEW_LOGO", LOGO_PATH) + var logo := _try_load(logo_path) + for mi in meshes: + _apply_garment_shader(mi, shader, mask, logo) + + # Freeze mid-walk so the coverall reads under a real pose. + var ap := cv.get_animation_player() + if ap != null: + var walk := _resolve_clip(ap, "Walk") + if not walk.is_empty(): + cv.play_animation(walk) + ap.pause() + ap.seek(ap.current_animation_length * 0.25, true) + await get_tree().process_frame + + # Front + 3/4 — the two requested preview angles. + for yaw in [0, 45]: + cv.rotation_degrees.y = float(yaw) + await RenderingServer.frame_post_draw + await RenderingServer.frame_post_draw + var fname := "%s__%s__yaw%03d.png" % [item, body, yaw] + get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname)) + print("preview: wrote ", fname) + + print("preview: DONE") + get_tree().quit(0) + + +func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array: + var out: Array = [] + var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body] + if not ResourceLoader.exists(glb): + push_error("preview: garment GLB missing %s" % glb) + return out + var inst := (load(glb) as PackedScene).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 _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D, logo: Texture2D) -> void: + if mi.mesh == null or shader == null: + return + var albedo := _albedo_of(mi) + var mat := ShaderMaterial.new() + mat.shader = shader + if albedo: + mat.set_shader_parameter("albedo_tex", albedo) + if mask: + mat.set_shader_parameter("region_mask", mask) + if logo: + mat.set_shader_parameter("logo_tex", logo) + mat.set_shader_parameter("logo_enabled", 1.0) + mat.set_shader_parameter("tint_0", TINT_TRIM) + mat.set_shader_parameter("tint_1", TINT_BODY) + mat.set_shader_parameter("tint_2", TINT_BELT) + mat.set_shader_parameter("tint_3", TINT_MARKS) + mat.set_shader_parameter("shadow_strength", 0.2) + mi.material_override = mat + + +func _albedo_of(mi: MeshInstance3D) -> Texture2D: + for surf in range(mi.mesh.get_surface_count()): + var m := mi.mesh.surface_get_material(surf) + if m is BaseMaterial3D: + return (m as BaseMaterial3D).albedo_texture + return null + + +func _try_load(path: String) -> Texture2D: + return load(path) as Texture2D if ResourceLoader.exists(path) else null + + +func _resolve_clip(ap: AnimationPlayer, requested: String) -> String: + for lib_name in ap.get_animation_library_list(): + var lib := ap.get_animation_library(lib_name) + for a in lib.get_animation_list(): + if str(a).to_lower().contains(requested.to_lower()): + return str(a) + return "" diff --git a/client/tools/garment_preview/preview_uniform_utility.tscn b/client/tools/garment_preview/preview_uniform_utility.tscn new file mode 100644 index 000000000..f68dd3e40 --- /dev/null +++ b/client/tools/garment_preview/preview_uniform_utility.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://c8rtprevunifrm1"] + +[ext_resource type="Script" path="res://tools/garment_preview/preview_uniform_utility.gd" id="1_prevu"] + +[node name="UniformUtilityPreview" type="Node3D"] +script = ExtResource("1_prevu") diff --git a/client/ui/meta/screens/character_creation/character_creation.gd b/client/ui/meta/screens/character_creation/character_creation.gd index ff27f3d75..c54ba5373 100644 --- a/client/ui/meta/screens/character_creation/character_creation.gd +++ b/client/ui/meta/screens/character_creation/character_creation.gd @@ -77,6 +77,14 @@ const EYEBROW_LABELS := ["Regular", "Female", "Teen", "Thick"] const CLOTHING_SLOTS := ["torso", "legs", "feet", "hands"] const CLOTHING_SLOT_LABELS := ["Torso", "Legs", "Feet", "Hands"] +# --- T-1089 wardrobe engine: multi-region tints + logo decals --- +# Region indices match the offset-shell mask channels (R/G/B/A -> tint_0..3): +# 0 = collar band, 1 = main body, 2 = sleeves, 3 = trim (generic labels). +const REGION_TINT_LABELS := ["Collar", "Body", "Sleeves", "Trim"] +const LOGO_DIR := "res://assets/characters/logos/" +const CLOTHING_DIR := "res://assets/characters/clothing/" +const DEFAULT_CLOTHING_TINT := Color(0.7, 0.65, 0.6) + # --- Accessory slots (no held_l/held_r — play-time decisions per wireframe spec) --- const ACCESSORY_SLOTS := [ "hat", @@ -236,6 +244,9 @@ var _clothing_accent_swatches: Dictionary = {} # slot -> Button var _accessory_primary_swatches: Dictionary = {} # slot -> Button var _accessory_secondary_swatches: Dictionary = {} # slot -> Button +# --- T-1089 multi-region tint + logo picker state (active clothing dock only) --- +var _clothing_logo_btns: Dictionary = {} # brand_id ("" = None) -> Button, rebuilt per dock + # --- Auto-derive flags (true = auto-derive from hair/clothing primary) --- # Note: hair highlight is always auto-derived (display-only swatch, no override). var _eyebrow_tint_auto: bool = true @@ -260,6 +271,8 @@ var _accessory_dock_container: Control = null # --- Cached asset scan results (populated once during tab build, reused by randomize) --- var _cached_hair_ids: Array = [] var _cached_head_ids: Array = [] +var _cached_logo_ids: Array = [] # brand ids scanned from LOGO_DIR (filename = brand id) +var _coverage_cache: Dictionary = {} # item_id -> coverage.json dict (UI-side, read-only) # --- Appearance sub-section search text and grid references --- var _appearance_search: Array[String] = ["", "", "", "", ""] # one per sub-section (Body=0 … Accessories=4) @@ -296,6 +309,8 @@ var _screenshot_frame_count: int = 0 var _quit_after_screenshot: bool = false var _screenshot_cardinals: bool = false var _screenshot_cardinal_idx: int = 0 +var _test_ui_tab: int = -1 # --test-config: top-level tab to open once tabs are built +var _test_appearance_sub: int = -1 # --test-config: appearance sub-section to open # resolved in _ready() — @onready path fails when instantiated as child var _tab_container: TabContainer = null @@ -346,6 +361,7 @@ func _manifest_array(key: String) -> Array: func _ready() -> void: _load_manifest() + _cached_logo_ids = _scan_logo_ids() # Build tab panel programmatically — Godot 4.6 destroys TabContainer children # defined in .tscn when the scene is instantiated as a child of another scene. @@ -418,6 +434,14 @@ func _ready() -> void: _update_cam_angle_label() _update_start_btn_state() + # --test-config UI navigation (applied after tabs exist; -1 = untouched) + if _test_ui_tab >= 0 and _tab_container != null: + _tab_container.current_tab = clampi(_test_ui_tab, 0, _tab_container.get_tab_count() - 1) + if _test_appearance_sub >= 0 and not _appearance_sub_sections.is_empty(): + _on_appearance_sub_selected( + clampi(_test_appearance_sub, 0, _appearance_sub_sections.size() - 1) + ) + # ============================================================================= # Camera management (D-158) @@ -1321,18 +1345,41 @@ func _on_clothing_slot_selected(slot: String) -> void: func _rebuild_clothing_color_dock(container: Control) -> void: for child in container.get_children(): child.queue_free() - - var row := HBoxContainer.new() - row.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - row.add_theme_constant_override("separation", 8) - container.add_child(row) + _clothing_logo_btns.clear() var item_id: String = str(_descriptor.clothing_slots.get(_active_clothing_slot, "")) + var multi_region: bool = not item_id.is_empty() and _is_multi_region(item_id) + var logo_capable: bool = not item_id.is_empty() and _is_logo_capable(item_id) + + var vbox := VBoxContainer.new() + vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + vbox.add_theme_constant_override("separation", 4) + container.add_child(vbox) + + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 8) + vbox.add_child(row) + + if multi_region: + _build_region_tint_row(row, item_id) + else: + _build_legacy_clothing_tint_row(row, item_id) + + if logo_capable: + vbox.add_child(_make_section_label("Logo")) + vbox.add_child(_build_logo_picker_row()) + + # Logo row needs extra dock height (label + button row) + container.custom_minimum_size = Vector2(0, 112 if logo_capable else 64) + + +## Legacy single-mask garments: Primary + auto-derived Secondary/Accent swatches. +func _build_legacy_clothing_tint_row(row: HBoxContainer, item_id: String) -> void: var tints: Array = ( _descriptor.clothing_tints.get(item_id, []) as Array if not item_id.is_empty() else [] as Array ) - var primary: Color = tints[0] as Color if tints.size() > 0 else Color(0.7, 0.65, 0.6) + var primary: Color = tints[0] as Color if tints.size() > 0 else DEFAULT_CLOTHING_TINT var secondary: Color = tints[1] as Color if tints.size() > 1 else _derive_secondary(primary) var accent: Color = tints[2] as Color if tints.size() > 2 else _derive_accent(primary) @@ -1351,15 +1398,104 @@ func _rebuild_clothing_color_dock(container: Control) -> void: _clothing_accent_swatches[_active_clothing_slot] = a_swatch +## Multi-region garments (coverage.json multi_region:true): one swatch per mask +## region 0-3, feeding descriptor.clothing_tints[item_id] as a 4-Color array. +func _build_region_tint_row(row: HBoxContainer, item_id: String) -> void: + # Legacy swatch refs stay null for this slot — legacy handlers can't fire here. + _clothing_primary_swatches[_active_clothing_slot] = null + _clothing_secondary_swatches[_active_clothing_slot] = null + _clothing_accent_swatches[_active_clothing_slot] = null + + var tints := _region_tints_for(item_id) + for i in REGION_TINT_LABELS.size(): + var swatch := _make_color_swatch( + tints[i], REGION_TINT_LABELS[i], _on_clothing_region_tint_changed.bind(i) + ) + row.add_child(swatch) + + +## Resolve the effective 4 region tints for an item, mirroring the compositor's +## fallback chain (t1<-t0, t2<-t1, t3<-t2) so the dock shows what the shader renders. +func _region_tints_for(item_id: String) -> Array: + var stored: Array = _descriptor.clothing_tints.get(item_id, []) as Array + var t0: Color = stored[0] as Color if stored.size() > 0 else DEFAULT_CLOTHING_TINT + var t1: Color = stored[1] as Color if stored.size() > 1 else t0 + var t2: Color = stored[2] as Color if stored.size() > 2 else t1 + var t3: Color = stored[3] as Color if stored.size() > 3 else t2 + return [t0, t1, t2, t3] + + +func _on_clothing_region_tint_changed(color: Color, region_idx: int) -> void: + var item_id: String = str(_descriptor.clothing_slots.get(_active_clothing_slot, "")) + if item_id.is_empty(): + return + # Materialize the full 4-Color array so untouched regions keep their current look. + var tints := _region_tints_for(item_id) + tints[region_idx] = color + _descriptor.clothing_tints[item_id] = tints + _refresh_preview() + + +## Logo picker row: "None" + one button per brand PNG in LOGO_DIR (filename = brand id). +func _build_logo_picker_row() -> Control: + var flow := HFlowContainer.new() + flow.add_theme_constant_override("h_separation", 4) + flow.add_theme_constant_override("v_separation", 4) + + var none_btn := _make_slot_btn("None") + none_btn.pressed.connect(_on_clothing_logo_selected.bind("")) + flow.add_child(none_btn) + _clothing_logo_btns[""] = none_btn + + for brand_id in _cached_logo_ids: + var bid := str(brand_id) + var btn := _make_slot_btn(bid.capitalize()) + btn.pressed.connect(_on_clothing_logo_selected.bind(bid)) + flow.add_child(btn) + _clothing_logo_btns[bid] = btn + + _update_clothing_logo_btns() + return flow + + +func _on_clothing_logo_selected(brand_id: String) -> void: + var item_id: String = str(_descriptor.clothing_slots.get(_active_clothing_slot, "")) + if item_id.is_empty(): + return + if brand_id.is_empty(): + _descriptor.clothing_logos.erase(item_id) + else: + _descriptor.clothing_logos[item_id] = brand_id + _update_clothing_logo_btns() + _refresh_preview() + + +func _update_clothing_logo_btns() -> void: + var item_id: String = str(_descriptor.clothing_slots.get(_active_clothing_slot, "")) + var current: String = str(_descriptor.clothing_logos.get(item_id, "")) + for brand_id: String in _clothing_logo_btns: + _set_item_selected(_clothing_logo_btns[brand_id], brand_id == current) + + func _on_clothing_item_selected(slot: String, item_id: String) -> void: var current: String = str(_descriptor.clothing_slots.get(slot, "")) if current == item_id: # Toggle off — remove from slot _descriptor.clothing_slots.erase(slot) _descriptor.clothing_tints.erase(item_id) + _descriptor.clothing_logos.erase(item_id) else: _descriptor.clothing_slots[slot] = item_id - _descriptor.clothing_tints[item_id] = [Color(0.7, 0.65, 0.6)] + if _is_multi_region(item_id): + # 4-Color array, one per mask region (collar/body/sleeves/trim) + _descriptor.clothing_tints[item_id] = [ + DEFAULT_CLOTHING_TINT, + DEFAULT_CLOTHING_TINT, + DEFAULT_CLOTHING_TINT, + DEFAULT_CLOTHING_TINT, + ] + else: + _descriptor.clothing_tints[item_id] = [DEFAULT_CLOTHING_TINT] _clothing_secondary_auto[slot] = true _clothing_accent_auto[slot] = true @@ -1682,6 +1818,26 @@ func _load_test_config() -> void: var clothing: Dictionary = cfg["clothing"] for slot: String in clothing: _descriptor.clothing_slots[slot] = clothing[slot] + if cfg.has("clothing_tints"): + # item_id -> array of [r,g,b] / [r,g,b,a] arrays (multi-region: 4 entries) + var tint_cfg: Dictionary = cfg["clothing_tints"] + for item_id: String in tint_cfg: + var colors: Array = [] + for c in tint_cfg[item_id] as Array: + var arr: Array = c + colors.append( + Color(arr[0], arr[1], arr[2], arr[3] if arr.size() > 3 else 1.0) + ) + _descriptor.clothing_tints[item_id] = colors + if cfg.has("clothing_logos"): + # item_id -> brand id (resolved to assets/characters/logos/.png) + var logo_cfg: Dictionary = cfg["clothing_logos"] + for item_id: String in logo_cfg: + _descriptor.clothing_logos[item_id] = str(logo_cfg[item_id]) + if cfg.has("ui_tab"): + _test_ui_tab = int(cfg["ui_tab"]) + if cfg.has("appearance_sub"): + _test_appearance_sub = int(cfg["appearance_sub"]) # Auto-quit after screenshot (for CI/automated testing) if cfg.has("screenshot_delay_frames"): @@ -2401,6 +2557,62 @@ func _get_clothing_ids_for_slot(slot: String) -> Array: return result +## Read (and cache) a clothing item's coverage.json — UI-side, read-only. +## Mirrors CharacterVisual._read_coverage; kept separate so the dock can resolve +## region capability before the compositor has loaded the item. +func _get_item_coverage(item_id: String) -> Dictionary: + if item_id.is_empty(): + return {} + if _coverage_cache.has(item_id): + return _coverage_cache[item_id] + var coverage: Dictionary = {} + var path := CLOTHING_DIR + "%s/coverage.json" % item_id + if FileAccess.file_exists(path): + var file := FileAccess.open(path, FileAccess.READ) + if file != null: + var parsed: Variant = JSON.parse_string(file.get_as_text()) + file.close() + if parsed is Dictionary: + coverage = parsed as Dictionary + _coverage_cache[item_id] = coverage + return coverage + + +func _is_multi_region(item_id: String) -> bool: + return bool(_get_item_coverage(item_id).get("multi_region", false)) + + +func _is_logo_capable(item_id: String) -> bool: + var clothing_data: Variant = _manifest.get("clothing", {}) + if not (clothing_data is Dictionary): + return false + var item_info: Variant = (clothing_data as Dictionary).get(item_id, {}) + if item_info is Dictionary: + return bool((item_info as Dictionary).get("logo_capable", false)) + return false + + +## Scan LOGO_DIR for brand PNGs (filename = brand id). Handles both editor +## listings (foo.png) and exported-pack listings (foo.png.import / foo.png.remap). +func _scan_logo_ids() -> Array: + var ids := {} + var dir := DirAccess.open(LOGO_DIR) + if dir == null: + return [] + dir.list_dir_begin() + var fname := dir.get_next() + while not fname.is_empty(): + if not dir.current_is_dir(): + var base := fname.trim_suffix(".import").trim_suffix(".remap") + if base.ends_with(".png"): + ids[base.trim_suffix(".png")] = true + fname = dir.get_next() + dir.list_dir_end() + var result: Array = ids.keys() + result.sort() + return result + + func _get_accessory_ids_for_slot(slot: String) -> Array: # Accessory items from manifest, filtered by slot name prefix convention. var all_ids: Array = _manifest_array("accessories") diff --git a/tooling/garment-fit/blender_author_buttondown.py b/tooling/garment-fit/blender_author_buttondown.py new file mode 100644 index 000000000..eb5fc9abe --- /dev/null +++ b/tooling/garment-fit/blender_author_buttondown.py @@ -0,0 +1,426 @@ +""" +blender_author_buttondown.py (T-1089, buttondown_modern — per-body offset shell) + +Button-down shirt authored per body via the offset-shell route. Imports +blender_author_offset_shell.py as a module and reuses its scene/build/offset/ +solidify/logo-UV/export helpers; this file adds what the button-down needs +beyond the t-shirt base: + + * long-sleeve coverage — torso + torso_upper + BOTH full arms (upper+lower), + with a wrist-plane cut derived from the lowerarm bone (SLEEVE_END_FRAC), + replacing the base script's upper-arm short-sleeve cut + * seam WELD after join — the body segments share exact duplicated boundary + rings (probe: 24-38 coincident verts/seam, zero near-misses), so a + remove-doubles pass gives continuous normals across segment seams -> + crack-free outward offset and no internal solidify rims (UVs live on + loops, so UV seams survive the weld; weights are identical by construction) + * per-PIXEL region mask bake (barycentric-interpolated body-local positions) + instead of the base per-face classification — the placket and cuff + boundaries cut through the middle of low-poly faces + * PAINTED albedo — texture-carried identity per the style pin: placket band + with stitch lines, button dots, collar + cuff seam lines, over the flat + toon fabric noise. Baked per body because each body archetype has its own + UV atlas (the shell inherits body UVs). + +Region convention (spec): collar=R (tint_0), body=G (tint_1), +cuffs+placket=B (tint_2). A unused. Logo-capable OFF, but the UV2 chest +channel is still authored (costs nothing; enables future brand variants). + +Run (per-body only — this garment ships the per-body route): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_buttondown.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/buttondown_modern \ + [--bodies average_m,child,...] [--offset 0.009] + +Writes per body: /.glb + /_mask.png +Plus: /base_albedo.png (average_m's painted albedo) + /reference_mask.png (copy of average_m_mask.png) + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060 +(per-body authoring for offset shells). +""" + +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import blender_author_offset_shell as base # noqa: E402 + +# -------------------------------------------------------------------------- +# Garment parameters (average_m metres; scaled per body by shoulder ratio) +# -------------------------------------------------------------------------- + +COVERED_SEGMENTS = [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", +] + +OFFSET_M = 0.009 # slimmer standoff than the tee (12 mm) — dress shirt +CLOTH_THICKNESS_M = 0.003 # thinner cloth than the tee (4 mm) +SLEEVE_END_FRAC = 0.92 # of lowerarm bone length — long sleeve, ends above wrist +CUFF_START_FRAC = 0.62 # of lowerarm bone length — last ~38% of forearm = cuff (B) +WELD_DIST = 0.0002 # merge duplicated segment-boundary rings (0.2 mm) + +MASK_SIZE = 512 +ALBEDO_SIZE = 1024 # painted detail (buttons ~8 px radius) needs 1024 + +# Style dimensions on average_m (shoulder |x| = base._REF_SHOULDER_X); +# scaled per body by shoulder_x ratio so proportions hold from child to heavy_m. +PLACKET_HALF_M = 0.022 # placket half-width (4.4 cm total band) +STITCH_W_M = 0.0025 # stitch/seam line half-width +BUTTON_R_M = 0.0095 # button disc radius (chunky toon read) +SEAM_HALF_M = 0.0022 # collar seam line half-height +BUTTON_COUNT = 6 +FRONT_MIN_Y = 0.004 # body-local front test: y * FRONT_Y_SIGN > this + +# Flat toon oxford base tone + painted feature colours (luma carries through +# the luminance-preserving region tint in toon_garment.gdshader). +FABRIC_RGB = (0.63, 0.64, 0.66) +FABRIC_NOISE = 0.02 +PLACKET_BAND_MUL = 1.07 # subtle lift so the band reads under any tint +STITCH_MUL = 0.52 # dark stitch lines +SEAM_MUL = 0.55 # collar/cuff seam lines +BUTTON_RGB_OUTER = (0.10, 0.10, 0.12) +BUTTON_RGB_CORE = (0.24, 0.24, 0.27) + + +def log(msg): + print(f"[buttondown] {msg}") + + +# -------------------------------------------------------------------------- +# Threshold derivation (extends base.derive_thresholds with arm + style dims) +# -------------------------------------------------------------------------- + +def derive_arm_thresholds(armature): + """Wrist cut planes + cuff start from the lowerarm bones (X-axis arms).""" + bones = armature.data.bones + la_l = bones.get("lowerarm_l") + la_r = bones.get("lowerarm_r") + if la_l is None or la_r is None: + raise RuntimeError("lowerarm bones missing — cannot derive sleeve cut") + + def along(b, frac): + return b.head_local.x + frac * (b.tail_local.x - b.head_local.x) + + cut_l = along(la_l, SLEEVE_END_FRAC) # left arm +x: delete x > cut_l + cut_r = along(la_r, SLEEVE_END_FRAC) # right arm -x: delete x < cut_r + cuff_x_abs = (abs(along(la_l, CUFF_START_FRAC)) + + abs(along(la_r, CUFF_START_FRAC))) / 2.0 + log(f"sleeve: cut_l={cut_l:.3f} cut_r={cut_r:.3f} cuff |x|>={cuff_x_abs:.3f}") + return {"cut_l": cut_l, "cut_r": cut_r, "cuff_x_abs": cuff_x_abs} + + +def derive_style(armature): + """Scale the painted-detail dimensions by this body's shoulder ratio.""" + bones = armature.data.bones + ua_l = bones.get("upperarm_l") + ua_r = bones.get("upperarm_r") + if ua_l is None or ua_r is None: + s = 1.0 + else: + shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + s = shoulder_x / base._REF_SHOULDER_X + log(f"style scale {s:.3f}") + return { + "placket_half": PLACKET_HALF_M * s, + "stitch_w": STITCH_W_M * s, + "button_r": BUTTON_R_M * s, + "seam_half": SEAM_HALF_M * s, + } + + +def derive_buttons(shell, thr): + """Button Z positions: evenly spaced down the placket (collar -> hem).""" + hem_z = min(v.co.z for v in shell.data.vertices) + collar_z = thr["collar_z_min"] + span = collar_z - hem_z + z_top = collar_z - 0.05 * span + z_bot = hem_z + 0.07 * span + zs = list(np.linspace(z_top, z_bot, BUTTON_COUNT)) + log(f"buttons: {BUTTON_COUNT} @ z {z_bot:.3f}..{z_top:.3f} (hem {hem_z:.3f})") + return {"button_zs": zs, "hem_z": hem_z} + + +# -------------------------------------------------------------------------- +# Geometry: seam weld + wrist cut +# -------------------------------------------------------------------------- + +def weld_seams(shell): + """Merge the duplicated segment-boundary rings into one continuous shell.""" + bm = bmesh.new() + bm.from_mesh(shell.data) + before = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=WELD_DIST) + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"seam weld: {before} -> {len(shell.data.vertices)} verts " + f"({before - len(shell.data.vertices)} merged)") + + +def wrist_cut(shell, thr): + """Delete sleeve verts beyond the wrist plane on each forearm.""" + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + to_delete = [v for v in bm.verts + if v.co.x > thr["cut_l"] or v.co.x < thr["cut_r"]] + bmesh.ops.delete(bm, geom=to_delete, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"wrist cut removed {len(to_delete)} verts; " + f"{len(shell.data.vertices)} remain") + + +# -------------------------------------------------------------------------- +# Per-pixel bake: region mask + painted albedo in one pass +# -------------------------------------------------------------------------- + +def _gather_tris(shell): + """Fan-triangulate every face into (uv_a, uv_b, uv_c, co_a, co_b, co_c).""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + uvl = bm.loops.layers.uv[me.uv_layers[0].name] # UV0 = shared body atlas + tris = [] + for face in bm.faces: + loops = face.loops[:] + uvs = [np.array((lp[uvl].uv.x, lp[uvl].uv.y)) for lp in loops] + cos = [np.array(lp.vert.co[:]) for lp in loops] + for i in range(1, len(loops) - 1): + tris.append((uvs[0], uvs[i], uvs[i + 1], + cos[0], cos[i], cos[i + 1])) + bm.free() + return tris + + +def _tri_cover(a, b, c, W, H): + """Pixel centres covered by UV triangle abc -> (ys, xs, w0, w1, w2).""" + ax, ay = a[0] * (W - 1), a[1] * (H - 1) + bx, by = b[0] * (W - 1), b[1] * (H - 1) + cx, cy = c[0] * (W - 1), c[1] * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return None + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return None # degenerate (solidify rim) — no UV area to write + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + px = xs + 0.5 + py = ys + 0.5 + w0 = ((by - cy) * (px - cx) + (cx - bx) * (py - cy)) / denom + w1 = ((cy - ay) * (px - cx) + (ax - cx) * (py - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return None + return (ys[inside].ravel(), xs[inside].ravel(), + w0[inside].ravel(), w1[inside].ravel(), w2[inside].ravel()) + + +def _interp_pos(cover, co_a, co_b, co_c): + _, _, w0, w1, w2 = cover + return (w0[:, None] * co_a[None, :] + + w1[:, None] * co_b[None, :] + + w2[:, None] * co_c[None, :]) + + +def _classify_px(pos, thr): + """Vectorized region classification -> (N,4) one-hot RGBA rows.""" + x, y, z = pos[:, 0], pos[:, 1], pos[:, 2] + ax = np.abs(x) + front = y * base.FRONT_Y_SIGN > FRONT_MIN_Y + cuff = ax >= thr["cuff_x_abs"] + collar = (~cuff) & (z >= thr["collar_z_min"]) & (ax < thr["collar_x_abs"]) + placket = ((~cuff) & (~collar) & front + & (ax <= thr["placket_half"]) & (z < thr["collar_z_min"])) + rgba = np.zeros((len(x), 4), dtype=np.float32) + rgba[:, 1] = 1.0 # default: body -> G + rgba[collar] = (1.0, 0.0, 0.0, 0.0) # collar band -> R + rgba[cuff | placket] = (0.0, 0.0, 1.0, 0.0) # cuffs + placket -> B + return rgba + + +def _paint_px(pos, rows, thr): + """Painted albedo: placket band + stitches, seams, button dots (in-place).""" + x, y, z = pos[:, 0], pos[:, 1], pos[:, 2] + ax = np.abs(x) + front = y * base.FRONT_Y_SIGN > FRONT_MIN_Y + below_collar = z < thr["collar_z_min"] + + mul = np.ones(len(x), dtype=np.float32) + band = front & below_collar & (ax <= thr["placket_half"]) + mul[band] = PLACKET_BAND_MUL + stitch = front & below_collar & (np.abs(ax - thr["placket_half"]) + <= thr["stitch_w"]) + mul[stitch] = STITCH_MUL + collar_seam = ((np.abs(z - thr["collar_z_min"]) <= thr["seam_half"]) + & (ax < thr["collar_x_abs"] * 1.8)) + mul[collar_seam] = SEAM_MUL + cuff_seam = np.abs(ax - thr["cuff_x_abs"]) <= thr["stitch_w"] + mul[cuff_seam] = SEAM_MUL + out = rows * mul[:, None] + + r = thr["button_r"] + for bz in thr["button_zs"]: + d2 = x * x + (z - bz) ** 2 + disc = front & (d2 <= r * r) + out[disc] = BUTTON_RGB_OUTER + core = front & (d2 <= (0.45 * r) ** 2) + out[core] = BUTTON_RGB_CORE + np.clip(out, 0.0, 1.0, out) + return out + + +def bake_maps(shell, thr, mask_path): + """One pass over the shell: bake _mask.png + return painted albedo.""" + tris = _gather_tris(shell) + + mbuf = np.zeros((MASK_SIZE, MASK_SIZE, 4), dtype=np.float32) + mbuf[:, :, 1] = 1.0 # green background = main body (bilinear-bleed safe) + + rng = np.random.default_rng(1090) # deterministic fabric noise + fabric = np.array(FABRIC_RGB, dtype=np.float32) + noise = (rng.random((ALBEDO_SIZE, ALBEDO_SIZE, 1), dtype=np.float32) + - 0.5) * 2.0 * FABRIC_NOISE + abuf = np.clip(fabric[None, None, :] + noise, 0.0, 1.0) + + for uv_a, uv_b, uv_c, co_a, co_b, co_c in tris: + cover = _tri_cover(uv_a, uv_b, uv_c, MASK_SIZE, MASK_SIZE) + if cover is not None: + pos = _interp_pos(cover, co_a, co_b, co_c) + mbuf[cover[0], cover[1], :] = _classify_px(pos, thr) + cover = _tri_cover(uv_a, uv_b, uv_c, ALBEDO_SIZE, ALBEDO_SIZE) + if cover is not None: + pos = _interp_pos(cover, co_a, co_b, co_c) + abuf[cover[0], cover[1], :] = _paint_px( + pos, abuf[cover[0], cover[1], :], thr) + + tot = MASK_SIZE * MASK_SIZE + log("mask texels: collar={:.1f}% body={:.1f}% cuff+placket={:.1f}%".format( + 100.0 * float((mbuf[:, :, 0] > 0.5).sum()) / tot, + 100.0 * float((mbuf[:, :, 1] > 0.5).sum()) / tot, + 100.0 * float((mbuf[:, :, 2] > 0.5).sum()) / tot)) + + mask_img = bpy.data.images.new("garment_region_mask", MASK_SIZE, MASK_SIZE, + alpha=True) + mask_img.pixels.foreach_set(mbuf.reshape(-1)) + mask_img.update() + mask_img.filepath_raw = mask_path + mask_img.file_format = 'PNG' + mask_img.save() + log(f"baked region mask -> {mask_path}") + + argba = np.concatenate( + [abuf, np.ones((ALBEDO_SIZE, ALBEDO_SIZE, 1), dtype=np.float32)], + axis=2) + albedo_img = bpy.data.images.new("base_albedo", ALBEDO_SIZE, ALBEDO_SIZE, + alpha=False) + albedo_img.pixels.foreach_set(argba.reshape(-1)) + albedo_img.update() + return albedo_img + + +# -------------------------------------------------------------------------- +# Author one body +# -------------------------------------------------------------------------- + +def author_buttondown(body_dir, out_dir, body, offset): + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS # long-sleeve coverage set + shell, armature = base.build_covered_mesh(body_dir) + + thr = base.derive_thresholds(armature) # collar/chest from bone landmarks + thr.update(derive_arm_thresholds(armature)) + thr.update(derive_style(armature)) + + weld_seams(shell) + wrist_cut(shell, thr) + thr.update(derive_buttons(shell, thr)) + + base.offset_outward(shell, offset) + base.solidify(shell, CLOTH_THICKNESS_M) + + base.author_logo_uv(shell, thr) # UV2 before bake (mask/albedo use UV0) + albedo_img = bake_maps(shell, thr, os.path.join(out_dir, f"{body}_mask.png")) + base.save_albedo_sidecar(albedo_img, os.path.join(out_dir, "base_albedo.png")) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +# -------------------------------------------------------------------------- +# Entry +# -------------------------------------------------------------------------- + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- " + "[--bodies a,b,c] [--offset M]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"per-body mode: {len(bodies)} bodies, offset {offset*1000:.1f} mm") + + avg_albedo_stash = os.path.join(out_dir, "_tmp_avg_base_albedo.png") + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_buttondown(body_dir, out_dir, body, offset) + if body == base.REFERENCE_BODY: + shutil.copy2(os.path.join(out_dir, "base_albedo.png"), + avg_albedo_stash) + results.append((body, "ok")) + except Exception as exc: # noqa: BLE001 — per-body isolation + log(f"ERROR {body}: {exc}") + results.append((body, f"error: {exc}")) + + # Root sidecars: reference mask + albedo mirror the reference body. + ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {base.REFERENCE_BODY}_mask.png -> reference_mask.png") + if os.path.isfile(avg_albedo_stash): + shutil.move(avg_albedo_stash, os.path.join(out_dir, "base_albedo.png")) + log(f"base_albedo.png = {base.REFERENCE_BODY}'s painted albedo") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_denim_pants.py b/tooling/garment-fit/blender_author_denim_pants.py new file mode 100644 index 000000000..b7a5cd8d8 --- /dev/null +++ b/tooling/garment-fit/blender_author_denim_pants.py @@ -0,0 +1,659 @@ +""" +blender_author_denim_pants.py (T-1089, jeans_modern + denim pants family) + +Authors full-length DENIM pants as per-body offset shells, reusing +blender_author_offset_shell.py as a library (scene build, join, offset, +solidify, GLB export). Sibling companions already cover the plain lower-body +family (blender_author_offset_shell_legs.py — shorts, 2-region; +blender_author_lower_shell.py — formal, crease lines); this one adds what +denim needs and they don't provide, as reusable parameters: + + * TEXEL-level feature painting: every UV0 triangle is rasterized with + barycentric-interpolated 3D positions, and an analytic denim feature + field is evaluated per texel. ONE field evaluation drives BOTH outputs, + so the painted albedo and the region mask always agree: + - albedo: painted seam thread lines (outseam/inseam azimuth around the + per-z leg axis, centre-front fly, back yoke V), front pocket + arcs, back pocket outlines, belt loops, waist button, + waist/cuff border stitching — flat toon-friendly, identity + carried by the texture (style pin: modern only). + - mask: waistband -> R, legs -> G, seams + cuff band -> B + (spec: waistband=R, legs=G, seams/cuffs=B). Seam LINES are + in the B channel, not just the cuff band, so contrast-thread + recoloring works (toon_garment.gdshader channel-blends). + * boundary WELD before offsetting — the waist join (hips<->leg_upper) and + knee join (leg_upper<->leg_lower) carry duplicated boundary-ring verts + per segment; offsetting un-welded rings along diverging normals opens + cracks, so coincident verts are merged first (weights identical by + origin, so skinning is unaffected). + * --hem-frac: fraction of the hip->ankle span covered, measured up from the + ankle (1.0 = full length to the ankle = jeans; lower values give cropped + variants; cut rims are capped by Solidify(use_rim) like the base sleeves). + * a parked logo_uv TEXCOORD_1 layer (all UVs at (2,2)): pants are not + logo-capable, but toon_garment.gdshader samples UV2 unconditionally, so + every multi_region garment ships a well-defined logo channel. + * open-rim FLATTENING: the segment splitter cuts along weight thresholds, + so the waist and ankle boundary rings are jagged "teeth" (~5-6 cm deep on + average_m); boundary verts are pulled onto clean planes (waist ring down + to its own valley, ankle rings onto the ankle-joint plane) pre-offset. + * --waist-flare: extra feathered radial stand-off at the waistband rim — + deep-crouch waist-fold clip mitigation (QA evidence, peasant + jeans). + +Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r. Natural +boundaries give the waist opening (seg_hips top rim) and the ankle hems +(seg_leg_lower bottom) for free. + +All cut/mask/paint parameters derive PER BODY from that body's own bone +landmarks (thigh_l/calf_l line) and measured mesh extents (waist rim, ankle +rim, crotch = seg_hips lowest ring), scaled by the body's garment span and +hip half-width — the same proportional-ratio philosophy as +base.derive_thresholds, so the denim details stay consistent across all 11 +bodies. Per-body mode only (offset shells author per body, Q-060). + +Usage (jeans_modern reference invocation): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_denim_pants.py -- \ + client/assets/characters/bodies \ + client/assets/characters/clothing/jeans_modern \ + [--bodies average_m,child,...] [--offset 0.012] [--hem-frac 1.0] \ + [--waist-flare 0.007] [--base-rgb 0.24,0.32,0.45] [--plain] + +Writes per body: /.glb (skinned, albedo embedded) + /_mask.png (RGBA region mask, UV0) + /_base_albedo.png +Plus: /base_albedo.png (average_m's, shared sidecar) + /reference_mask.png (average_m's, runtime fallback) + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house +wardrobe), Q-060 (per-body offset shells). +""" + +import importlib.util +import os +import shutil +import sys + +import bmesh +import bpy +import numpy as np + +# -------------------------------------------------------------------------- +# Import the base offset-shell module (shared machinery) +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_spec = importlib.util.spec_from_file_location( + "offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py")) +base = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(base) + +log = base.log +FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base) + +# -------------------------------------------------------------------------- +# Parameters +# -------------------------------------------------------------------------- +COVERED_SEGMENTS = [ + "seg_hips", + "seg_leg_upper_l", "seg_leg_upper_r", + "seg_leg_lower_l", "seg_leg_lower_r", +] + +HEM_FRAC = 1.0 # 1.0 = full leg to the ankle (jeans) +WELD_DIST = 5e-4 # boundary-ring weld tolerance (0.5 mm) +TEX_SIZE = 1024 # albedo + mask resolution (painted seams need >512) +WAIST_FLARE_M = 0.007 # extra radial stand-off at the waistband rim + # (deep-crouch waist-fold mitigation, feathered) + +# Vertical proportions — fractions of the garment span (waist_z - hem_z). +BAND_FRAC = 0.055 # waistband height (R region) +CUFF_FRAC = 0.032 # cuff band height (B region) +SEAM_W_FRAC = 0.0095 # painted seam line width +YOKE_DROP_FRAC = 0.055 # back yoke centre depth below the waistband +YOKE_RISE_FRAC = 0.030 # yoke V rise toward the sides +BPOCKET_TOP_FRAC = 0.075 # back pocket top below the waistband +BPOCKET_HH_FRAC = 0.048 # back pocket half-height +FPOCKET_RZ_FRAC = 0.14 # front pocket arc vertical radius + +# Horizontal proportions — fractions of the hip half-width (|thigh head x|). +BPOCKET_CX_FRAC = 0.80 # back pocket centre +BPOCKET_HW_FRAC = 0.52 # back pocket half-width +FPOCKET_CX_FRAC = 1.30 # front pocket arc centre (near the outseam corner) +FPOCKET_RX_FRAC = 0.75 # front pocket arc horizontal radius +LOOP_X_FRACS = (0.55, 1.30) # belt-loop |x| positions (front + back pairs) +LOOP_W_M = 0.016 # belt loop width (m, scaled by hip width) + +# Denim style (sRGB floats; saved as-is — matches the proven base pipeline). +DENIM_RGB = (0.240, 0.320, 0.450) # indigo denim +THREAD_RGB = (0.800, 0.620, 0.340) # contrast stitching thread +BUTTON_RGB = (0.850, 0.720, 0.480) # waist button metal +ALBEDO_NOISE = 0.020 # +/- woven jitter +CUFF_SHADE = 0.90 # cuff band albedo darkening +LOOP_SHADE = 0.80 # belt-loop albedo darkening +PLAIN = False # --plain: skip thread/pocket/button paint +NOISE_SEED = 2089 + +# Reference proportions (average_m) the fractions were calibrated against. +_REF_SPAN = 1.007 # waist rim z (1.093) - ankle z (0.086) +_REF_HIP_X = 0.0906 # |thigh_l head x| + + +# -------------------------------------------------------------------------- +# Per-body landmarks +# -------------------------------------------------------------------------- + +class LegLandmarks: + """Cut/mask/paint parameters derived from one body's bones + mesh.""" + + def __init__(self, armature, waist_z, ankle_z, crotch_z): + bones = armature.data.bones + thigh = bones.get("thigh_l") + calf = bones.get("calf_l") + if thigh is None or calf is None: + raise RuntimeError("thigh_l/calf_l missing — not the 65-bone rig?") + self.waist_z = waist_z + self.ankle_z = ankle_z + self.crotch_z = crotch_z + self.hip_x = abs(thigh.head_local.x) + # Leg axis control points (z-increasing) for azimuth seam placement. + # x values are the +x (left) leg; the right leg mirrors via sign. + self.leg_z_pts = np.array( + [calf.tail_local.z, calf.head_local.z, thigh.head_local.z]) + self.leg_x_pts = np.array( + [abs(calf.tail_local.x), abs(calf.head_local.x), + abs(thigh.head_local.x)]) + self.leg_y_pts = np.array( + [calf.tail_local.y, calf.head_local.y, thigh.head_local.y]) + self.hem_z = ankle_z # finalized after the hem cut + + def finalize(self, hem_z): + self.hem_z = hem_z + self.span = self.waist_z - self.hem_z + self.sh = self.hip_x / _REF_HIP_X + self.band_h = BAND_FRAC * self.span + self.cuff_h = CUFF_FRAC * self.span + self.seam_w = SEAM_W_FRAC * self.span + self.band_z = self.waist_z - self.band_h + self.cuff_top = self.hem_z + self.cuff_h + log(f"landmarks: waist={self.waist_z:.3f} hem={self.hem_z:.3f} " + f"crotch={self.crotch_z:.3f} hip_x={self.hip_x:.3f} " + f"band_z={self.band_z:.3f} cuff_top={self.cuff_top:.3f} " + f"seam_w={self.seam_w * 1000:.1f}mm") + + +def probe_hips_bounds(body_dir): + """Import seg_hips alone to measure the crotch (its lowest ring) exactly.""" + base.clear_scene() + objs = base.import_glb(os.path.join(body_dir, "seg_hips.glb")) + zs = [] + for o in objs: + if base.is_body_mesh(o): + zs.extend(v.co.z for v in o.data.vertices) + if not zs: + raise RuntimeError("seg_hips.glb yielded no skinned mesh") + return min(zs), max(zs) + + +# -------------------------------------------------------------------------- +# Geometry: weld + hem cut +# -------------------------------------------------------------------------- + +def weld_boundaries(shell): + """Merge coincident segment-boundary verts so the offset can't open cracks.""" + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + before = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=WELD_DIST) + merged = before - len(bm.verts) + bm.to_mesh(me) + bm.free() + me.update() + log(f"welded segment boundaries: {merged} verts merged " + f"({before} -> {len(me.vertices)})") + + +def flatten_open_rims(shell, ankle_plane): + """Pull the open boundary rings onto clean planes (pre-offset). + + The body segment splitter cuts along weight thresholds, not edge loops, so + the seg_hips top edge and the seg_leg_lower ankle edges are jagged rings + of "teeth" (~5-6 cm on average_m). On the skin this hides under the + neighbouring segment; on a garment shell it becomes a ragged silhouette. + + The top ring's verts are pulled DOWN to the ring's own valley (deepest + notch) — a clean straight waist edge without inventing coverage. The + bottom rings' verts are moved onto `ankle_plane` (the ankle joint for + full-length pants, or the hem-cut plane for cropped variants) — a clean + straight hem AT the ankle. Only boundary verts move; interior verts are + untouched, and weights/UVs ride along, so skinning and painting are + unaffected. Returns (waist_plane, ankle_plane). + """ + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + boundary = set() + for e in bm.edges: + if len(e.link_faces) == 1: # open boundary + boundary.update(v.index for v in e.verts) + zs = [bm.verts[i].co.z for i in boundary] + z_mid = (min(zs) + max(zs)) / 2.0 + top = [i for i in boundary if bm.verts[i].co.z > z_mid] + bottom = [i for i in boundary if bm.verts[i].co.z <= z_mid] + waist_plane = min(bm.verts[i].co.z for i in top) + teeth_top = max(bm.verts[i].co.z for i in top) - waist_plane + teeth_bot_lo = min(bm.verts[i].co.z for i in bottom) + teeth_bot_hi = max(bm.verts[i].co.z for i in bottom) + for i in top: + bm.verts[i].co.z = waist_plane + for i in bottom: + bm.verts[i].co.z = ankle_plane + bm.to_mesh(me) + bm.free() + me.update() + log(f"flattened open rims: waist ring ({len(top)} verts, teeth " + f"{teeth_top:.3f} m) -> {waist_plane:.3f}; ankle rings " + f"({len(bottom)} verts, z {teeth_bot_lo:.3f}..{teeth_bot_hi:.3f}) " + f"-> {ankle_plane:.3f}") + return waist_plane, ankle_plane + + +def waist_flare(shell, waist_plane, band_h, flare): + """Extra RADIAL stand-off at the waistband rim, feathered over 2x the band + height (pre-solidify). QA evidence: in deep crouch the lower-back skin + folds over the waist rim (the peasant set's known 'deep-crouch waist gap', + worst on small bodies). Flaring the band outward gives the fold room and + reads as a natural jeans waistband stand-off.""" + if flare <= 0.0: + return + z0 = waist_plane - 2.0 * band_h + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.normal_update() + n = 0 + for v in bm.verts: + if v.co.z > z0: + t = min((v.co.z - z0) / (2.0 * band_h), 1.0) + nx, ny = v.normal.x, v.normal.y + mag = (nx * nx + ny * ny) ** 0.5 + if mag > 1e-6: + v.co.x += flare * t * nx / mag + v.co.y += flare * t * ny / mag + n += 1 + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"waist flare: {n} verts, +{flare * 1000:.1f} mm radial at rim " + f"(feathered from z={z0:.3f})") + + +def clamp_waist_residue(shell, waist_plane): + """Post-solidify safety clamp: any interior tooth verts still above the + waist plane (multi-triangle teeth) get squashed onto it.""" + me = shell.data + n = 0 + for v in me.vertices: + if v.co.z > waist_plane: + v.co.z = waist_plane + n += 1 + me.update() + if n: + log(f"clamped {n} residual waist verts -> {waist_plane:.3f}") + + +def hem_cut(shell, lm, hem_frac): + """Trim the legs below the hem plane. 1.0 keeps the full ankle length.""" + if hem_frac >= 0.999: + return lm.ankle_z + hip_z = float(lm.leg_z_pts[-1]) + hem_z = lm.ankle_z + (1.0 - hem_frac) * (hip_z - lm.ankle_z) + bm = bmesh.new() + bm.from_mesh(shell.data) + doomed = [v for v in bm.verts if v.co.z < hem_z] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"hem cut at z={hem_z:.3f}: removed {len(doomed)} verts") + return hem_z + + +# -------------------------------------------------------------------------- +# Denim feature field (texel-level; drives albedo AND mask together) +# -------------------------------------------------------------------------- + +def _seam_field(px, py, pz, lm): + """Evaluate denim features at texel 3D positions (numpy arrays). + + Returns bool arrays (seams, thread, in_cuff, in_band, front): + `seams` feeds the mask B channel; `thread` is every painted stitch line. + """ + w2 = lm.seam_w * 0.5 + front = py * FRONT_Y_SIGN > 0.004 + backside = py * FRONT_Y_SIGN < -0.004 + in_band = pz >= lm.band_z + in_cuff = pz <= lm.cuff_top + mid = (~in_band) & (~in_cuff) + + # Per-z leg axis, mirrored by x sign. + side = np.where(px >= 0.0, 1.0, -1.0) + cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts) + cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts) + dx = px - cx + dy = py - cy + r = np.hypot(dx, dy) + 1e-9 + + # Outseam: azimuth toward the outer (+/-x) direction; constant metric + # width via arc distance. Inseam: inner direction, below the crotch only. + arc_out = np.arccos(np.clip(dx * side / r, -1.0, 1.0)) * r + outseam = mid & (arc_out < w2) + arc_in = np.arccos(np.clip(-dx * side / r, -1.0, 1.0)) * r + inseam = mid & (arc_in < w2) & (pz < lm.crotch_z - 0.01 * lm.span / _REF_SPAN) + + # Centre-front fly stitch (slightly off-centre, classic J-front). + fly = front & mid & (np.abs(px - 0.012 * lm.sh) < w2) \ + & (pz > lm.crotch_z + 0.015 * lm.span / _REF_SPAN) + + # Back yoke: shallow V, higher toward the sides. + yoke_z = (lm.band_z - YOKE_DROP_FRAC * lm.span + + YOKE_RISE_FRAC * lm.span + * np.minimum(np.abs(px) / (1.5 * lm.hip_x), 1.0)) + yoke = backside & mid & (np.abs(pz - yoke_z) < w2) \ + & (np.abs(px) < 1.6 * lm.hip_x) + + seams = outseam | inseam | fly | yoke + + # Stitch-only lines (albedo, not mask): waist + cuff border stitching. + wstitch = np.abs(pz - lm.band_z) < w2 + cstitch = np.abs(pz - lm.cuff_top) < w2 + + # Back pocket outlines (rectangle rings on the seat). + bhw = BPOCKET_HW_FRAC * lm.hip_x + bhh = BPOCKET_HH_FRAC * lm.span + bcz = lm.band_z - BPOCKET_TOP_FRAC * lm.span - bhh + adx = np.abs(np.abs(px) - BPOCKET_CX_FRAC * lm.hip_x) + adz = np.abs(pz - bcz) + bpocket = backside & (adx < bhw + w2) & (adz < bhh + w2) \ + & ~((adx < bhw - w2) & (adz < bhh - w2)) + + # Front pocket arcs (quarter-ellipse from waistband toward the outseam). + fcx = FPOCKET_CX_FRAC * lm.hip_x + frx = FPOCKET_RX_FRAC * lm.hip_x + frz = FPOCKET_RZ_FRAC * lm.span + ex = (np.abs(px) - fcx) / frx + ez = (pz - lm.band_z) / frz + fdist = (np.sqrt(ex * ex + ez * ez) - 1.0) * (0.5 * (frx + frz)) + fpocket = front & (np.abs(fdist) < w2) & (pz <= lm.band_z) \ + & (np.abs(px) <= fcx) + + thread = seams | wstitch | cstitch | bpocket | fpocket + return seams, thread, in_cuff, in_band, front + + +def _paint_texels(px, py, pz, noise, lm): + """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" + n = px.shape[0] + seams, thread, in_cuff, in_band, front = _seam_field(px, py, pz, lm) + + # --- albedo ------------------------------------------------------------ + alb = np.empty((n, 4), dtype=np.float32) + for c in range(3): + alb[:, c] = DENIM_RGB[c] + noise + alb[:, 3] = 1.0 + alb[in_cuff, :3] *= CUFF_SHADE + + if not PLAIN: + # Belt loops: darkened denim bands on the waistband. + loop_w = LOOP_W_M * lm.sh + loops = np.zeros(n, dtype=bool) + for fx in LOOP_X_FRACS: + loops |= np.abs(np.abs(px) - fx * lm.hip_x) < loop_w * 0.5 + loops |= (~front) & (np.abs(px) < loop_w * 0.5) # centre-back loop + loops &= in_band + alb[loops, :3] *= LOOP_SHADE + + alb[thread, 0] = THREAD_RGB[0] + alb[thread, 1] = THREAD_RGB[1] + alb[thread, 2] = THREAD_RGB[2] + + # Waist button (front, mid-band). + btn = front & (np.hypot(px, pz - (lm.band_z + 0.5 * lm.band_h)) + < 0.009 * lm.sh) + alb[btn, 0] = BUTTON_RGB[0] + alb[btn, 1] = BUTTON_RGB[1] + alb[btn, 2] = BUTTON_RGB[2] + + # --- region mask: waistband R / legs G / seams+cuffs B ------------------ + mask = np.zeros((n, 4), dtype=np.float32) + is_b = (in_cuff | seams) & ~in_band + is_g = ~(in_band | is_b) + mask[in_band, 0] = 1.0 + mask[is_b, 2] = 1.0 + mask[is_g, 1] = 1.0 + return alb, mask + + +def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H): + """Barycentric texel fill of one UV triangle: interpolate 3D positions, + evaluate the denim field, write albedo + mask together.""" + a, b, c = uvs + A, B, C = cos + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + pxg = xs + 0.5 + pyg = ys + 0.5 + w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom + w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + px3 = w0i * A.x + w1i * B.x + w2i * C.x + py3 = w0i * A.y + w1i * B.y + w2i * C.y + pz3 = w0i * A.z + w1i * B.z + w2i * C.z + ysin = ys[inside] + xsin = xs[inside] + alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm) + alb_buf[ysin, xsin] = alb + mask_buf[ysin, xsin] = mask + + +def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body): + """Rasterize all UV0 triangles once, producing the painted albedo and the + region mask from one shared feature-field evaluation per texel.""" + W = H = TEX_SIZE + rng = np.random.default_rng(NOISE_SEED) + noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5) + * 2.0 * ALBEDO_NOISE) + + alb_buf = np.empty((H, W, 4), dtype=np.float32) + for c in range(3): + alb_buf[:, :, c] = DENIM_RGB[c] + noise_buf + alb_buf[:, :, 3] = 1.0 + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 # background = legs green (bleed-safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + if not len(bm.loops.layers.uv): + raise RuntimeError("no UV layer for albedo/mask paint") + uv_layer = bm.loops.layers.uv[0] + + tri_count = 0 + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + cos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_paint( + alb_buf, mask_buf, noise_buf, + (uvs[0], uvs[i], uvs[i + 1]), + (cos[0], cos[i], cos[i + 1]), + lm, W, H) + tri_count += 1 + bm.free() + log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})") + + def _save(buf, name, path): + img = bpy.data.images.new(name, W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = path + img.file_format = 'PNG' + img.save() + return img + + albedo_img = _save(alb_buf, f"denim_albedo_{body}", albedo_path) + _save(mask_buf, f"denim_mask_{body}", mask_path) + log(f"saved albedo -> {albedo_path}") + log(f"saved mask -> {mask_path}") + return albedo_img + + +# -------------------------------------------------------------------------- +# Parked logo UV2 (pants are not logo-capable; the shader still samples UV2) +# -------------------------------------------------------------------------- + +def author_parked_uv2(shell): + me = shell.data + while len(me.uv_layers) > 1: + me.uv_layers.remove(me.uv_layers[-1]) + me.uv_layers.new(name="logo_uv") + me.uv_layers.active = me.uv_layers[0] + bm = bmesh.new() + bm.from_mesh(me) + uvl = bm.loops.layers.uv.get("logo_uv") + for face in bm.faces: + for loop in face.loops: + loop[uvl].uv = (2.0, 2.0) + bm.to_mesh(me) + bm.free() + me.update() + log("logo UV2 authored fully parked (not logo-capable)") + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_denim_shell(body_dir, out_dir, body, offset, hem_frac): + crotch_z, _hips_top = probe_hips_bounds(body_dir) + + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + weld_boundaries(shell) + + zs = [v.co.z for v in shell.data.vertices] + waist_z, ankle_z = max(zs), min(zs) + lm = LegLandmarks(armature, waist_z, ankle_z, crotch_z + 0.005) + + hem_z = hem_cut(shell, lm, hem_frac) + # Full-length pants hem AT the ankle joint (calf tail); cropped variants + # hem at the cut plane. + ankle_plane = float(lm.leg_z_pts[0]) if hem_frac >= 0.999 else hem_z + waist_plane, ankle_plane = flatten_open_rims(shell, ankle_plane) + + base.offset_outward(shell, offset) + waist_flare(shell, waist_plane, + BAND_FRAC * (waist_plane - ankle_plane), WAIST_FLARE_M) + base.solidify(shell, base.CLOTH_THICKNESS_M) + clamp_waist_residue(shell, waist_plane) + + # Landmarks reference the CLEAN rims (band under the flattened waist edge, + # cuff above the flattened hem). + lm.waist_z = waist_plane + lm.finalize(ankle_plane) + author_parked_uv2(shell) + + albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png") + mask_path = os.path.join(out_dir, f"{body}_mask.png") + albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body) + base.assign_fabric_material(shell, albedo_img) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + global HEM_FRAC, DENIM_RGB, PLAIN, WAIST_FLARE_M + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] " + "[--offset M] [--hem-frac F] [--waist-flare M] " + "[--base-rgb r,g,b] [--plain]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = base.PER_BODY_OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--hem-frac" in argv: + HEM_FRAC = float(argv[argv.index("--hem-frac") + 1]) + if "--waist-flare" in argv: + WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1]) + if "--base-rgb" in argv: + DENIM_RGB = tuple( + float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) + if "--plain" in argv: + PLAIN = True + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"denim per-body mode: {len(bodies)} bodies, offset " + f"{offset * 1000:.0f} mm, hem-frac {HEM_FRAC}, plain={PLAIN}") + + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_denim_shell(body_dir, out_dir, body, offset, HEM_FRAC) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_hoodie.py b/tooling/garment-fit/blender_author_hoodie.py new file mode 100644 index 000000000..4db6245ff --- /dev/null +++ b/tooling/garment-fit/blender_author_hoodie.py @@ -0,0 +1,590 @@ +""" +blender_author_hoodie.py (T-1089 — hoodie_modern, per-body offset-shell) + +Hooded-top companion to blender_author_offset_shell.py: reuses the base +module's segment join / bone-ratio thresholds / offset / solidify / logo-UV2 / +export machinery and layers the hoodie-specific geometry + texture identity on +top. Everything spatial is derived from bone landmarks (ratios of neck_01 +length, shoulder |x|, spine span) so the same parameters produce a +proportionally identical garment on all 11 bodies. The parameter block below +is the reusable seam for the rest of the hooded/long-sleeve family +(track_jacket, sweater_modern): import this module and override. + +Hoodie deltas over the base t-shirt shell: + * covers torso + torso_upper + FULL arms (long sleeves, wrist-cut on the + lowerarm bone instead of the upperarm short-sleeve cut) + * HOOD DOWN — a rolled collar bulk ring: collar-band verts are inflated + radially away from the neck axis (back-biased, slight upward lift) before + solidify, so the roll gets real thickness and caps into a ring + * looser standoff (16 mm vs the 12 mm per-body tee) + thicker cloth (6 mm) + * painted albedo identity (texture-carried, per body because UV0 face + classification is per body): kangaroo pocket fill + stitch outline, + drawstrings, ribbed hem + cuff bands — all painted as LUMINANCE detail so + the luma-preserving toon_garment recolor keeps them under any tint + * region mask: R = collar/hood roll (asymmetric drop — drapes lower on the + back), G = body, B = sleeves + kangaroo pocket + * logo-capable chest via the base UV2 channel (unchanged) + +PER-BODY ONLY (Q-060: offset-shells are authored per body, never SD-fit): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_hoodie.py -- \ + client/assets/characters/bodies client/assets/characters/clothing/hoodie_modern \ + [--bodies a,b,c] [--offset M] + +Writes per body: /.glb, _mask.png, _base_albedo.png +Plus fallbacks: /base_albedo.png + reference_mask.png (= average_m's). + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060. +""" + +import importlib.util +import math +import os +import shutil +import sys + +import bpy +import bmesh +import numpy as np +from mathutils import Vector + +_HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _load_base(): + spec = importlib.util.spec_from_file_location( + "offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py")) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +base = _load_base() + +# -------------------------------------------------------------------------- +# Hoodie parameters (the reusable seam — override for track_jacket/sweater) +# -------------------------------------------------------------------------- + +GARMENT_ID = "hoodie_modern" + +SEGMENTS = [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", +] + +OFFSET_M = 0.016 # loose standoff (per-body construction guarantees it) +THICKNESS_M = 0.006 # fleece-weight cloth +WRIST_FRAC = 0.92 # fraction of the lowerarm kept (sleeve ends at wrist) + +# Hood-down roll: fractions of neck_01 length unless noted. +ROLL_OUT_FRAC = 0.40 # radial bulge magnitude +ROLL_LIFT_FRAC = 0.14 # upward lift at the rim +ROLL_Z_START_FRAC = 0.45 # roll influence starts this far below collar_z_min +ROLL_REACH_COLLAR_X = 1.7 # candidate radius around the neck axis (x collar_x_abs) +ROLL_FRONT_GAIN = 0.55 # bulge scale at the front... +ROLL_BACK_GAIN = 1.10 # ...and at the back (hood mass hangs behind) +ROLL_EXPONENT = 1.7 # falloff sharpness toward the rim + +# Region mask R (roll) band: drop below collar_z_min, x neck_len, per side. +R_Z_DROP_FRONT = 0.15 +R_Z_DROP_BACK = 0.55 + +# Kangaroo pocket: z as fractions of the waistband-top -> chest_z_lo span +# (sits ABOVE the ribbed hem band), x as fractions of shoulder |x|. +POCKET_Z0_FRAC = 0.06 +POCKET_Z1_FRAC = 0.88 +POCKET_WB_FRAC = 0.56 # bottom half-width +POCKET_WT_FRAC = 0.30 # top half-width (diagonal hand openings) +POCKET_FILL_MULT = 0.95 # subtle patch shading +POCKET_LINE_MULT = 0.70 # stitch outline darkening +POCKET_LINE_PX = 2 # outline thickness (erosion iterations) + +# Drawstrings (front only): x offset / half-width as fractions of collar_x_abs, +# z as fractions of neck_len. +STRING_X_FRAC = 0.30 +STRING_HALF_W_M = 0.005 +STRING_TOP_DROP_FRAC = 0.10 +STRING_LEN_FRAC = 0.75 +STRING_MULT = 0.50 + +# Ribbed bands: hem (x spine span) and cuffs (x lowerarm length). The cuff is +# anchored to the mesh's ACTUAL post-cut reach, not the nominal wrist plane — +# the vert-threshold cut leaves a jagged face boundary that stops 1-3 cm short +# of the plane, so a nominal-anchored band would mostly land on deleted faces. +HEM_BAND_FRAC = 0.08 +CUFF_LEN_FRAC = 0.16 +BAND_MULT = 0.85 +BAND_LINE_MULT = 0.72 +BAND_LINE_M = 0.004 + +# Muted street-tone fabric (luma drives the toon_garment recolor). +FABRIC_RGB = (0.55, 0.56, 0.58) +FABRIC_NOISE = 0.035 +ALBEDO_SEED = 1091 + + +def log(msg): + print(f"[hoodie] {msg}") + + +# -------------------------------------------------------------------------- +# Landmarks beyond the base thresholds +# -------------------------------------------------------------------------- + +def derive_landmarks(armature, thr): + """Hoodie-specific bone landmarks (all in body-local metres).""" + bones = armature.data.bones + neck = bones.get("neck_01") + la_l = bones.get("lowerarm_l") + la_r = bones.get("lowerarm_r") + if not all([neck, la_l, la_r]): + raise RuntimeError("landmark bones missing (neck_01 / lowerarm_l/r)") + neck_len = neck.tail_local.z - neck.head_local.z + lm = { + "neck_y": neck.head_local.y, + "neck_len": neck_len, + "roll_out": ROLL_OUT_FRAC * neck_len, + "roll_lift": ROLL_LIFT_FRAC * neck_len, + "roll_reach": ROLL_REACH_COLLAR_X * thr["collar_x_abs"], + # wrist cut thresholds per side: (sign, x threshold) + "wrist_cuts": [], + "lowerarm_len": 0.0, + } + for b, sign in [(la_l, +1), (la_r, -1)]: + head_x, tail_x = b.head_local.x, b.tail_local.x + thr_x = head_x + WRIST_FRAC * (tail_x - head_x) + lm["wrist_cuts"].append((sign, thr_x)) + lm["lowerarm_len"] = abs(tail_x - head_x) + log(f"landmarks: neck_len {neck_len:.4f} roll_out {lm['roll_out']*1000:.0f}mm " + f"reach {lm['roll_reach']:.3f} wrist cuts " + + " ".join(f"{s:+d}@{t:.3f}" for s, t in lm["wrist_cuts"])) + return lm + + +# -------------------------------------------------------------------------- +# Geometry: wrist cut + hood roll +# -------------------------------------------------------------------------- + +def wrist_cut(shell, lm): + """Trim the sleeve tubes at the wrist plane (same pattern as base.sleeve_cut, + but on the lowerarm bone so the sleeves stay full length).""" + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + to_delete = [] + for v in bm.verts: + for sign, thr_x in lm["wrist_cuts"]: + if sign > 0 and v.co.x > thr_x: + to_delete.append(v) + break + if sign < 0 and v.co.x < thr_x: + to_delete.append(v) + break + bmesh.ops.delete(bm, geom=to_delete, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"wrist cut removed {len(to_delete)} verts; " + f"{len(shell.data.vertices)} remain") + + +def hood_roll(shell, thr, lm): + """Inflate collar-band verts radially away from the neck axis to read as a + rolled-down hood: back-biased bulge + slight rim lift. Runs after the + outward offset and before solidify (the roll then gets cloth thickness).""" + z_start = thr["collar_z_min"] - ROLL_Z_START_FRAC * lm["neck_len"] + reach = lm["roll_reach"] + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + + candidates = [] + z_rim = z_start + for v in bm.verts: + if v.co.z < z_start: + continue + hd = math.hypot(v.co.x, v.co.y - lm["neck_y"]) + if hd > reach or hd < 1e-6: + continue + candidates.append((v, hd)) + z_rim = max(z_rim, v.co.z) + if z_rim <= z_start or not candidates: + log("WARNING: no hood-roll candidates found — roll skipped") + bm.free() + return + + moved = 0 + for v, hd in candidates: + t = (v.co.z - z_start) / (z_rim - z_start) + w = max(0.0, min(1.0, t)) ** ROLL_EXPONENT + if w <= 0.0: + continue + dir_h = Vector((v.co.x, v.co.y - lm["neck_y"], 0.0)) / hd + backness = 0.5 * (1.0 + dir_h.y * -base.FRONT_Y_SIGN) # +Y = back + gain = ROLL_FRONT_GAIN + (ROLL_BACK_GAIN - ROLL_FRONT_GAIN) * backness + v.co += dir_h * (lm["roll_out"] * w * gain) + v.co.z += lm["roll_lift"] * w + moved += 1 + bm.to_mesh(me) + bm.free() + me.update() + log(f"hood roll: {moved} verts inflated (rim z {z_rim:.3f}, " + f"start z {z_start:.3f})") + + +# -------------------------------------------------------------------------- +# Paint maps: rasterize body-space (x, z) into UV0 texel space +# -------------------------------------------------------------------------- + +def _raster_tri_attr(maps, a, b, c, xs, zs, is_front, is_back, W, H): + """Barycentric rasterization interpolating body-space x/z per texel.""" + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return + ys, px_x = np.mgrid[miny:maxy + 1, minx:maxx + 1] + px = px_x + 0.5 + py = ys + 0.5 + w0 = ((by - cy) * (px - cx) + (cx - bx) * (py - cy)) / denom + w1 = ((cy - ay) * (px - cx) + (ax - cx) * (py - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + x_val = w0 * xs[0] + w1 * xs[1] + w2 * xs[2] + z_val = w0 * zs[0] + w1 * zs[1] + w2 * zs[2] + sl = (slice(miny, maxy + 1), slice(minx, maxx + 1)) + maps["X"][sl][inside] = x_val[inside] + maps["Z"][sl][inside] = z_val[inside] + maps["valid"][sl][inside] = True + if is_front: + maps["front"][sl][inside] = True + if is_back: + maps["back"][sl][inside] = True + + +def bake_paint_maps(shell, W, H): + """Rasterize the shell's UV0 layout into per-texel body-space X/Z maps, + with front/back facing flags (front = -Y on this rig).""" + maps = { + "X": np.zeros((H, W), dtype=np.float32), + "Z": np.zeros((H, W), dtype=np.float32), + "valid": np.zeros((H, W), dtype=bool), + "front": np.zeros((H, W), dtype=bool), + "back": np.zeros((H, W), dtype=bool), + } + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.faces.ensure_lookup_table() + bm.normal_update() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for paint-map bake") + for face in bm.faces: + n_front = face.normal.y * base.FRONT_Y_SIGN + c_front = face.calc_center_median().y * base.FRONT_Y_SIGN + is_front = n_front > 0.15 and c_front > 0.0 + is_back = n_front < -0.15 and c_front < 0.0 + loops = face.loops[:] + uvs = [lp[uv_layer].uv.copy() for lp in loops] + cos = [lp.vert.co for lp in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_attr( + maps, uvs[0], uvs[i], uvs[i + 1], + (cos[0].x, cos[i].x, cos[i + 1].x), + (cos[0].z, cos[i].z, cos[i + 1].z), + is_front, is_back, W, H) + bm.free() + overlap = int((maps["front"] & maps["back"]).sum()) + log(f"paint maps: {int(maps['valid'].sum())} texels " + f"(front {int(maps['front'].sum())}, back {int(maps['back'].sum())}, " + f"front/back UV overlap {overlap})") + dump = os.environ.get("HOODIE_DEBUG_MAPS", "") + if dump: + np.savez_compressed(dump, **maps) + log(f"debug maps dumped -> {dump}") + return maps + + +# -------------------------------------------------------------------------- +# Painted albedo (texture-carried modern identity) +# -------------------------------------------------------------------------- + +def derive_paint_params(shell, thr, lm): + """Body-space paint geometry, derived from thresholds + final shell mesh.""" + sleeve_x = thr["sleeve_x_abs"] + hem_z = min((v.co.z for v in shell.data.vertices + if abs(v.co.x) < sleeve_x), default=1.0) + # Actual sleeve reach from the final mesh (jagged post-cut boundary). + wrist_x = max((abs(v.co.x) for v in shell.data.vertices), default=sleeve_x) + chest_lo = thr["chest_z"][0] + hem_band = HEM_BAND_FRAC * (thr["collar_z_min"] - hem_z) + hem_top = hem_z + hem_band + span = chest_lo - hem_top + shoulder_x = thr["chest_x"][1] / base.CHEST_X_FRAC # invert base ratio + p = { + "hem_z": hem_z, + "hem_band": hem_band, + "pocket_z0": hem_top + POCKET_Z0_FRAC * span, + "pocket_z1": hem_top + POCKET_Z1_FRAC * span, + "pocket_wb": POCKET_WB_FRAC * shoulder_x, + "pocket_wt": POCKET_WT_FRAC * shoulder_x, + "string_x": STRING_X_FRAC * thr["collar_x_abs"], + "string_z_top": thr["collar_z_min"] - STRING_TOP_DROP_FRAC * lm["neck_len"], + "string_len": STRING_LEN_FRAC * lm["neck_len"], + "cuff_len": CUFF_LEN_FRAC * lm["lowerarm_len"], + "wrist_x": wrist_x, + "sleeve_x": sleeve_x, + } + log(f"paint params: hem {hem_z:.3f} pocket z ({p['pocket_z0']:.3f}," + f"{p['pocket_z1']:.3f}) w ({p['pocket_wt']:.3f}->{p['pocket_wb']:.3f})") + return p + + +def _erode(m, iters): + for _ in range(iters): + m = (m + & np.roll(m, 1, 0) & np.roll(m, -1, 0) + & np.roll(m, 1, 1) & np.roll(m, -1, 1)) + return m + + +def pocket_mask(maps, p): + """Kangaroo-pocket texel mask: front-only trapezoid with diagonal sides.""" + X, Z = maps["X"], maps["Z"] + paintable = maps["front"] & ~maps["back"] + z0, z1 = p["pocket_z0"], p["pocket_z1"] + if z1 <= z0: + return np.zeros_like(paintable) + t = np.clip((z1 - Z) / (z1 - z0), 0.0, 1.0) # 1 at bottom, 0 at top + half_w = p["pocket_wt"] + (p["pocket_wb"] - p["pocket_wt"]) * t + inside = paintable & (Z >= z0) & (Z <= z1) & (np.abs(X) <= half_w) + log(f"pocket mask: {int(inside.sum())} texels") + return inside + + +def make_painted_albedo(maps, pocket_px, p, out_dir, body): + """Flat street-tone fabric + painted luminance detail; returns bpy image.""" + W = H = base.ALBEDO_SIZE + rng = np.random.default_rng(ALBEDO_SEED) + fabric = np.array(FABRIC_RGB, dtype=np.float32) + noise = (rng.random((H, W, 1), dtype=np.float32) - 0.5) * 2.0 * FABRIC_NOISE + rgb = np.clip(fabric[None, None, :] + noise, 0.0, 1.0) + + X, Z = maps["X"], maps["Z"] + valid = maps["valid"] + front_only = maps["front"] & ~maps["back"] + + # Ribbed hem band (all around) + top stitch line. + hem_top = p["hem_z"] + p["hem_band"] + band = valid & (Z <= hem_top) & (np.abs(X) <= p["sleeve_x"]) + rgb[band] *= BAND_MULT + line = valid & (np.abs(Z - hem_top) <= BAND_LINE_M) & (np.abs(X) <= p["sleeve_x"]) + rgb[line] *= BAND_LINE_MULT + + # Ribbed cuffs (all around) + inner stitch line. + cuff_x0 = p["wrist_x"] - p["cuff_len"] + cuff = valid & (np.abs(X) >= cuff_x0) + rgb[cuff] *= BAND_MULT + cline = valid & (np.abs(np.abs(X) - cuff_x0) <= BAND_LINE_M) + rgb[cline] *= BAND_LINE_MULT + log(f"paint counts: hem {int(band.sum())} hemline {int(line.sum())} " + f"cuff {int(cuff.sum())} cuffline {int(cline.sum())} " + f"(hem_top {hem_top:.3f}, cuff_x0 {cuff_x0:.3f})") + zs = Z[valid] + xs_v = np.abs(X[valid]) + log(f"map ranges: Z [{zs.min():.3f},{zs.max():.3f}] " + f"|X| [{xs_v.min():.3f},{xs_v.max():.3f}] " + f"Z<=hem_top {int((zs <= hem_top).sum())} " + f"|X|>=cuff_x0 {int((xs_v >= cuff_x0).sum())}") + + # Kangaroo pocket: subtle fill + dark stitch outline. + rgb[pocket_px] *= POCKET_FILL_MULT + outline = pocket_px & ~_erode(pocket_px, POCKET_LINE_PX) + rgb[outline] *= POCKET_LINE_MULT + + # Drawstrings (front only, hanging from the collar). + z_top = p["string_z_top"] + z_bot = z_top - p["string_len"] + for sx in (+p["string_x"], -p["string_x"]): + s = front_only & (np.abs(X - sx) <= STRING_HALF_W_M) \ + & (Z >= z_bot) & (Z <= z_top) + rgb[s] *= STRING_MULT + + rgba = np.concatenate([rgb, np.ones((H, W, 1), dtype=np.float32)], axis=2) + img = bpy.data.images.new(f"{GARMENT_ID}_albedo_{body}", W, H, alpha=False) + img.pixels.foreach_set(rgba.reshape(-1)) + img.update() + sidecar = os.path.join(out_dir, f"{body}_base_albedo.png") + img.filepath_raw = sidecar + img.file_format = 'PNG' + img.save() + log(f"painted albedo -> {sidecar}") + # The glTF exporter names the embedded image after the file basename, and + # the Godot import EXTRACTS it as _.png. Point the image at + # the shared base_albedo.png so the extraction lands exactly on the sidecar + # name above (_base_albedo.png, same pixels — tshirt convention), + # instead of a doubled __base_albedo.png. The shared file is + # re-pointed to the reference body's paint at the end of the run. + img.filepath_raw = os.path.join(out_dir, "base_albedo.png") + img.save() + return img + + +# -------------------------------------------------------------------------- +# Region mask: R = hood roll, G = body, B = sleeves + pocket +# -------------------------------------------------------------------------- + +def _classify_hoodie(center, thr, lm): + x, y, z = center.x, center.y, center.z + if abs(x) >= thr["sleeve_x_abs"]: + return (0.0, 0.0, 1.0, 0.0) # sleeves -> B + hd = math.hypot(x, y - lm["neck_y"]) + is_front = y * base.FRONT_Y_SIGN > 0.0 + drop = R_Z_DROP_FRONT if is_front else R_Z_DROP_BACK + z_min = thr["collar_z_min"] - drop * lm["neck_len"] + if z >= z_min and hd <= lm["roll_reach"] + lm["roll_out"] + 0.01: + return (1.0, 0.0, 0.0, 0.0) # hood roll -> R + return (0.0, 1.0, 0.0, 0.0) # body -> G + + +def bake_hoodie_mask(shell, out_path, thr, lm, pocket_px): + """Per-face region rasterization (like base.bake_region_mask) with the + hoodie classifier, then the pocket texels overlaid into B.""" + W = H = base.MASK_SIZE + buf = np.zeros((H, W, 4), dtype=np.float32) + buf[:, :, 1] = 1.0 # body-green background (bleed safety) + + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for region mask bake") + counts = {"roll": 0, "body": 0, "sleeve": 0} + for face in bm.faces: + color = _classify_hoodie(face.calc_center_median(), thr, lm) + if color[0] > 0.5: + counts["roll"] += 1 + elif color[2] > 0.5: + counts["sleeve"] += 1 + else: + counts["body"] += 1 + for a, b, c in base._tris_from_face(face, uv_layer): + base._raster_tri(buf, a, b, c, color, W, H) + bm.free() + total = max(sum(counts.values()), 1) + log("region faces: " + " ".join( + f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in counts.items())) + + # Pocket joins the sleeve tint region (B), per the spec. + if pocket_px is not None and pocket_px.shape == (H, W): + buf[pocket_px] = (0.0, 0.0, 1.0, 0.0) + + img = bpy.data.images.new(f"{GARMENT_ID}_mask", W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = out_path + img.file_format = 'PNG' + img.save() + log(f"baked region mask -> {out_path}") + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_hoodie(body_dir, out_dir, body, offset): + base.clear_scene() + shell, armature = base.build_covered_mesh(body_dir) + thr = base.derive_thresholds(armature) + lm = derive_landmarks(armature, thr) + + wrist_cut(shell, lm) + base.offset_outward(shell, offset) + hood_roll(shell, thr, lm) + base.solidify(shell, THICKNESS_M) + + maps = bake_paint_maps(shell, base.ALBEDO_SIZE, base.ALBEDO_SIZE) + p = derive_paint_params(shell, thr, lm) + pocket_px = pocket_mask(maps, p) + + albedo_img = make_painted_albedo(maps, pocket_px, p, out_dir, body) + base.assign_fabric_material(shell, albedo_img) + base.author_logo_uv(shell, thr) + + # Mask texels are MASK_SIZE; paint maps are ALBEDO_SIZE. Sizes match (512) + # today; rebake the pocket mask if they ever diverge. + mask_pocket = pocket_px if base.MASK_SIZE == base.ALBEDO_SIZE else None + bake_hoodie_mask(shell, os.path.join(out_dir, f"{body}_mask.png"), + thr, lm, mask_pocket) + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] [--offset M]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + base.COVERED_SEGMENTS = SEGMENTS + + log(f"per-body hoodie: {len(bodies)} bodies, offset {offset*1000:.0f} mm") + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_hoodie(body_dir, out_dir, body, offset) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + # Runtime fallbacks mirror the reference body (average_m). + ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log("copied reference_mask.png fallback") + ref_albedo = os.path.join(out_dir, f"{base.REFERENCE_BODY}_base_albedo.png") + if os.path.isfile(ref_albedo): + shutil.copy2(ref_albedo, os.path.join(out_dir, "base_albedo.png")) + log("copied base_albedo.png fallback") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_jacket_shell.py b/tooling/garment-fit/blender_author_jacket_shell.py new file mode 100644 index 000000000..4906de9e6 --- /dev/null +++ b/tooling/garment-fit/blender_author_jacket_shell.py @@ -0,0 +1,529 @@ +""" +blender_author_jacket_shell.py (T-1089, route (c) hand-author proof: suit jacket) + +Jacket-family companion to blender_author_offset_shell.py — imports that module +as a library (segments -> join -> offset -> solidify -> skinned export are +reused unchanged) and adds what a JACKET needs beyond a t-shirt: + + * full-arm coverage (torso + torso_upper + upper AND lower arms) with a + WRIST cut on the lowerarm bone (long sleeves) instead of an upper-arm cut; + * a boundary WELD after the segment join, so the elbow/shoulder segment + seams offset as one continuous surface (no gap rings on long sleeves); + * a jacket standoff (default 18 mm — outerwear drape over the 12 mm tee); + * a PAINTED albedo + region mask baked together in ONE per-pixel + rasterization pass: each texel's 3D body-local position is interpolated + barycentric-ally, so the V-opening / lapels / shirt triangle have crisp + edges independent of the low-poly tessellation (the base script's + per-FACE classification would read chunky on a chest V). + +Region-mask convention for the suit (toon_garment.gdshader tint routing): + R = lapels + collar band (tint_0 — subtle satin two-tone) + G = jacket body (tint_1) + B = sleeves (tint_2) + A = shirt triangle in the V (tint_3 — default WHITE so outfits can + recolor the visible shirt) + +Painted albedo details (luma-carried; the shader recolors hue per region but +keeps albedo luminance, so detail must live in the 0.53-max luma band or the +`luma*1.5+0.2` curve clips it): white-shirt triangle with placket + buttons, +lapel fold + edge shading, front closure seam + jacket button below the V, +sleeve cuff band. All geometry parameters are bone-landmark RATIOS (same +anchors as the base script: shoulder = upperarm head |x|, neck_01, spine_01, +plus lowerarm for the wrist), so every body derives its own proportional +cut/paint constants — calibrated to the intended metric sizes on average_m. + +PER-BODY ONLY: offset-shell garments are authored per body (Q-060 route +guidance) — there is no single-reference mode here. + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_jacket_shell.py -- \ + \ + [--bodies average_m,child,...] [--offset 0.018] [--wrist-frac 0.85] + + Writes per body: + /.glb skinned jacket authored on that body + /_mask.png RGBA region mask (that body's UV0) + Plus: + /base_albedo.png copy of average_m's painted albedo + /reference_mask.png copy of average_m's mask (runtime fallback) + + The painted per-body albedo is embedded in each GLB (image URI basename + "base_albedo", staged under .cache/garment-fit-suit//); on import + Godot extracts it to /_base_albedo.png — the same layout the + t-shirt reference garment has. + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060. +""" + +import sys +import os +import shutil +import bpy +import bmesh +import numpy as np + +# Import the base offset-shell module as a library. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import blender_author_offset_shell as base # noqa: E402 + +# -------------------------------------------------------------------------- +# Jacket parameters +# -------------------------------------------------------------------------- + +JACKET_SEGMENTS = [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", +] + +JACKET_OFFSET_M = 0.018 # outerwear standoff (tee is 12 mm per-body) +WRIST_FRAC = 0.92 # fraction of lowerarm length kept. NB the vert-delete + # cut also drops faces touching deleted verts, so the + # visible sleeve ends ~1 low-poly ring earlier — 0.92 + # lands the hem just above the wrist (matches the + # buttondown's calibrated SLEEVE_END_FRAC). +CUFF_FRAC = 0.14 # last fraction of the KEPT sleeve painted as cuff band +WELD_DIST_M = 0.0001 # merge distance for segment-boundary weld + +BAKE_SIZE = 1024 # albedo + mask resolution (512 is too coarse for the + # 6 mm placket / button dots on the torso island) + +# --- V-opening / lapel geometry, as ratios of average_m bone landmarks ----- +# (anchors identical to base: shoulder |x| 0.1919, neck head z 1.5205, +# spine_01 head z 1.072 -> spine span 0.4485) +V_HALF_TOP_M = 0.068 # V half-width at the collar on average_m +V_BOT_Z_M = 1.17 # V bottom (jacket closure point) on average_m +LAPEL_W_M = 0.045 # lapel band width +LAPEL_DROP_M = 0.020 # lapel wraps slightly below the V point +EDGE_W_M = 0.011 # lapel fold/edge shading line width (albedo only) +PLACKET_HALF_M = 0.006 # shirt placket half-width +CLOSURE_HALF_M = 0.0045 # jacket front closure seam half-width +SHIRT_BTN_R_M = 0.0055 # shirt button radius +JACKET_BTN_R_M = 0.009 # jacket button radius +JACKET_BTN_DROP_M = 0.030 # jacket button sits this far below the V point +SHIRT_BTN_FRACS = (0.11, 0.30) # shirt button z, as frac of (v_top - v_bot) + +V_HALF_TOP_FRAC = V_HALF_TOP_M / base._REF_SHOULDER_X +LAPEL_W_FRAC = LAPEL_W_M / base._REF_SHOULDER_X +EDGE_W_FRAC = EDGE_W_M / base._REF_SHOULDER_X +PLACKET_HALF_FRAC = PLACKET_HALF_M / base._REF_SHOULDER_X +CLOSURE_HALF_FRAC = CLOSURE_HALF_M / base._REF_SHOULDER_X +SHIRT_BTN_R_FRAC = SHIRT_BTN_R_M / base._REF_SHOULDER_X +JACKET_BTN_R_FRAC = JACKET_BTN_R_M / base._REF_SHOULDER_X +_REF_SPAN = base._REF_NECK_Z - base._REF_SPINE_LO_Z +V_BOT_FRAC = (V_BOT_Z_M - base._REF_SPINE_LO_Z) / _REF_SPAN +LAPEL_DROP_FRAC = LAPEL_DROP_M / _REF_SPAN +JACKET_BTN_DROP_FRAC = JACKET_BTN_DROP_M / _REF_SPAN + +# --- Painted albedo luma palette (neutral hue; shader tints supply color) --- +# Kept inside the luma*1.5+0.2 <= 1.0 budget (luma <= 0.53) so painted detail +# survives a white tint on the shirt region. +LUMA_FABRIC = 0.400 # jacket body + sleeves + back +LUMA_SATIN = 0.475 # lapels + collar band (subtle two-tone vs body) +LUMA_EDGE = 0.250 # lapel fold/edge shading lines +LUMA_SHIRT = 0.520 # shirt triangle (renders ~white under white tint) +LUMA_PLACKET = 0.440 # shirt placket strip +LUMA_SHIRT_BTN = 0.320 # shirt buttons +LUMA_CLOSURE = 0.290 # jacket closure seam below the V +LUMA_JACKET_BTN = 0.180 # jacket button +LUMA_CUFF = 0.330 # sleeve cuff band +ALBEDO_NOISE = 0.018 # +/- woven jitter (4-px blocks — compresses well) +ALBEDO_TINT = (1.0, 1.0, 1.03) # slightly cool cast on the neutral greys + +log = base.log + + +# -------------------------------------------------------------------------- +# Suit-specific threshold derivation (extends base.derive_thresholds) +# -------------------------------------------------------------------------- + +def derive_jacket_thresholds(armature, wrist_frac): + thr = base.derive_thresholds(armature) + bones = armature.data.bones + ua_l = bones.get("upperarm_l") + neck = bones.get("neck_01") + spine01 = bones.get("spine_01") + + if ua_l and neck and spine01: + ua_r = bones.get("upperarm_r") + shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + spine_lo = spine01.head_local.z + span = neck.head_local.z - spine_lo + else: + log("WARNING: landmark bones missing — legacy average_m jacket constants") + shoulder_x = base._REF_SHOULDER_X + spine_lo = base._REF_SPINE_LO_Z + span = _REF_SPAN + + v_top = thr["collar_z_min"] + v_bot = spine_lo + V_BOT_FRAC * span + thr.update({ + "v_top_z": v_top, + "v_bot_z": v_bot, + "v_half_top": shoulder_x * V_HALF_TOP_FRAC, + "lapel_w": shoulder_x * LAPEL_W_FRAC, + "lapel_drop": LAPEL_DROP_FRAC * span, + "edge_w": shoulder_x * EDGE_W_FRAC, + "placket_half": shoulder_x * PLACKET_HALF_FRAC, + "closure_half": shoulder_x * CLOSURE_HALF_FRAC, + "shirt_btn_r": shoulder_x * SHIRT_BTN_R_FRAC, + "jacket_btn_r": shoulder_x * JACKET_BTN_R_FRAC, + "jacket_btn_z": v_bot - JACKET_BTN_DROP_FRAC * span, + "shirt_btn_zs": [v_bot + f * (v_top - v_bot) for f in SHIRT_BTN_FRACS], + }) + + # Wrist cut planes + cuff band start, from the lowerarm bones (arms run + # along +/-X in rest pose, elbow head -> wrist tail). + cut_planes = [] # (sign, wrist_thr_x, cuff_start_x) + for bone_name, sign in [("lowerarm_l", +1), ("lowerarm_r", -1)]: + b = bones.get(bone_name) + if b is None: + log(f"WARNING: bone {bone_name} missing — sleeve uncut on that side") + continue + head_x = b.head_local.x + tail_x = b.tail_local.x + wrist_thr = head_x + wrist_frac * (tail_x - head_x) + cuff_start = head_x + wrist_frac * (1.0 - CUFF_FRAC) * (tail_x - head_x) + cut_planes.append((sign, wrist_thr, cuff_start)) + log(f"wrist cut {bone_name}: keep |x| to {wrist_thr:.3f} " + f"(elbow {head_x:.3f} -> wrist {tail_x:.3f}), cuff from {cuff_start:.3f}") + thr["wrist_planes"] = cut_planes + + log(f"jacket thresholds: V top z {v_top:.3f} bottom z {v_bot:.3f} " + f"half-top {thr['v_half_top']:.3f} lapel {thr['lapel_w']:.3f} " + f"btn z {thr['jacket_btn_z']:.3f}") + return thr + + +# -------------------------------------------------------------------------- +# Geometry: weld + wrist cut +# -------------------------------------------------------------------------- + +def weld_segment_boundaries(shell): + """Merge coincident verts along segment seams (elbow/shoulder/chest lines). + + Adjacent body segments duplicate their shared boundary loop; unwelded, the + two copies get island-local normals and offset APART, opening a gap ring at + every seam. Welding first makes the shell offset as one surface. Merged + verts come from the same original body vertex, so weights and loop UVs are + identical/preserved. + """ + bm = bmesh.new() + bm.from_mesh(shell.data) + before = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=WELD_DIST_M) + bm.to_mesh(shell.data) + after = len(shell.data.vertices) + bm.free() + shell.data.update() + log(f"welded segment boundaries: {before} -> {after} verts " + f"({before - after} merged)") + + +def wrist_cut(shell, thr): + """Delete sleeve verts beyond the wrist plane on each lower arm.""" + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + to_delete = [] + for v in bm.verts: + for sign, wrist_thr, _cuff in thr["wrist_planes"]: + if sign > 0 and v.co.x > wrist_thr: + to_delete.append(v) + break + if sign < 0 and v.co.x < wrist_thr: + to_delete.append(v) + break + bmesh.ops.delete(bm, geom=to_delete, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"wrist cut removed {len(to_delete)} verts; " + f"{len(shell.data.vertices)} remain") + + +# -------------------------------------------------------------------------- +# Combined per-pixel albedo paint + region mask bake +# -------------------------------------------------------------------------- + +def _classify_pixels(X, Y, Z, face_sleeve, face_side, thr): + """Vectorized region + luma classification for one triangle's texels. + + Returns (mask_rgba[N,4], luma[N]) for interpolated body-local positions. + Region priority: collar > shirt > lapel > body; sleeves are per-face. + """ + n = X.shape[0] + mask = np.zeros((n, 4), dtype=np.float32) + luma = np.full(n, LUMA_FABRIC, dtype=np.float32) + + front = (Y * base.FRONT_Y_SIGN) > 0.010 + ax = np.abs(X) + + if face_sleeve: + mask[:, 2] = 1.0 # B = sleeves + # cuff band on this arm's outer end + for sign, wrist_thr, cuff_start in thr["wrist_planes"]: + if sign != face_side: + continue + in_cuff = (X * sign) >= (cuff_start * sign) + luma[in_cuff] = LUMA_CUFF + return mask, luma + + v_top = thr["v_top_z"] + v_bot = thr["v_bot_z"] + # V half-width narrows linearly from v_half_top at the collar to 0 at v_bot. + t = np.clip((Z - v_bot) / max(v_top - v_bot, 1e-6), 0.0, None) + vhalf = thr["v_half_top"] * t + + collar = (Z >= thr["collar_z_min"]) & (ax < thr["collar_x_abs"]) + shirt = front & ~collar & (Z >= v_bot) & (Z < v_top) & (ax < vhalf) + lapel_out = vhalf + thr["lapel_w"] + lapel = (front & ~collar & ~shirt + & (Z >= v_bot - thr["lapel_drop"]) & (Z < v_top) + & (ax >= vhalf) & (ax < lapel_out)) + + mask[:, 0][collar | lapel] = 1.0 # R = lapels + collar band + mask[:, 3][shirt] = 1.0 # A = shirt triangle + body = ~(collar | shirt | lapel) + mask[:, 1][body] = 1.0 # G = jacket body + + # ---- painted luma detail ---- + luma[collar | lapel] = LUMA_SATIN + # lapel fold (inner) + edge (outer) shading lines + edge = lapel & ((ax < vhalf + thr["edge_w"]) | (ax > lapel_out - thr["edge_w"])) + luma[edge] = LUMA_EDGE + # shirt triangle: base, placket, buttons + luma[shirt] = LUMA_SHIRT + luma[shirt & (ax < thr["placket_half"])] = LUMA_PLACKET + for bz in thr["shirt_btn_zs"]: + btn = shirt & ((X ** 2 + (Z - bz) ** 2) < thr["shirt_btn_r"] ** 2) + luma[btn] = LUMA_SHIRT_BTN + # jacket closure seam + button below the V point + closure = body & front & (Z < v_bot) & (ax < thr["closure_half"]) + luma[closure] = LUMA_CLOSURE + jbtn = front & ((X ** 2 + (Z - thr["jacket_btn_z"]) ** 2) + < thr["jacket_btn_r"] ** 2) + luma[jbtn] = LUMA_JACKET_BTN + return mask, luma + + +def bake_albedo_and_mask(shell, thr, albedo_path, mask_path, albedo_name): + """One rasterization pass -> painted albedo PNG + RGBA region mask PNG. + + Per texel the 3D body-local position is barycentric-interpolated, so paint + and regions are classified per PIXEL (crisp V edges on coarse topology). + Mask background = body green (bilinear bleed stays tinted); albedo + background = fabric luma. + """ + W = H = BAKE_SIZE + mask_buf = np.zeros((H, W, 4), dtype=np.float32) + mask_buf[:, :, 1] = 1.0 + albedo_buf = np.full((H, W), LUMA_FABRIC, dtype=np.float32) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for bake") + + counts = {"collar_lapel": 0, "body": 0, "sleeve": 0, "shirt": 0} + for face in bm.faces: + center = face.calc_center_median() + face_sleeve = abs(center.x) >= thr["sleeve_x_abs"] + face_side = 1 if center.x >= 0.0 else -1 + loops = face.loops[:] + uvs = [lo[uv_layer].uv for lo in loops] + cos = [lo.vert.co for lo in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_pos( + mask_buf, albedo_buf, + uvs[0], uvs[i], uvs[i + 1], + cos[0], cos[i], cos[i + 1], + face_sleeve, face_side, thr, W, H, counts) + bm.free() + log("bake texel classes: " + " ".join(f"{k}={v}" for k, v in counts.items())) + + # ---- save mask ---- + img = bpy.data.images.new("jacket_region_mask", W, H, alpha=True) + img.pixels.foreach_set(mask_buf.reshape(-1)) + img.update() + img.filepath_raw = mask_path + img.file_format = 'PNG' + img.save() + log(f"baked region mask -> {mask_path}") + + # ---- albedo: neutral grey luma + blocky woven noise, slightly cool ---- + rng = np.random.default_rng(1089) + coarse = (rng.random((H // 4, W // 4), dtype=np.float32) - 0.5) * 2.0 * ALBEDO_NOISE + noise = np.kron(coarse, np.ones((4, 4), dtype=np.float32)) + lum = np.clip(albedo_buf + noise, 0.0, 1.0) + rgba = np.empty((H, W, 4), dtype=np.float32) + for c, mul in enumerate(ALBEDO_TINT): + rgba[:, :, c] = np.clip(lum * mul, 0.0, 1.0) + rgba[:, :, 3] = 1.0 + aimg = bpy.data.images.new(albedo_name, W, H, alpha=False) + aimg.pixels.foreach_set(rgba.reshape(-1)) + aimg.update() + aimg.filepath_raw = albedo_path + aimg.file_format = 'PNG' + aimg.save() + log(f"painted albedo -> {albedo_path}") + return aimg + + +def _raster_tri_pos(mask_buf, albedo_buf, a, b, c, pa, pb, pc, + face_sleeve, face_side, thr, W, H, counts): + """Barycentric fill interpolating 3D position for per-pixel classification.""" + ax_, ay_ = a.x * (W - 1), a.y * (H - 1) + bx_, by_ = b.x * (W - 1), b.y * (H - 1) + cx_, cy_ = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax_, bx_, cx_))), 0) + maxx = min(int(np.ceil(max(ax_, bx_, cx_))), W - 1) + miny = max(int(np.floor(min(ay_, by_, cy_))), 0) + maxy = min(int(np.ceil(max(ay_, by_, cy_))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by_ - cy_) * (ax_ - cx_) + (cx_ - bx_) * (ay_ - cy_) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + px = xs + 0.5 + py = ys + 0.5 + w0 = ((by_ - cy_) * (px - cx_) + (cx_ - bx_) * (py - cy_)) / denom + w1 = ((cy_ - ay_) * (px - cx_) + (ax_ - cx_) * (py - cy_)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + w0i, w1i, w2i = w0[inside], w1[inside], w2[inside] + X = w0i * pa.x + w1i * pb.x + w2i * pc.x + Y = w0i * pa.y + w1i * pb.y + w2i * pc.y + Z = w0i * pa.z + w1i * pb.z + w2i * pc.z + mask_px, luma_px = _classify_pixels( + X.astype(np.float32), Y.astype(np.float32), Z.astype(np.float32), + face_sleeve, face_side, thr) + + counts["collar_lapel"] += int((mask_px[:, 0] > 0.5).sum()) + counts["shirt"] += int((mask_px[:, 3] > 0.5).sum()) + counts["sleeve"] += int((mask_px[:, 2] > 0.5).sum()) + counts["body"] += int((mask_px[:, 1] > 0.5).sum()) + + m_region = mask_buf[miny:maxy + 1, minx:maxx + 1, :] + a_region = albedo_buf[miny:maxy + 1, minx:maxx + 1] + m_region[inside] = mask_px + a_region[inside] = luma_px + + +# -------------------------------------------------------------------------- +# Parked UV2 (suit is not logo-capable; keep mesh format pipeline-consistent) +# -------------------------------------------------------------------------- + +def author_parked_uv2(shell): + me = shell.data + while len(me.uv_layers) > 1: + me.uv_layers.remove(me.uv_layers[-1]) + me.uv_layers.new(name="logo_uv") + me.uv_layers.active = me.uv_layers[0] + bm = bmesh.new() + bm.from_mesh(me) + uvl = bm.loops.layers.uv.get("logo_uv") + for face in bm.faces: + for loop in face.loops: + loop[uvl].uv = (2.0, 2.0) + bm.to_mesh(me) + bm.free() + me.update() + log("UV2 authored fully parked (non-logo garment)") + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_jacket(body_dir, out_dir, stage_dir, body, offset, wrist_frac): + base.clear_scene() + base.COVERED_SEGMENTS = JACKET_SEGMENTS + shell, armature = base.build_covered_mesh(body_dir) + thr = derive_jacket_thresholds(armature, wrist_frac) + weld_segment_boundaries(shell) + wrist_cut(shell, thr) + base.offset_outward(shell, offset) + base.solidify(shell, base.CLOTH_THICKNESS_M) + + # Stage the painted albedo OUTSIDE the Godot project as "base_albedo.png" + # — the glTF image URI takes the file basename, so Godot's on-import + # extraction lands at /_base_albedo.png (tshirt layout) + # without a duplicate authored sidecar next to it. + body_stage = os.path.join(stage_dir, body) + os.makedirs(body_stage, exist_ok=True) + albedo_img = bake_albedo_and_mask( + shell, thr, + os.path.join(body_stage, "base_albedo.png"), + os.path.join(out_dir, f"{body}_mask.png"), + f"jacket_albedo_{body}") + base.assign_fabric_material(shell, albedo_img) + author_parked_uv2(shell) + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- " + "[--bodies a,b,c] [--offset M] [--wrist-frac F]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = JACKET_OFFSET_M + wrist_frac = WRIST_FRAC + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--wrist-frac" in argv: + wrist_frac = float(argv[argv.index("--wrist-frac") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + stage_dir = os.path.join(repo_root, ".cache", "garment-fit-suit") + log(f"jacket per-body mode: {len(bodies)} bodies, offset {offset*1000:.0f} mm, " + f"wrist frac {wrist_frac}") + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_jacket(body_dir, out_dir, stage_dir, body, offset, wrist_frac) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + results.append((body, f"error: {exc}")) + + ref = base.REFERENCE_BODY + ref_mask = os.path.join(out_dir, f"{ref}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {ref}_mask.png -> reference_mask.png (fallback)") + ref_alb = os.path.join(stage_dir, ref, "base_albedo.png") + if os.path.isfile(ref_alb): + shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png")) + log(f"copied staged {ref} albedo -> base_albedo.png") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_lower_shell.py b/tooling/garment-fit/blender_author_lower_shell.py new file mode 100644 index 000000000..7eeaf94d8 --- /dev/null +++ b/tooling/garment-fit/blender_author_lower_shell.py @@ -0,0 +1,524 @@ +""" +blender_author_lower_shell.py (T-1089, lower-body offset-shell companion) + +Author LOWER-BODY offset-shell garments (the pants family) per body, reusing +blender_author_offset_shell.py as a library (scene build, offset, solidify, +raster helpers, export). The base script stays the torso/t-shirt reference; +this companion parameterizes the lower-body family so pants_formal, jeans, +shorts, joggers etc. are all CLI parameter sets over ONE code path — reusable +parameters over one-off hacks. + +Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r. +Natural boundaries give the waist opening (top of seg_hips) and ankle hems +(bottom of seg_leg_lower) for free; a leg cut is only applied for --leg-frac +< 1.0 (shorts family). + +Region mask (RGBA, UV0-aligned — toon_garment.gdshader convention): + R = waistband (top band of the hips, height derived from the pelvis bone) + G = legs (everything else — the main fabric region) + B = cuffs (optional, --cuff-frac > 0; joggers) + +Painted texture identity (baked into the per-body albedo, luma-carried so it +survives any region tint — the shader replaces hue but keeps luminance): + --crease subtle LIGHT front crease line down each leg (formal press line), + drawn by intersecting the shell with each leg's hip->knee->ankle + axis plane and rasterizing the crossing segments in UV space. + --seam subtle DARK horizontal seam line at the waistband boundary. + +Per-body mode only (route guidance from Q-060 / 216-capture QA evidence: +offset-shell garments author per body; weights inherited by construction). + +Usage: + tooling/blender --background --python \ + tooling/garment-fit/blender_author_lower_shell.py -- \ + \ + [--bodies a,b,c] [--offset 0.010] [--leg-frac 1.0] \ + [--waistband-frac 1.0] [--cuff-frac 0.0] \ + [--fabric R,G,B] [--fabric-noise 0.012] \ + [--no-crease] [--crease-gain 0.10] [--no-seam] [--seam-gain 0.05] \ + [--seed 1093] + +Writes per body: + /.glb skinned garment authored on that body + /_mask.png RGBA region mask (that body's UV0 layout) +Plus: + /base_albedo.png reference body's albedo (sidecar; the + painted albedo is embedded per GLB, and + the Godot importer extracts it per body + as _base_albedo.png) + /reference_mask.png copy of average_m_mask.png (runtime fallback) + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe). +""" + +import os +import shutil +import sys + +import bpy +import bmesh +import numpy as np + +# Make the sibling base module importable inside Blender. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import blender_author_offset_shell as base # noqa: E402 + +# -------------------------------------------------------------------------- +# Lower-body garment parameters (CLI-overridable) +# -------------------------------------------------------------------------- + +LOWER_SEGMENTS = [ + "seg_hips", + "seg_leg_upper_l", "seg_leg_upper_r", + "seg_leg_lower_l", "seg_leg_lower_r", +] + +DEFAULTS = { + "offset": 0.010, # slim silhouette standoff (m); per-body authoring + # guarantees clearance by construction, and bottoms + # sit UNDER tops (t-shirt hem = 12 mm), so 10 mm + # keeps the layering order correct at the waist. + "leg_frac": 1.0, # fraction of hip->ankle length kept (1.0 = full) + "waistband_frac": 1.0, # waistband starts at pelvis head + frac*bone_len + "cuff_frac": 0.0, # B-region cuff height as fraction of knee->ankle + "fabric": (0.145, 0.147, 0.160), # dark charcoal, slight cool cast + "fabric_noise": 0.012, # +/- albedo jitter, subtle woven feel + "crease": True, # painted front crease line per leg + "crease_gain": 0.10, # luma lift along the crease (subtle press line) + "seam": True, # painted seam at the waistband boundary + "seam_gain": 0.05, # luma drop along the seam + "seed": 1093, # deterministic albedo noise +} + +CREASE_TOP_MARGIN = 0.12 # crease starts this fraction below the hip joint +CREASE_NORMAL_MIN = 0.35 # face must point forward this much for the crease +LINE_RADIUS_PX = 0.9 # painted line half-width in texels + + +def log(msg): + print(f"[lower-shell] {msg}") + + +# -------------------------------------------------------------------------- +# Threshold derivation (bone landmarks; the 11 bodies share the 65-bone rig) +# -------------------------------------------------------------------------- + +def derive_lower_thresholds(armature, p): + """Derive waistband/cuff z-planes and per-leg axis polylines. + + Anchors: pelvis (waistband band), thigh_* head (hip joint), thigh_* tail / + calf_* head (knee), calf_* tail (ankle). All in body-local metres so each + body derives its OWN proportionally-consistent parameters. + """ + bones = armature.data.bones + pelvis = bones.get("pelvis") + need = ["thigh_l", "thigh_r", "calf_l", "calf_r"] + legs = {} + for side in ("l", "r"): + thigh = bones.get(f"thigh_{side}") + calf = bones.get(f"calf_{side}") + if thigh is None or calf is None: + raise RuntimeError(f"landmark bones missing for leg _{side} ({need})") + # Polyline hip -> knee -> ankle in the XZ plane (rest pose, z-up). + legs[side] = { + "hip": (thigh.head_local.x, thigh.head_local.z), + "knee": (thigh.tail_local.x, thigh.tail_local.z), + "ankle": (calf.tail_local.x, calf.tail_local.z), + } + if pelvis is None: + raise RuntimeError("pelvis bone missing — cannot derive waistband") + + pelvis_len = pelvis.tail_local.z - pelvis.head_local.z + wb_z = pelvis.head_local.z + p["waistband_frac"] * pelvis_len + + hip_z = (legs["l"]["hip"][1] + legs["r"]["hip"][1]) / 2.0 + ankle_z = (legs["l"]["ankle"][1] + legs["r"]["ankle"][1]) / 2.0 + knee_z = (legs["l"]["knee"][1] + legs["r"]["knee"][1]) / 2.0 + leg_len = hip_z - ankle_z + + cut_z = None + if p["leg_frac"] < 0.999: + cut_z = hip_z - p["leg_frac"] * leg_len + + cuff_z = None + if p["cuff_frac"] > 1e-6: + cuff_z = ankle_z + p["cuff_frac"] * (knee_z - ankle_z) + + thr = { + "wb_z": wb_z, + "cut_z": cut_z, + "cuff_z": cuff_z, + "legs": legs, + "hip_z": hip_z, + "ankle_z": ankle_z, + "leg_len": leg_len, + } + log(f"thresholds: waistband z>={wb_z:.3f} " + f"cut_z={cut_z if cut_z is None else round(cut_z, 3)} " + f"cuff_z={cuff_z if cuff_z is None else round(cuff_z, 3)} " + f"hip_z={hip_z:.3f} ankle_z={ankle_z:.3f}") + return thr + + +def _leg_axis_x(leg, z): + """Piecewise-linear x of the leg axis at height z (clamped to endpoints).""" + hx, hz = leg["hip"] + kx, kz = leg["knee"] + ax, az = leg["ankle"] + if z >= hz: + return hx + if z <= az: + return ax + if z >= kz: + t = (hz - z) / max(hz - kz, 1e-6) + return hx + t * (kx - hx) + t = (kz - z) / max(kz - az, 1e-6) + return kx + t * (ax - kx) + + +# -------------------------------------------------------------------------- +# Seam weld (pre-offset) +# -------------------------------------------------------------------------- + +def weld_seams(shell): + """Weld coincident verts before offsetting. + + glTF import splits vertices along UV seams (and the joined segment + boundaries duplicate their shared rings), so offsetting each copy along + its OWN split normal tears the shell open — on the pants this showed as a + skin-coloured slit down the front-centre seam of the hips. Welding fuses + the copies so the offset moves one vertex along one averaged normal. + Per-loop UVs are untouched (the UV seam itself survives); coincident + copies carry identical bone weights by construction, so skinning is + unaffected. + """ + bm = bmesh.new() + bm.from_mesh(shell.data) + before = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=1e-4) + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"welded seams: {before} -> {len(shell.data.vertices)} verts") + + +# -------------------------------------------------------------------------- +# Leg cut (shorts family; skipped at leg_frac 1.0) +# -------------------------------------------------------------------------- + +def leg_cut(shell, cut_z): + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + doomed = [v for v in bm.verts if v.co.z < cut_z] + bmesh.ops.delete(bm, geom=doomed, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"leg cut at z={cut_z:.3f}: removed {len(doomed)} verts; " + f"{len(shell.data.vertices)} remain") + + +# -------------------------------------------------------------------------- +# Painted albedo (charcoal base + crease/seam lines, luma-carried identity) +# -------------------------------------------------------------------------- + +def _plane_crossings_uv(face, uv_layer, dist_fn): + """UV points where the face's edge loop crosses dist_fn(co) == 0.""" + loops = face.loops[:] + n = len(loops) + pts = [] + for i in range(n): + la, lb = loops[i], loops[(i + 1) % n] + d1 = dist_fn(la.vert.co) + d2 = dist_fn(lb.vert.co) + if (d1 > 0.0) == (d2 > 0.0): + continue + denom = d1 - d2 + if abs(denom) < 1e-9: + continue + t = d1 / denom + uv1 = la[uv_layer].uv + uv2 = lb[uv_layer].uv + pts.append((uv1[0] + t * (uv2[0] - uv1[0]), + uv1[1] + t * (uv2[1] - uv1[1]))) + return pts + + +def _stamp_uv_line(hits, a, b, W, H): + """Mark texels along UV segment a->b into boolean mask `hits`.""" + ax, ay = a[0] * (W - 1), a[1] * (H - 1) + bx, by = b[0] * (W - 1), b[1] * (H - 1) + steps = int(max(abs(bx - ax), abs(by - ay)) * 2.0) + 1 + r = LINE_RADIUS_PX + for i in range(steps + 1): + t = i / steps + px = ax + (bx - ax) * t + py = ay + (by - ay) * t + x0 = max(int(np.floor(px - r)), 0) + x1 = min(int(np.ceil(px + r)), W - 1) + y0 = max(int(np.floor(py - r)), 0) + y1 = min(int(np.ceil(py + r)), H - 1) + for yy in range(y0, y1 + 1): + for xx in range(x0, x1 + 1): + if (xx - px) ** 2 + (yy - py) ** 2 <= r * r + 0.25: + hits[yy, xx] = True + + +def bake_painted_albedo(shell, thr, p): + """Charcoal fabric + painted crease/seam lines, baked in UV0 space. + + The toon_garment shader keeps only albedo LUMINANCE under region tints, so + identity painted here (light crease, dark seam) survives any recolor. + """ + W = H = base.ALBEDO_SIZE + rng = np.random.default_rng(p["seed"]) + fabric = np.array(p["fabric"], dtype=np.float32) + noise = (rng.random((H, W, 1), dtype=np.float32) - 0.5) * 2.0 * p["fabric_noise"] + rgb = np.clip(fabric[None, None, :] + noise, 0.0, 1.0) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + bm.normal_update() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for albedo bake") + + crease_hits = np.zeros((H, W), dtype=bool) + seam_hits = np.zeros((H, W), dtype=bool) + crease_faces = 0 + seam_faces = 0 + + crease_top = thr["hip_z"] - CREASE_TOP_MARGIN * thr["leg_len"] + for face in bm.faces: + center = face.calc_center_median() + # Waistband seam: any face crossing the wb_z plane (full ring). + if p["seam"]: + pts = _plane_crossings_uv(face, uv_layer, + lambda co: co.z - thr["wb_z"]) + if len(pts) >= 2: + _stamp_uv_line(seam_hits, pts[0], pts[1], W, H) + seam_faces += 1 + # Front crease: forward-facing faces crossing the leg-axis plane. + if p["crease"]: + if face.normal.y * base.FRONT_Y_SIGN < CREASE_NORMAL_MIN: + continue + if center.z > crease_top or center.z < thr["ankle_z"]: + continue + leg = thr["legs"]["l"] if center.x >= 0.0 else thr["legs"]["r"] + pts = _plane_crossings_uv( + face, uv_layer, lambda co: co.x - _leg_axis_x(leg, co.z)) + if len(pts) >= 2: + _stamp_uv_line(crease_hits, pts[0], pts[1], W, H) + crease_faces += 1 + bm.free() + + if p["seam"]: + rgb[seam_hits, :] = np.clip(rgb[seam_hits, :] - p["seam_gain"], 0.0, 1.0) + if p["crease"]: + rgb[crease_hits, :] = np.clip(rgb[crease_hits, :] + p["crease_gain"], 0.0, 1.0) + log(f"painted albedo: crease faces={crease_faces} " + f"({int(crease_hits.sum())} px) seam faces={seam_faces} " + f"({int(seam_hits.sum())} px)") + if p["crease"] and crease_faces == 0: + log("WARNING: crease painted on 0 faces — check leg axis / front sign") + + rgba = np.concatenate( + [rgb, np.ones((H, W, 1), dtype=np.float32)], axis=2) + img = bpy.data.images.new("garment_lower_albedo", W, H, alpha=False) + img.pixels.foreach_set(rgba.reshape(-1)) + img.update() + return img + + +# -------------------------------------------------------------------------- +# Region mask (waistband R / legs G / optional cuff B) +# -------------------------------------------------------------------------- + +def _classify_lower(center, thr): + if center.z >= thr["wb_z"]: + return (1.0, 0.0, 0.0, 0.0) # waistband -> R (tint_0) + if thr["cuff_z"] is not None and center.z <= thr["cuff_z"]: + return (0.0, 0.0, 1.0, 0.0) # cuff -> B (tint_2) + return (0.0, 1.0, 0.0, 0.0) # legs -> G (tint_1) + + +def bake_lower_mask(shell, out_path, thr): + W = H = base.MASK_SIZE + buf = np.zeros((H, W, 4), dtype=np.float32) + buf[:, :, 1] = 1.0 # green background = legs (main region) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for region mask bake") + + counts = {"waistband": 0, "legs": 0, "cuff": 0} + for face in bm.faces: + color = _classify_lower(face.calc_center_median(), thr) + if color[0] > 0.5: + counts["waistband"] += 1 + elif color[2] > 0.5: + counts["cuff"] += 1 + else: + counts["legs"] += 1 + for a, b, c in base._tris_from_face(face, uv_layer): + base._raster_tri(buf, a, b, c, color, W, H) + bm.free() + total = max(sum(counts.values()), 1) + log("region faces: " + " ".join( + f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in counts.items())) + if counts["waistband"] == 0: + log("WARNING: waistband region empty — check waistband_frac / pelvis") + + img = bpy.data.images.new("garment_lower_mask", W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = out_path + img.file_format = 'PNG' + img.save() + log(f"baked region mask -> {out_path}") + + +# -------------------------------------------------------------------------- +# Parked logo UV2 (pants are not logo-capable; keep the channel guarded) +# -------------------------------------------------------------------------- + +def author_parked_logo_uv(shell): + me = shell.data + while len(me.uv_layers) > 1: + me.uv_layers.remove(me.uv_layers[-1]) + logo_uv = me.uv_layers.new(name="logo_uv") + me.uv_layers.active = me.uv_layers[0] + data = logo_uv.data + parked = [2.0, 2.0] * len(data) + logo_uv.data.foreach_set("uv", parked) + me.update() + log(f"logo UV2 parked outside [0,1] on all {len(data)} loops") + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_lower_shell(body_dir, out_dir, body, p): + base.clear_scene() + base.COVERED_SEGMENTS = LOWER_SEGMENTS # reuse the base segment importer + shell, armature = base.build_covered_mesh(body_dir) + thr = derive_lower_thresholds(armature, p) + + zs = [v.co.z for v in shell.data.vertices] + log(f"shell z-range: {min(zs):.3f}..{max(zs):.3f}") + if thr["wb_z"] >= max(zs): + log("WARNING: waistband plane above shell top — band will be empty") + + weld_seams(shell) + if thr["cut_z"] is not None: + leg_cut(shell, thr["cut_z"]) + base.offset_outward(shell, p["offset"]) + base.solidify(shell, base.CLOTH_THICKNESS_M) + + albedo_img = bake_painted_albedo(shell, thr, p) + base.assign_fabric_material(shell, albedo_img) + author_parked_logo_uv(shell) + bake_lower_mask(shell, os.path.join(out_dir, f"{body}_mask.png"), thr) + + # Save under the SHARED sidecar name for every body: the glTF exporter + # names the embedded image after this filepath, and the Godot importer + # extracts embedded textures as _.png — so this yields the + # per-body _base_albedo.png convention (as tshirt_modern) on import. + # The loop authors the reference body LAST so base_albedo.png ends up as + # the reference albedo (per-body albedos differ: painted crease follows + # each body's own geometry). + base.save_albedo_sidecar( + albedo_img, os.path.join(out_dir, "base_albedo.png")) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +# -------------------------------------------------------------------------- +# Entry +# -------------------------------------------------------------------------- + +def parse_args(argv): + p = dict(DEFAULTS) + # Reference body last: the shared base_albedo.png sidecar is rewritten per + # body, so ordering makes the surviving copy the reference body's. + bodies = [b for b in base.BODY_TYPES if b != base.REFERENCE_BODY] + bodies.append(base.REFERENCE_BODY) + + def _val(flag): + return argv[argv.index(flag) + 1] + + if "--bodies" in argv: + bodies = [s.strip() for s in _val("--bodies").split(",")] + for flag, key, cast in [ + ("--offset", "offset", float), + ("--leg-frac", "leg_frac", float), + ("--waistband-frac", "waistband_frac", float), + ("--cuff-frac", "cuff_frac", float), + ("--fabric-noise", "fabric_noise", float), + ("--crease-gain", "crease_gain", float), + ("--seam-gain", "seam_gain", float), + ("--seed", "seed", int), + ]: + if flag in argv: + p[key] = cast(_val(flag)) + if "--fabric" in argv: + p["fabric"] = tuple(float(s) for s in _val("--fabric").split(",")) + if "--no-crease" in argv: + p["crease"] = False + if "--no-seam" in argv: + p["seam"] = False + return p, bodies + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print(__doc__) + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + p, bodies = parse_args(argv) + os.makedirs(out_dir, exist_ok=True) + + log(f"lower-shell per-body: {len(bodies)} bodies, offset " + f"{p['offset']*1000:.0f} mm, leg_frac {p['leg_frac']}, " + f"fabric {p['fabric']}") + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_lower_shell(body_dir, out_dir, body, p) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + results.append((body, f"error: {exc}")) + + ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {base.REFERENCE_BODY}_mask.png -> reference_mask.png") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_offset_coverall.py b/tooling/garment-fit/blender_author_offset_coverall.py new file mode 100644 index 000000000..ad00382cc --- /dev/null +++ b/tooling/garment-fit/blender_author_offset_coverall.py @@ -0,0 +1,533 @@ +""" +blender_author_offset_coverall.py (T-1089, uniform_utility — route (c) proof) + +Full-body COVERALL authored via the offset-shell technique, per body. Companion +to blender_author_offset_shell.py (imported as a module): reuses its scene +plumbing, offset/solidify, logo-UV2 authoring, rasterizer and export — and +extends it with the coverall-specific geometry and the four-region utility +mask that this garment exists to prove: + + ONE garment covering torso + torso_upper + arms + hips + legs. The upper and + lower shells join at the waist for free: adjacent body segments duplicate a + coincident overlap band (probe: median nearest-vertex distance 0.0 in every + seam band), so the joined mesh has no gap by construction and the duplicated + faces offset identically (same verts, same normals, same atlas UVs) and + render invisibly. + +Geometry beyond the base script (parameterised, not hard-coded): + - WRIST cut: trim lowerarm tubes at a fraction along the lowerarm bone + (full sleeve ending in a cuff above the hand). + - ANKLE cut: trim calf tubes at a fraction along the calf bone (leg ends in + a cuff above the boot line; feet stay free for footwear garments). + Both are bone-landmark fractions, so every body derives its own planes. + +Region mask (the multi-region showcase, R/G/B/A -> tint_0..3): + G = main body fabric + R = trim: collar band + arm cuffs + leg cuffs + B = belt line (waist band, hides the segment seam) + chest patch (logo box, + logo-capable) + right-thigh utility patch + A = shoulder marks (top-facing epaulette strap on each shoulder) + +Painted albedo details (flat, toon-friendly; identity lives in the texture): + per-region flat luminance fills (dark belt webbing, bright patches, mid + shoulder marks), a brighter buckle plate at the front-centre of the belt, + and dark stitch lines rasterised along every region-boundary edge (patch + borders, collar seam, cuff seams, belt edges). The toon_garment shader is + luminance-preserving, so these survive any region recolor. + +Per-body only (this garment ships per-body per the Q-060 route guidance): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_offset_coverall.py -- \ + [--bodies a,b,c] [--offset 0.012] + + Writes per body: + /.glb skinned coverall authored on that body + /_mask.png RGBA region mask (that body's atlas UVs) + /_base_albedo.png painted detail albedo (also in the GLB) + Plus: + /reference_mask.png copy of average_m_mask.png (runtime fallback) + /base_albedo.png copy of average_m's albedo (convention) + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060 +(per-body authoring for offset shells). +""" + +import sys +import os +import shutil + +import bpy +import bmesh +import numpy as np + +# Make the sibling base module importable when Blender runs this file directly. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import blender_author_offset_shell as base # noqa: E402 + +# -------------------------------------------------------------------------- +# Coverall parameters (all bone-landmark fractions unless noted) +# -------------------------------------------------------------------------- + +COVERED_SEGMENTS = [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", + "seg_hips", + "seg_leg_upper_l", "seg_leg_upper_r", + "seg_leg_lower_l", "seg_leg_lower_r", +] + +WRIST_CUT_FRAC = 0.88 # keep this fraction of the lowerarm (elbow->wrist) +ANKLE_CUT_FRAC = 0.82 # keep this fraction of the calf (knee->ankle) +CUFF_ARM_START_FRAC = 0.60 # cuff band = beyond this fraction of the lowerarm +CUFF_LEG_START_FRAC = 0.56 # cuff band = beyond this fraction of the calf + +# Belt: centred between pelvis head and spine_01 head (the torso|hips seam +# band sits at ~0.90 of that span on average_m), half-height in ref metres +# scaled by each body's pelvis->spine_01 span. +BELT_CENTER_FRAC = 0.90 +BELT_HALF_M_REF = 0.032 +BUCKLE_X_ABS_REF = 0.045 # front-centre belt faces within this |x| = buckle plate + +# Shoulder marks: top-facing band above the upperarm head, outside the collar. +SHOULDER_Z_FRAC = 0.18 # of (neck head z - upperarm head z), above upperarm head +SHOULDER_X_MAX_FRAC = 1.38 # of shoulder |x| +SHOULDER_NORMAL_Z_MIN = 0.45 # surface must point upward + +# Chest patch: the logo box inset by this fraction of its width/height per +# side, so the amber patch frames the decal instead of touching the box edge. +CHEST_PATCH_INSET_FRAC = 0.10 + +# Right-thigh utility patch: box along the thigh bone, front-facing. +THIGH_PATCH_SIDE = "thigh_r" +THIGH_PATCH_Z_FRACS = (0.30, 0.58) # fraction down the thigh bone +THIGH_PATCH_HALF_X_REF = 0.058 +THIGH_PATCH_NORMAL_Y_MIN = 0.10 # forward-facing (front = -Y, base.FRONT_Y_SIGN) + +# Painted-albedo luminance per region label (flat toon fills). +ALBEDO_LUMA = { + "body": 0.62, + "collar": 0.66, + "cuff": 0.66, + "shoulder": 0.50, + "belt": 0.34, + "buckle": 0.82, + "patch": 0.72, +} +STITCH_LUMA = 0.30 # dark seam/stitch lines on region boundaries +ALBEDO_NOISE = 0.03 # +/- woven-feel jitter (matches the base script) + +# Label ids index the vectorised classifier's output arrays. +LABELS = ["body", "collar", "cuff", "shoulder", "belt", "buckle", "patch"] +LABEL_ID = {name: i for i, name in enumerate(LABELS)} +LABEL_RGBA = { + "collar": (1.0, 0.0, 0.0, 0.0), # R trim + "cuff": (1.0, 0.0, 0.0, 0.0), # R trim + "body": (0.0, 1.0, 0.0, 0.0), # G main fabric + "belt": (0.0, 0.0, 1.0, 0.0), # B belt + patches + "buckle": (0.0, 0.0, 1.0, 0.0), # B + "patch": (0.0, 0.0, 1.0, 0.0), # B + "shoulder": (0.0, 0.0, 0.0, 1.0), # A shoulder marks +} +LABEL_RGBA_ARR = np.array([LABEL_RGBA[n] for n in LABELS], dtype=np.float32) +LABEL_LUMA_ARR = np.array([ALBEDO_LUMA[n] for n in LABELS], dtype=np.float32) + +_REF_SHOULDER_X = 0.1919 # average_m upperarm head |x| (same anchor as base) + +log = base.log + + +# -------------------------------------------------------------------------- +# Threshold derivation (extends base.derive_thresholds with coverall zones) +# -------------------------------------------------------------------------- + +def derive_coverall_thresholds(armature): + thr = base.derive_thresholds(armature) + bones = armature.data.bones + + def bone(name): + b = bones.get(name) + if b is None: + raise RuntimeError(f"landmark bone {name} missing") + return b + + ua = bone("upperarm_l") + neck = bone("neck_01") + pelvis = bone("pelvis") + spine01 = bone("spine_01") + shoulder_x = abs(ua.head_local.x) + scale = shoulder_x / _REF_SHOULDER_X + + # Wrist cut planes + arm cuff start, per side (arm runs along +/-X). + for side, sign in (("l", +1), ("r", -1)): + la = bone(f"lowerarm_{side}") + hx, tx = la.head_local.x, la.tail_local.x + thr[f"wrist_cut_x_{side}"] = hx + WRIST_CUT_FRAC * (tx - hx) + thr[f"cuff_arm_x_{side}"] = hx + CUFF_ARM_START_FRAC * (tx - hx) + # Ankle cut + leg cuff start (leg runs down -Z; both calves share z). + calf = bone("calf_l") + hz, tz = calf.head_local.z, calf.tail_local.z + thr["ankle_cut_z"] = hz + ANKLE_CUT_FRAC * (tz - hz) + thr["cuff_leg_z"] = hz + CUFF_LEG_START_FRAC * (tz - hz) + + # Belt band. + pz, sz = pelvis.head_local.z, spine01.head_local.z + span = sz - pz + thr["belt_z"] = pz + BELT_CENTER_FRAC * span + thr["belt_half"] = BELT_HALF_M_REF * scale + thr["buckle_x_abs"] = BUCKLE_X_ABS_REF * scale + + # Shoulder marks. + uz = ua.head_local.z + thr["shoulder_z_min"] = uz + SHOULDER_Z_FRAC * (neck.head_local.z - uz) + thr["shoulder_x_max"] = shoulder_x * SHOULDER_X_MAX_FRAC + + # Chest patch = logo box inset a little on every side. + cx0, cx1 = thr["chest_x"] + cz0, cz1 = thr["chest_z"] + dx = (cx1 - cx0) * CHEST_PATCH_INSET_FRAC + dz = (cz1 - cz0) * CHEST_PATCH_INSET_FRAC + thr["patch_x"] = (cx0 + dx, cx1 - dx) + thr["patch_z"] = (cz0 + dz, cz1 - dz) + + # Right-thigh patch box. + thigh = bone(THIGH_PATCH_SIDE) + thz, ttz = thigh.head_local.z, thigh.tail_local.z + f0, f1 = THIGH_PATCH_Z_FRACS + thr["thigh_patch_z"] = (thz + f1 * (ttz - thz), thz + f0 * (ttz - thz)) + tx_ctr = thigh.head_local.x + half = THIGH_PATCH_HALF_X_REF * scale + thr["thigh_patch_x"] = (tx_ctr - half, tx_ctr + half) + + log( + "coverall thresholds: " + f"wrist_l x>{thr['wrist_cut_x_l']:.3f} wrist_r x<{thr['wrist_cut_x_r']:.3f} " + f"ankle z<{thr['ankle_cut_z']:.3f} " + f"belt z={thr['belt_z']:.3f}+/-{thr['belt_half']:.3f} " + f"shoulder z>={thr['shoulder_z_min']:.3f} " + f"thigh_patch x=({thr['thigh_patch_x'][0]:.3f},{thr['thigh_patch_x'][1]:.3f}) " + f"z=({thr['thigh_patch_z'][0]:.3f},{thr['thigh_patch_z'][1]:.3f})" + ) + return thr + + +# -------------------------------------------------------------------------- +# Limb cuts (wrists + ankles) +# -------------------------------------------------------------------------- + +def limb_cuts(shell, thr): + """Delete verts beyond the wrist planes (|X|) and below the ankle plane (Z).""" + wl = thr["wrist_cut_x_l"] + wr = thr["wrist_cut_x_r"] + az = thr["ankle_cut_z"] + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + to_delete = [ + v for v in bm.verts + if v.co.x > wl or v.co.x < wr or v.co.z < az + ] + bmesh.ops.delete(bm, geom=to_delete, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"limb cuts removed {len(to_delete)} verts " + f"(wrist x>{wl:.3f}/x<{wr:.3f}, ankle z<{az:.3f}); " + f"{len(shell.data.vertices)} remain") + + +# -------------------------------------------------------------------------- +# Region classification (vectorised, per texel) +# +# Face-granular classification made the patch/belt/shoulder boundaries follow +# the triangulation (jagged, torn-looking zones on the first preview). The +# bake therefore classifies every TEXEL: barycentric interpolation gives each +# covered texel a body-space position + smoothed vertex normal, and the zone +# boundaries land exactly where the thresholds say — crisp at mask resolution. +# -------------------------------------------------------------------------- + +def classify_texels(pos, nrm, thr): + """Classify N texels. pos/nrm are (N,3) body-local arrays. + + Returns (N,) uint8 label ids (indices into LABELS). Precedence: collar, + cuffs, shoulder marks, belt/buckle, chest patch, thigh patch, body. + """ + x, y, z = pos[:, 0], pos[:, 1], pos[:, 2] + fy = y * base.FRONT_Y_SIGN + fwd = nrm[:, 1] * base.FRONT_Y_SIGN + ax = np.abs(x) + + lab = np.full(x.shape, LABEL_ID["body"], dtype=np.uint8) + remaining = np.ones(x.shape, dtype=bool) + + def take(cond, name): + m = cond & remaining + lab[m] = LABEL_ID[name] + remaining[m] = False + + take((z >= thr["collar_z_min"]) & (ax < thr["collar_x_abs"]), "collar") + take((x >= thr["cuff_arm_x_l"]) | (x <= thr["cuff_arm_x_r"]), "cuff") + take(z <= thr["cuff_leg_z"], "cuff") + take( + (z >= thr["shoulder_z_min"]) + & (ax >= thr["collar_x_abs"]) & (ax <= thr["shoulder_x_max"]) + & (nrm[:, 2] > SHOULDER_NORMAL_Z_MIN), + "shoulder", + ) + belt = np.abs(z - thr["belt_z"]) <= thr["belt_half"] + take(belt & (fy > 0.0) & (ax < thr["buckle_x_abs"]), "buckle") + take(belt, "belt") + px0, px1 = thr["patch_x"] + pz0, pz1 = thr["patch_z"] + take( + (fy > base.CHEST_FRONT_Y) + & (x >= px0) & (x <= px1) & (z >= pz0) & (z <= pz1) + & (fwd > base.CHEST_NORMAL_Y), + "patch", + ) + tx0, tx1 = thr["thigh_patch_x"] + tz0, tz1 = thr["thigh_patch_z"] + take( + (fy > 0.0) + & (x >= tx0) & (x <= tx1) & (z >= tz0) & (z <= tz1) + & (fwd > THIGH_PATCH_NORMAL_Y_MIN), + "patch", + ) + return lab + + +# -------------------------------------------------------------------------- +# Combined mask + painted-albedo bake (one classification pass) +# -------------------------------------------------------------------------- + +def _tri_texels(a, b, c, W, H): + """Texels covered by UV triangle (a,b,c) with barycentric weights. + + Same maths as base._raster_tri, but returns (ys, xs, w0, w1, w2) arrays + instead of writing a flat colour, so the caller can interpolate per-texel + attributes (position, normal) across the triangle. + """ + empty = (np.empty(0, int),) * 2 + (np.empty(0, np.float32),) * 3 + ax, ay = a.x * (W - 1), a.y * (H - 1) + bx, by = b.x * (W - 1), b.y * (H - 1) + cx, cy = c.x * (W - 1), c.y * (H - 1) + minx = max(int(np.floor(min(ax, bx, cx))), 0) + maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) + miny = max(int(np.floor(min(ay, by, cy))), 0) + maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) + if minx > maxx or miny > maxy: + return empty + denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denom) < 1e-9: + return empty + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + px = xs + 0.5 + py = ys + 0.5 + w0 = ((by - cy) * (px - cx) + (cx - bx) * (py - cy)) / denom + w1 = ((cy - ay) * (px - cx) + (ax - cx) * (py - cy)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return empty + return ( + ys[inside], xs[inside], + w0[inside].astype(np.float32), + w1[inside].astype(np.float32), + w2[inside].astype(np.float32), + ) + + +def bake_mask_and_albedo(shell, thr, mask_path, albedo_name, seed): + """One pass over the faces: bake the RGBA region mask AND the painted + detail albedo (flat per-region luminance + stitch lines + woven noise). + + Classification is per TEXEL (interpolated position + smoothed vertex + normal), so zone boundaries are crisp at mask resolution instead of + following the triangulation. + """ + W = H = base.MASK_SIZE + mask = np.zeros((H, W, 4), dtype=np.float32) + mask[:, :, 1] = 1.0 # green background = main body (bilinear-bleed safe) + albedo = np.zeros((H, W, 4), dtype=np.float32) + albedo[:, :, 0:3] = ALBEDO_LUMA["body"] + albedo[:, :, 3] = 1.0 + label_map = np.full((H, W), LABEL_ID["body"], dtype=np.uint8) + covered = np.zeros((H, W), dtype=bool) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + bm.normal_update() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for bake") + + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv for loop in loops] + pos = [loop.vert.co for loop in loops] + nrm = [loop.vert.normal for loop in loops] + for i in range(1, len(loops) - 1): + tri = (0, i, i + 1) + ys, xs, w0, w1, w2 = _tri_texels(uvs[tri[0]], uvs[tri[1]], uvs[tri[2]], W, H) + if ys.size == 0: + continue + p = np.empty((ys.size, 3), dtype=np.float32) + n = np.empty((ys.size, 3), dtype=np.float32) + for axis in range(3): + p[:, axis] = (w0 * pos[tri[0]][axis] + w1 * pos[tri[1]][axis] + + w2 * pos[tri[2]][axis]) + n[:, axis] = (w0 * nrm[tri[0]][axis] + w1 * nrm[tri[1]][axis] + + w2 * nrm[tri[2]][axis]) + n /= np.maximum(np.linalg.norm(n, axis=1, keepdims=True), 1e-9) + lab = classify_texels(p, n, thr) + label_map[ys, xs] = lab + covered[ys, xs] = True + mask[ys, xs] = LABEL_RGBA_ARR[lab] + albedo[ys, xs, 0:3] = LABEL_LUMA_ARR[lab][:, None] + total = max(int(covered.sum()), 1) + tex_counts = np.bincount(label_map[covered], minlength=len(LABELS)) + log("region texels: " + " ".join( + f"{LABELS[i]}={int(c)} ({100.0 * c / total:.1f}%)" + for i, c in enumerate(tex_counts) if c > 0)) + + # Woven-feel noise over the fills, before stitch lines (lines stay crisp). + rng = np.random.default_rng(seed) + noise = (rng.random((H, W, 1), dtype=np.float32) - 0.5) * 2.0 * ALBEDO_NOISE + albedo[:, :, 0:3] = np.clip(albedo[:, :, 0:3] + noise, 0.0, 1.0) + + # Stitch lines: texel-space label transitions where BOTH texels belong to + # rasterised geometry (skipping UV-island borders against background). + # Buckle|belt stays seamless (same physical strap). + lm = np.where(label_map == LABEL_ID["buckle"], LABEL_ID["belt"], label_map) + edge = np.zeros((H, W), dtype=bool) + dh = (lm[:, 1:] != lm[:, :-1]) & covered[:, 1:] & covered[:, :-1] + edge[:, 1:] |= dh + edge[:, :-1] |= dh + dv = (lm[1:, :] != lm[:-1, :]) & covered[1:, :] & covered[:-1, :] + edge[1:, :] |= dv + edge[:-1, :] |= dv + albedo[edge, 0:3] = STITCH_LUMA + bm.free() + log(f"stitch lines on {int(edge.sum())} boundary texels") + + # Floor the alpha channel at 2/255: Godot's default texture import runs + # process/fix_alpha_border, which rewrites the RGB of fully-transparent + # texels bordering opaque ones — that would smear the shoulder-mark island + # edges into the surrounding body-green weights. With no alpha-0 texels the + # pass is a no-op; the 0.8% tint_3 weight it adds everywhere is invisible. + mask[:, :, 3] = np.maximum(mask[:, :, 3], 2.0 / 255.0) + + img_mask = bpy.data.images.new(f"mask_{albedo_name}", W, H, alpha=True) + # The mask is channel-packed DATA (R/G/B/A region weights), not imagery + # with transparency: without this, Blender's straight-alpha PNG save + # zeroes the A channel (verified: shoulder-mark texels came back A=0). + img_mask.alpha_mode = 'CHANNEL_PACKED' + img_mask.pixels.foreach_set(mask.reshape(-1)) + img_mask.update() + img_mask.filepath_raw = mask_path + img_mask.file_format = 'PNG' + img_mask.save() + log(f"baked region mask -> {mask_path}") + + img_albedo = bpy.data.images.new(f"albedo_{albedo_name}", W, H, alpha=False) + img_albedo.pixels.foreach_set(albedo.reshape(-1)) + img_albedo.update() + return img_albedo + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_coverall(body_dir, out_dir, body, offset, seed): + base.clear_scene() + base.COVERED_SEGMENTS = COVERED_SEGMENTS # build_covered_mesh reads this + shell, armature = base.build_covered_mesh(body_dir) + thr = derive_coverall_thresholds(armature) + limb_cuts(shell, thr) + base.offset_outward(shell, offset) + + # Author UV2 + bake mask/albedo BEFORE solidify: the inner shell that + # solidify adds duplicates every face with the SAME atlas UVs but a + # flipped normal — the normal-gated rules (shoulder marks, patches) would + # classify those copies as body and overwrite the very texels the outer + # faces just wrote. Pre-solidify there is exactly one face per texel. + base.author_logo_uv(shell, thr) # UV2 (inner shell inherits it, harmless) + albedo_img = bake_mask_and_albedo( + shell, thr, os.path.join(out_dir, f"{body}_mask.png"), body, seed) + + base.solidify(shell, base.CLOTH_THICKNESS_M) + base.assign_fabric_material(shell, albedo_img) + + # Save the albedo under the SHARED name before export: the glTF exporter + # names the embedded texture after the image filepath basename, and the + # Godot GLB import extracts it as _.png — with the shared + # name that lands on the tshirt-convention _base_albedo.png (a + # -specific filepath here would yield __base_albedo.png). + base.save_albedo_sidecar(albedo_img, os.path.join(out_dir, "base_albedo.png")) + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + # Keep a deterministic authored per-body sidecar as well. + shutil.copy2(os.path.join(out_dir, "base_albedo.png"), + os.path.join(out_dir, f"{body}_base_albedo.png")) + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- [--bodies a,b,c] [--offset M]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + offset = base.PER_BODY_OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + log(f"coverall per-body mode: {len(bodies)} bodies, offset {offset*1000:.0f} mm") + + results = [] + for i, body in enumerate(bodies): + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_coverall(body_dir, out_dir, body, offset, seed=1089 + i) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + import traceback + traceback.print_exc() + results.append((body, f"error: {exc}")) + + # Runtime fallback: reference_mask.png + base_albedo.png mirror average_m. + ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {base.REFERENCE_BODY}_mask.png -> reference_mask.png (fallback)") + # base_albedo.png currently holds the LAST body's albedo; restore the + # reference body's copy so the shared sidecar is deterministic. + ref_albedo = os.path.join(out_dir, f"{base.REFERENCE_BODY}_base_albedo.png") + if os.path.isfile(ref_albedo): + shutil.copy2(ref_albedo, os.path.join(out_dir, "base_albedo.png")) + log(f"copied {base.REFERENCE_BODY}_base_albedo.png -> base_albedo.png") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_offset_shell.py b/tooling/garment-fit/blender_author_offset_shell.py index 1a04291cc..44d4b1bec 100644 --- a/tooling/garment-fit/blender_author_offset_shell.py +++ b/tooling/garment-fit/blender_author_offset_shell.py @@ -27,24 +27,51 @@ Pipeline (t-shirt reference on average_m): 9. Export the reference GLB (export_skins=True) + write reference_mask.png and base_albedo.png sidecars. -The output reference is authored on average_m only; G1 -(blender_batch_fit_skinned.py) fits it to the other body types. +Two modes (T-1089): + +SINGLE-REFERENCE (default) — author on one body (average_m); G1 +(blender_batch_fit_skinned.py) SD-fits it to the other body types. Right for +derived/hand-authored garments that share one UV layout + one mask. -Run: tooling/blender --background --python \ tooling/garment-fit/blender_author_offset_shell.py -- \ - /average_m [--offset 0.012] [--sleeve-frac 0.40] + /average_m [--offset 0.020] [--sleeve-frac 0.40] -Writes: - /average_m.glb reference garment (skinned) - /reference_mask.png RGBA region mask (UV0-aligned) - /base_albedo.png flat fabric albedo (also embedded in the GLB) + Writes: + /average_m.glb reference garment (skinned) + /reference_mask.png RGBA region mask (UV0-aligned) + /base_albedo.png flat fabric albedo (also embedded in the GLB) + +PER-BODY (--per-body) — author the shell from EACH body's own segment meshes. +QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with +girth divergence (muscular_m 859px worst clip at 24 mm standoff). Authoring +per body gives guaranteed standoff + exact weights by construction, and lets +the offset drop back to the ~12 mm ideal. Cut/mask parameters are derived +from each body's OWN bone landmarks using the same proportional ratios +(anchored to reproduce the hand-calibrated average_m constants exactly), so +regions stay consistent across bodies. + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_offset_shell.py -- \ + --per-body \ + [--bodies average_m,child,...] [--offset 0.012] [--sleeve-frac 0.40] + + Writes per body: + /.glb skinned garment authored on that body + /_mask.png RGBA region mask (that body's UV0 layout) + Plus: + /base_albedo.png shared flat fabric albedo (deterministic) + /reference_mask.png copy of average_m_mask.png (runtime fallback) + + The runtime compositor (character_visual.gd) prefers _mask.png and + falls back to reference_mask.png for SD-fit garments. Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house wardrobe). """ import sys import os +import shutil import bpy import bmesh import numpy as np @@ -56,15 +83,25 @@ import numpy as np # Segments a t-shirt covers. Order matters only for join-active choice. COVERED_SEGMENTS = ["seg_torso", "seg_torso_upper", "seg_arm_upper_l", "seg_arm_upper_r"] -OFFSET_M = 0.020 # outward standoff along vertex normals; larger than the - # 10-14 mm ideal on the reference body buys clearance for - # bigger bodies under Surface-Deform batch-fit (Q-060) — - # the muscular/female torso otherwise pokes through. -CLOTH_THICKNESS_M = 0.004 # Solidify thickness after offset (total ~24 mm standoff) +OFFSET_M = 0.020 # single-reference standoff along vertex normals; larger + # than the 10-14 mm ideal on the reference body buys + # clearance for bigger bodies under Surface-Deform + # batch-fit (Q-060) — the muscular/female torso otherwise + # pokes through. +PER_BODY_OFFSET_M = 0.012 # per-body standoff — each body's own surface guarantees + # clearance by construction, so the ideal applies. +CLOTH_THICKNESS_M = 0.004 # Solidify thickness after offset SLEEVE_FRAC = 0.40 # fraction of upper-arm length kept (short sleeve) MASK_SIZE = 512 ALBEDO_SIZE = 512 +# All shipping body types (matches blender_batch_fit_skinned.py / the runtime). +BODY_TYPES = [ + "average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", + "heavy_m", "heavy_f", "teen_m", "teen_f", "child", +] +REFERENCE_BODY = "average_m" + # Flat modern-fabric base tone (linear-ish sRGB), neutral heather grey. FABRIC_RGB = (0.60, 0.61, 0.63) FABRIC_NOISE = 0.03 # +/- albedo jitter for a subtle woven feel @@ -83,11 +120,75 @@ COLLAR_Z_MIN = 1.49 # faces above this AND near centre -> collar band (R) COLLAR_X_ABS = 0.11 # collar band stays near the neck, not the shoulders SLEEVE_X_ABS = 0.20 # faces with |center X| beyond this -> sleeve cap (B) +# -------------------------------------------------------------------------- +# Per-body threshold derivation (T-1089 per-body shell mode) +# +# The absolute constants above were hand-calibrated on average_m. The ratios +# below re-express every one of them against average_m's bone landmarks +# (shoulder = upperarm head |x| 0.1919, neck_01 head z 1.5205 / length 0.0793, +# spine_01 head z 1.072) so any body derives the SAME proportional cut/mask +# parameters from its own armature. On average_m the derivation reproduces +# the legacy constants exactly; the 11 bodies share one 65-bone rig, so the +# landmarks exist everywhere. +# -------------------------------------------------------------------------- +_REF_SHOULDER_X = 0.1919 +_REF_NECK_Z = 1.5205 +_REF_NECK_LEN = 0.0793 +_REF_SPINE_LO_Z = 1.072 + +SLEEVE_X_FRAC = SLEEVE_X_ABS / _REF_SHOULDER_X # of shoulder |x| +COLLAR_X_FRAC = COLLAR_X_ABS / _REF_SHOULDER_X # of shoulder |x| +COLLAR_DROP_FRAC = (_REF_NECK_Z - COLLAR_Z_MIN) / _REF_NECK_LEN # below neck head +CHEST_X_FRAC = CHEST_X[1] / _REF_SHOULDER_X # of shoulder |x| +_REF_SPINE_SPAN = _REF_NECK_Z - _REF_SPINE_LO_Z +CHEST_Z_LO_FRAC = (CHEST_Z[0] - _REF_SPINE_LO_Z) / _REF_SPINE_SPAN +CHEST_Z_HI_FRAC = (CHEST_Z[1] - _REF_SPINE_LO_Z) / _REF_SPINE_SPAN + def log(msg): print(f"[offset-shell] {msg}") +def derive_thresholds(armature): + """Derive cut/mask thresholds from this body's bone landmarks. + + Returns a dict {sleeve_x_abs, collar_z_min, collar_x_abs, chest_x, chest_z}. + Falls back to the legacy average_m constants when landmarks are missing. + """ + bones = armature.data.bones + ua_l = bones.get("upperarm_l") + ua_r = bones.get("upperarm_r") + neck = bones.get("neck_01") + spine01 = bones.get("spine_01") + if not all([ua_l, ua_r, neck, spine01]): + log("WARNING: landmark bones missing — using legacy average_m thresholds") + return { + "sleeve_x_abs": SLEEVE_X_ABS, + "collar_z_min": COLLAR_Z_MIN, + "collar_x_abs": COLLAR_X_ABS, + "chest_x": CHEST_X, + "chest_z": CHEST_Z, + } + shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + neck_z = neck.head_local.z + neck_len = neck.tail_local.z - neck.head_local.z + spine_lo = spine01.head_local.z + spine_span = neck_z - spine_lo + thr = { + "sleeve_x_abs": shoulder_x * SLEEVE_X_FRAC, + "collar_z_min": neck_z - COLLAR_DROP_FRAC * neck_len, + "collar_x_abs": shoulder_x * COLLAR_X_FRAC, + "chest_x": (-shoulder_x * CHEST_X_FRAC, shoulder_x * CHEST_X_FRAC), + "chest_z": (spine_lo + CHEST_Z_LO_FRAC * spine_span, + spine_lo + CHEST_Z_HI_FRAC * spine_span), + } + log(f"thresholds: sleeve |x|>={thr['sleeve_x_abs']:.3f} " + f"collar z>={thr['collar_z_min']:.3f} |x|<{thr['collar_x_abs']:.3f} " + f"chest x=({thr['chest_x'][0]:.3f},{thr['chest_x'][1]:.3f}) " + f"z=({thr['chest_z'][0]:.3f},{thr['chest_z'][1]:.3f})") + return thr + + # -------------------------------------------------------------------------- # Scene helpers # -------------------------------------------------------------------------- @@ -290,12 +391,12 @@ def assign_fabric_material(shell, albedo_img): # Region mask bake (UV0-aligned, per-face rasterization) # -------------------------------------------------------------------------- -def _classify_region(center): +def _classify_region(center, thr): """Return an RGBA region colour for a face centre (body-local coords).""" - x, y, z = center.x, center.y, center.z - if abs(x) >= SLEEVE_X_ABS: + x, z = center.x, center.z + if abs(x) >= thr["sleeve_x_abs"]: return (0.0, 0.0, 1.0, 0.0) # sleeve caps -> B (tint[2]) - if z >= COLLAR_Z_MIN and abs(x) < COLLAR_X_ABS: + if z >= thr["collar_z_min"] and abs(x) < thr["collar_x_abs"]: return (1.0, 0.0, 0.0, 0.0) # neck collar band -> R (tint[0]) return (0.0, 1.0, 0.0, 0.0) # main body -> G (tint[1]) @@ -310,7 +411,7 @@ def _tris_from_face(face, uv_layer): return tris -def bake_region_mask(shell, out_path): +def bake_region_mask(shell, out_path, thr): """Rasterize each face's UV0 triangle with its region colour into MASK_SIZE^2. Background initialised to main-body green so bilinear bleed at island edges @@ -328,12 +429,21 @@ def bake_region_mask(shell, out_path): if uv_layer is None: raise RuntimeError("no active UV layer for region mask bake") - yy, xx = np.mgrid[0:H, 0:W] + region_counts = {"collar": 0, "body": 0, "sleeve": 0} for face in bm.faces: - color = _classify_region(face.calc_center_median()) + color = _classify_region(face.calc_center_median(), thr) + if color[0] > 0.5: + region_counts["collar"] += 1 + elif color[2] > 0.5: + region_counts["sleeve"] += 1 + else: + region_counts["body"] += 1 for a, b, c in _tris_from_face(face, uv_layer): _raster_tri(buf, a, b, c, color, W, H) bm.free() + total = max(sum(region_counts.values()), 1) + log("region faces: " + " ".join( + f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in region_counts.items())) # Blender image is bottom-up; buf row 0 is V=0 (bottom) already since we # rasterise with row = v*(H-1). Save via Blender to match the texture pipe. @@ -378,7 +488,7 @@ def _raster_tri(buf, a, b, c, color, W, H): # Logo UV2 chest channel # -------------------------------------------------------------------------- -def author_logo_uv(shell): +def author_logo_uv(shell, thr): """Create a 2nd UV layer projecting front chest faces into [0,1]; park the rest outside the box (shader guards uv2 in [0,1]).""" me = shell.data @@ -394,8 +504,8 @@ def author_logo_uv(shell): bm.normal_update() uvl = bm.loops.layers.uv.get("logo_uv") - x0, x1 = CHEST_X - z0, z1 = CHEST_Z + x0, x1 = thr["chest_x"] + z0, z1 = thr["chest_z"] placed = 0 for face in bm.faces: center = face.calc_center_median() @@ -463,37 +573,93 @@ def save_albedo_sidecar(albedo_img, out_path): # Entry # -------------------------------------------------------------------------- -def main(): - argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] - if len(argv) < 2: - print("Usage: -- /average_m " - "[--offset M] [--sleeve-frac F]") - sys.exit(1) - body_dir = argv[0] - out_dir = argv[1] +def author_shell(body_dir, out_dir, glb_name, mask_name, offset): + """Author one offset-shell garment from `body_dir`'s segments. - global OFFSET_M, SLEEVE_FRAC - if "--offset" in argv: - OFFSET_M = float(argv[argv.index("--offset") + 1]) - if "--sleeve-frac" in argv: - SLEEVE_FRAC = float(argv[argv.index("--sleeve-frac") + 1]) - - os.makedirs(out_dir, exist_ok=True) + Shared by both modes; thresholds derive from the body's own armature so + the same proportional cut/mask parameters apply on every body. + """ clear_scene() - shell, armature = build_covered_mesh(body_dir) + thr = derive_thresholds(armature) sleeve_cut(shell, armature) - offset_outward(shell, OFFSET_M) + offset_outward(shell, offset) solidify(shell, CLOTH_THICKNESS_M) albedo_img = make_base_albedo_image() assign_fabric_material(shell, albedo_img) - author_logo_uv(shell) # do UV2 before mask bake (mask uses UV0/active) - bake_region_mask(shell, os.path.join(out_dir, "reference_mask.png")) + author_logo_uv(shell, thr) # do UV2 before mask bake (mask uses UV0/active) + bake_region_mask(shell, os.path.join(out_dir, mask_name), thr) save_albedo_sidecar(albedo_img, os.path.join(out_dir, "base_albedo.png")) - export_reference(shell, armature, os.path.join(out_dir, "average_m.glb")) + export_reference(shell, armature, os.path.join(out_dir, glb_name)) + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- /average_m " + "[--offset M] [--sleeve-frac F]\n" + " or: -- --per-body " + "[--bodies a,b,c] [--offset M] [--sleeve-frac F]") + sys.exit(1) + in_dir = argv[0] + out_dir = argv[1] + per_body = "--per-body" in argv + + global SLEEVE_FRAC + offset = None + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--sleeve-frac" in argv: + SLEEVE_FRAC = float(argv[argv.index("--sleeve-frac") + 1]) + bodies = BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + + if not per_body: + # Single-reference mode: is one body's segment dir. + offset = OFFSET_M if offset is None else offset + body = os.path.basename(os.path.normpath(in_dir)) + author_shell(in_dir, out_dir, f"{body}.glb", "reference_mask.png", offset) + log("DONE") + return + + # Per-body mode: is the bodies root; loop each body's own segments. + offset = PER_BODY_OFFSET_M if offset is None else offset + log(f"per-body mode: {len(bodies)} bodies, offset {offset*1000:.0f} mm") + results = [] + for body in bodies: + body_dir = os.path.join(in_dir, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_shell(body_dir, out_dir, f"{body}.glb", f"{body}_mask.png", + offset) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + results.append((body, f"error: {exc}")) + + # Runtime fallback + SD-fit reference compatibility: reference_mask.png + # mirrors the reference body's mask. + ref_mask = os.path.join(out_dir, f"{REFERENCE_BODY}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {REFERENCE_BODY}_mask.png -> reference_mask.png (fallback)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) log("DONE") diff --git a/tooling/garment-fit/blender_author_offset_shell_legs.py b/tooling/garment-fit/blender_author_offset_shell_legs.py new file mode 100644 index 000000000..9a0b33fa7 --- /dev/null +++ b/tooling/garment-fit/blender_author_offset_shell_legs.py @@ -0,0 +1,373 @@ +""" +blender_author_offset_shell_legs.py (T-1089, lower-body offset-shell garments) + +Companion to blender_author_offset_shell.py (imported as a module — scene +helpers, offset/solidify, albedo, rasterizer and export are reused, not +copied). Where the base script is calibrated for TORSO garments (sleeve cut, +collar/sleeve regions, chest logo UV), this one authors LOWER-BODY garments +from seg_hips + leg segments with parameterized cuts/regions, so one script +serves shorts, jeans, joggers, formal pants, swim trunks: + + --coverage thigh|full thigh: seg_hips + seg_leg_upper_l/r (shorts) + full: + seg_leg_lower_l/r (jeans/joggers/formal) + --hem-frac F fraction of the hem bone's length KEPT below its + head (bone-plane cut). Hem bone = thigh for + --coverage thigh, calf for full. 0.78 on the thigh + = casual shorts hem ~9 cm above the knee. + --waistband-frac F waistband band height as a fraction of + (waist rim z − thigh head z). The band is + classified R in the region mask; everything else + is G. (B/A unused — 2-region garment.) + --fabric R,G,B flat toon-friendly base tone (luma ~0.6 keeps the + toon_garment.gdshader luma-recolor faithful). + --seed N albedo noise seed (deterministic output). + +Cut/region thresholds derive PER BODY from that body's own landmarks, same +philosophy as the base script: the hem plane comes from the thigh/calf bone +(head→tail, identical 65-bone rig on all 11 bodies), the waist rim is the +natural top boundary of seg_hips (its own mesh z-max), so proportions match +across bodies. The waistline itself comes free as a segment boundary — no +waist join geometry needed. + +Regions (RGBA mask, toon_garment.gdshader): waistband → R (tint_0), +body → G (tint_1). A parked logo_uv TEXCOORD_1 layer is authored for channel +consistency with torso garments (all UVs outside [0,1] → shader draws nothing). + +Modes mirror the base script: + +PER-BODY (preferred for offset shells, Q-060): + tooling/blender --background --python \ + tooling/garment-fit/blender_author_offset_shell_legs.py -- \ + --per-body \ + [--bodies a,b,c] [--offset 0.012] [--hem-frac 0.78] [--waistband-frac 0.35] + + Writes /.glb + /_mask.png per body, plus + shared base_albedo.png and reference_mask.png (= average_m's mask, runtime + fallback for the compositor). + +SINGLE-REFERENCE: + tooling/blender --background --python \ + tooling/garment-fit/blender_author_offset_shell_legs.py -- \ + /average_m [--offset 0.020] [...] + +Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house wardrobe). +""" + +import sys +import os +import shutil +import importlib.util + +import bpy # noqa: F401 (Blender runtime) +import bmesh +import numpy as np + +# -------------------------------------------------------------------------- +# Import the base authoring module (shared helpers; main() is __main__-guarded) +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_spec = importlib.util.spec_from_file_location( + "offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py")) +base = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(base) + +log = base.log + +# -------------------------------------------------------------------------- +# Parameters (defaults = shorts_modern) +# -------------------------------------------------------------------------- + +COVERAGE_SEGMENTS = { + "thigh": ["seg_hips", "seg_leg_upper_l", "seg_leg_upper_r"], + "full": ["seg_hips", "seg_leg_upper_l", "seg_leg_upper_r", + "seg_leg_lower_l", "seg_leg_lower_r"], +} +HEM_BONES = {"thigh": ("thigh_l", "thigh_r"), "full": ("calf_l", "calf_r")} + +HEM_FRAC = 0.78 # keep 78% of the thigh → hem above the knee +WAISTBAND_FRAC = 0.35 # top 35% of waist-rim→thigh-head span = waistband (R) +FABRIC_RGB = (0.63, 0.60, 0.53) # warm stone khaki, luma ~0.60 (toon-friendly) +ALBEDO_SEED = 20890 # deterministic albedo noise, distinct from the tshirt + + +# -------------------------------------------------------------------------- +# Per-body threshold derivation +# -------------------------------------------------------------------------- + +def derive_leg_thresholds(armature, coverage, hem_frac): + """Hem plane from this body's own hem bone; thigh head z for the waistband + span. All 11 bodies share the 65-bone rig, so the landmarks always exist.""" + bones = armature.data.bones + hem_l, hem_r = HEM_BONES[coverage] + zs = [] + thigh_heads = [] + for name in (hem_l, hem_r): + b = bones.get(name) + if b is None: + raise RuntimeError(f"hem bone {name} missing on armature") + zs.append(b.head_local.z + hem_frac * (b.tail_local.z - b.head_local.z)) + for name in ("thigh_l", "thigh_r"): + b = bones.get(name) + if b is None: + raise RuntimeError(f"landmark bone {name} missing on armature") + thigh_heads.append(b.head_local.z) + thr = { + "hem_z": sum(zs) / len(zs), + "thigh_head_z": sum(thigh_heads) / len(thigh_heads), + } + log(f"thresholds: hem z>={thr['hem_z']:.3f} " + f"(hem bone {hem_l}/{hem_r}, keep {hem_frac:.2f}) " + f"thigh head z={thr['thigh_head_z']:.3f}") + return thr + + +# -------------------------------------------------------------------------- +# Seam weld — the body segment meshes are assembled from patches whose seam +# vertices are coincident but DUPLICATED. Each copy's normal averages only its +# own patch's faces, so the outward offset pulls seams apart (visible slit at +# the front-centre of seg_hips, V-notches on the waist rim). Welding before +# the offset merges the copies (weights/UVs are identical on coincident verts) +# and gives one smooth normal per seam vertex. +# -------------------------------------------------------------------------- + +def weld_seams(shell, epsilon=1e-4): + bm = bmesh.new() + bm.from_mesh(shell.data) + before = len(bm.verts) + bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=epsilon) + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + after = len(shell.data.vertices) + log(f"seam weld merged {before - after} duplicate verts; {after} remain") + + +# -------------------------------------------------------------------------- +# Bone-plane hem cut (z threshold — legs hang along -Z in rest pose) +# -------------------------------------------------------------------------- + +def hem_cut(shell, hem_z): + """Delete every vert below the hem plane. A single global z threshold is + safe: the hips segment bottoms out far above any sensible hem.""" + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + to_delete = [v for v in bm.verts if v.co.z < hem_z] + bmesh.ops.delete(bm, geom=to_delete, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"hem cut removed {len(to_delete)} verts below z={hem_z:.3f}; " + f"{len(shell.data.vertices)} remain") + + +# -------------------------------------------------------------------------- +# Region mask: waistband (R) / body (G) +# -------------------------------------------------------------------------- + +def bake_region_mask_legs(shell, out_path, thr, waistband_frac): + """Rasterize UV0 faces: waistband → R, everything else → G. + + MUST run PRE-solidify (open boundaries still present). The waistband is + boundary-anchored: the waist rim is the top open boundary loop of seg_hips + (the rim DIPS ~5 cm at the navel, so a global z-max test misses the front + row), so a face is waistband when it touches a rim-boundary vertex — that + keys the full top face row on every tessellation — or when its centre lies + inside the proportional band. Solidify afterwards duplicates the UV loops + unchanged, so the baked texels serve outer shell, inner shell and rim caps + alike.""" + W = H = base.MASK_SIZE + buf = np.zeros((H, W, 4), dtype=np.float32) + buf[:, :, 1] = 1.0 # green background = body (bilinear-bleed safe) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + bm.verts.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for region mask bake") + + waist_rim_z = max(v.co.z for v in bm.verts) + band = waistband_frac * (waist_rim_z - thr["thigh_head_z"]) + wb_z_min = waist_rim_z - band + + # Waist-rim boundary verts: on an open boundary AND above the thigh head + # (the only other boundaries are the leg hems, far below). + rim_verts = set() + for e in bm.edges: + if e.is_boundary: + for v in e.verts: + if v.co.z >= thr["thigh_head_z"]: + rim_verts.add(v.index) + log(f"waistband: rim z={waist_rim_z:.3f} band {band*100:.1f} cm " + f"→ R for z>={wb_z_min:.3f} or touching {len(rim_verts)} rim verts") + + counts = {"waistband": 0, "body": 0} + for face in bm.faces: + in_band = face.calc_center_median().z >= wb_z_min + touches_rim = any(v.index in rim_verts for v in face.verts) + if in_band or touches_rim: + color = (1.0, 0.0, 0.0, 0.0) + counts["waistband"] += 1 + else: + color = (0.0, 1.0, 0.0, 0.0) + counts["body"] += 1 + for a, b, c in base._tris_from_face(face, uv_layer): + base._raster_tri(buf, a, b, c, color, W, H) + bm.free() + total = max(sum(counts.values()), 1) + log("region faces: " + " ".join( + f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in counts.items())) + + img = bpy.data.images.new("garment_region_mask", W, H, alpha=True) + img.pixels.foreach_set(buf.reshape(-1)) + img.update() + img.filepath_raw = out_path + img.file_format = 'PNG' + img.save() + log(f"baked region mask -> {out_path}") + + +def park_logo_uv(shell): + """Author a TEXCOORD_1 layer with every loop parked outside [0,1] — keeps + the UV-channel layout identical to torso garments; the shader's in-box + guard means nothing ever draws.""" + me = shell.data + while len(me.uv_layers) > 1: + me.uv_layers.remove(me.uv_layers[-1]) + me.uv_layers.new(name="logo_uv") + me.uv_layers.active = me.uv_layers[0] + bm = bmesh.new() + bm.from_mesh(me) + uvl = bm.loops.layers.uv.get("logo_uv") + for face in bm.faces: + for loop in face.loops: + loop[uvl].uv = (2.0, 2.0) + bm.to_mesh(me) + bm.free() + me.update() + log("logo UV2 parked (no logo region on this garment)") + + +# -------------------------------------------------------------------------- +# Author one body +# -------------------------------------------------------------------------- + +def author_legs_shell(body_dir, out_dir, glb_name, mask_name, offset, + coverage, hem_frac, waistband_frac, seed): + base.clear_scene() + base.COVERED_SEGMENTS = COVERAGE_SEGMENTS[coverage] + shell, armature = base.build_covered_mesh(body_dir) + weld_seams(shell) + thr = derive_leg_thresholds(armature, coverage, hem_frac) + hem_cut(shell, thr["hem_z"]) + base.offset_outward(shell, offset) + + # Region mask + parked logo UV are authored PRE-solidify: the boundary- + # anchored waistband needs the open waist rim, and solidify duplicates all + # UV loops unchanged so the baked texels stay valid for the final mesh. + park_logo_uv(shell) + bake_region_mask_legs(shell, os.path.join(out_dir, mask_name), thr, + waistband_frac) + + base.solidify(shell, base.CLOTH_THICKNESS_M) + + base.FABRIC_RGB = FABRIC_RGB + albedo_img = base.make_base_albedo_image(seed=seed) + base.assign_fabric_material(shell, albedo_img) + base.save_albedo_sidecar(albedo_img, os.path.join(out_dir, "base_albedo.png")) + + base.export_reference(shell, armature, os.path.join(out_dir, glb_name)) + + +# -------------------------------------------------------------------------- +# Entry +# -------------------------------------------------------------------------- + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- /average_m [--offset M] " + "[--coverage thigh|full] [--hem-frac F] [--waistband-frac F] " + "[--fabric R,G,B] [--seed N]\n" + " or: -- --per-body [--bodies a,b,c] " + "[same flags]") + sys.exit(1) + in_dir = argv[0] + out_dir = argv[1] + per_body = "--per-body" in argv + + global FABRIC_RGB + offset = None + coverage = "thigh" + hem_frac = HEM_FRAC + waistband_frac = WAISTBAND_FRAC + seed = ALBEDO_SEED + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--coverage" in argv: + coverage = argv[argv.index("--coverage") + 1] + if coverage not in COVERAGE_SEGMENTS: + print(f"unknown --coverage {coverage}") + sys.exit(1) + if "--hem-frac" in argv: + hem_frac = float(argv[argv.index("--hem-frac") + 1]) + if "--waistband-frac" in argv: + waistband_frac = float(argv[argv.index("--waistband-frac") + 1]) + if "--fabric" in argv: + FABRIC_RGB = tuple( + float(c) for c in argv[argv.index("--fabric") + 1].split(",")) + if "--seed" in argv: + seed = int(argv[argv.index("--seed") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + + os.makedirs(out_dir, exist_ok=True) + + if not per_body: + offset = base.OFFSET_M if offset is None else offset + body = os.path.basename(os.path.normpath(in_dir)) + author_legs_shell(in_dir, out_dir, f"{body}.glb", "reference_mask.png", + offset, coverage, hem_frac, waistband_frac, seed) + log("DONE") + return + + offset = base.PER_BODY_OFFSET_M if offset is None else offset + log(f"per-body mode: {len(bodies)} bodies, offset {offset*1000:.0f} mm, " + f"coverage={coverage} hem_frac={hem_frac} waistband_frac={waistband_frac}") + results = [] + for body in bodies: + body_dir = os.path.join(in_dir, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_legs_shell(body_dir, out_dir, f"{body}.glb", + f"{body}_mask.png", offset, coverage, hem_frac, + waistband_frac, seed) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + results.append((body, f"error: {exc}")) + + ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {base.REFERENCE_BODY}_mask.png -> reference_mask.png (fallback)") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-fit/blender_author_outerwear.py b/tooling/garment-fit/blender_author_outerwear.py new file mode 100644 index 000000000..442c8f84c --- /dev/null +++ b/tooling/garment-fit/blender_author_outerwear.py @@ -0,0 +1,445 @@ +""" +blender_author_outerwear.py (T-1089, jacket_modern — open-front outerwear family) + +Companion to blender_author_offset_shell.py: authors OUTERWEAR offset-shells +(jacket / track-jacket / parka family) per body. It imports the base module and +reuses its scene/join/offset/solidify/mask/logo/export machinery; everything +garment-specific lives in the PARAMS block below, so a new outerwear piece is a +parameter delta, not a new script. + +What it adds over the base t-shirt shell: + + * FULL SLEEVES — covers seg_arm_lower_l/r too; the sleeve cut moves from the + upper arm to a WRIST cut on the lowerarm bones (same bone-plane technique). + * 4th REGION (A = zip/trim) — a front zip placket strip running hem -> through + the collar, plus knit cuff bands on the sleeve ends. Region layout per spec: + collar=R, body=G, sleeves=B, zip/trim=A. + * POSITION-PAINTED ALBEDO — instead of the base script's flat-noise albedo, + each body gets an albedo baked in its own UV0 layout by rasterizing every + face and painting per-texel from interpolated body-local position: zip teeth + dashes + placket, panel seam lines (shoulder, collar, cuff, placket edges), + hem band. The toon_garment shader recolors by LUMA, so the painted detail is + authored as luma contrast and survives any tint choice. + * LEFT-BREAST LOGO PATCH — the UV2 chest box shifts off-centre (the zip owns + the centre line), scaled per body from the same bone-landmark ratios. + +Per-body only (this family is the poster child for the Q-060 per-body route): + + tooling/blender --background --python \ + tooling/garment-fit/blender_author_outerwear.py -- \ + \ + [--bodies a,b,c] [--offset 0.022] [--cuff-keep 0.88] + +Writes per body: /.glb (painted albedo embedded), _mask.png +Plus: /base_albedo.png (average_m's painted albedo) + /reference_mask.png (copy of average_m's, fallback) + +The per-body albedo is embedded in the GLB under the image name "base_albedo" +(tshirt convention): Godot's importer extracts it as _base_albedo.png. +average_m is deliberately authored LAST so the shared base_albedo.png sidecar +ends up holding the reference body's pixels. + +Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060. +""" + +import importlib.util +import os +import shutil +import sys + +import bpy +import bmesh +import numpy as np + +# -------------------------------------------------------------------------- +# Load the base offset-shell module (shared engine machinery). +# -------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_spec = importlib.util.spec_from_file_location( + "offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py")) +base = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(base) + +log = base.log + +# -------------------------------------------------------------------------- +# PARAMS — jacket_modern (casual zip jacket). A new outerwear garment should +# only need to touch this block (or override via CLI where exposed). +# -------------------------------------------------------------------------- + +# Full-arm coverage: torso + torso_upper + both whole arms. +SEGMENTS = [ + "seg_torso", "seg_torso_upper", + "seg_arm_upper_l", "seg_arm_upper_r", + "seg_arm_lower_l", "seg_arm_lower_r", +] + +OFFSET_M = 0.022 # LARGEST standoff of the tops — worn over a shirt + # (tshirt_modern outer surface ~16 mm), must neither + # clip the body nor read skin-tight. +CLOTH_THICKNESS_M = 0.005 # beefier cloth than the 4 mm tee + +CUFF_KEEP_FRAC = 0.88 # fraction of the lowerarm kept (wrist cut — hands show) +CUFF_BAND_FRAC = 0.22 # last fraction of the KEPT forearm = knit cuff trim (A) + +# Region-shape ratios, anchored to the base module's average_m reference +# landmarks (shoulder |x| 0.1919, neck len 0.0793) so they scale per body. +ZIP_HALF_FRAC = 0.030 / 0.1919 # zip placket half-width as frac of shoulder |x| +COLLAR_DROP_NECK_FRAC = 0.55 # collar band starts this far below neck head + # (t-shirt band was 0.3846 — jacket collar is taller, + # but 0.80 caught upper-chest faces and rendered as + # jagged shoulder "wings"; 0.55 keeps a neat band) +COLLAR_X_MULT = 1.15 # jacket collar slightly wider than the tee band + +# Left-breast logo patch (character-left = +X; centre line belongs to the zip). +# Fracs of shoulder |x| / spine span, from average_m absolutes (0.035..0.115 m, +# z 1.30..1.42 m). +CHEST_X_LO_FRAC = 0.035 / 0.1919 +CHEST_X_HI_FRAC = 0.115 / 0.1919 +CHEST_Z_LO_FRAC = (1.30 - 1.072) / (1.5205 - 1.072) +CHEST_Z_HI_FRAC = (1.42 - 1.072) / (1.5205 - 1.072) + +# Painted-albedo luma palette (the shader tints by luma: tint * (luma*1.5+0.2)). +BASE_LUMA = 0.58 +PLACKET_LUMA = 0.50 # slightly darker front placket band +SEAM_LUMA = 0.38 # panel seam / stitch lines +TEETH_LUMA = 0.88 # bright zip teeth dashes +TEETH_GAP_LUMA = 0.34 # dark tape between dashes +CUFF_LUMA = 0.48 # knit cuff band +HEM_LUMA = 0.50 # bottom hem band +ALBEDO_NOISE = 0.02 # woven jitter +SEAM_W = 0.005 # seam line half-width, metres +TEETH_HALF_W = 0.006 # zip teeth strip half-width, metres +TEETH_PERIOD = 0.024 # dash period along Z, metres +TEETH_DUTY = 0.014 # bright dash length within a period, metres +HEM_BAND_M = 0.020 # hem band height, metres +# Slate hue applied on top of luma (only visible if the region mask is missing). +FABRIC_HUE = np.array([0.93, 0.97, 1.06], dtype=np.float32) + +ALBEDO_SIZE = 512 + + +# -------------------------------------------------------------------------- +# Thresholds — base derivation + outerwear extras from the same armature +# -------------------------------------------------------------------------- + +def outerwear_thresholds(armature): + thr = base.derive_thresholds(armature) + bones = armature.data.bones + neck = bones.get("neck_01") + ua_l = bones.get("upperarm_l") + ua_r = bones.get("upperarm_r") + spine01 = bones.get("spine_01") + + # Shoulder |x| (same landmark the base derivation uses). + if ua_l and ua_r: + shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 + else: + shoulder_x = 0.1919 # average_m fallback + + # Taller, wider standing collar. + if neck: + neck_len = neck.tail_local.z - neck.head_local.z + thr["collar_z_min"] = neck.head_local.z - COLLAR_DROP_NECK_FRAC * neck_len + thr["collar_x_abs"] = thr["collar_x_abs"] * COLLAR_X_MULT + + # Front zip placket. + thr["zip_half_x"] = shoulder_x * ZIP_HALF_FRAC + + # Wrist cut planes + cuff trim band on the lowerarm bones. + cut_planes = [] + cuff_abs = [] + for bone_name, sign in [("lowerarm_l", +1), ("lowerarm_r", -1)]: + b = bones.get(bone_name) + if b is None: + log(f"WARNING: bone {bone_name} missing — sleeve left full-length") + continue + head_x = b.head_local.x + tail_x = b.tail_local.x + cut_x = head_x + CUFF_KEEP_FRAC * (tail_x - head_x) + band_x = head_x + (CUFF_KEEP_FRAC - CUFF_BAND_FRAC) * (tail_x - head_x) + cut_planes.append((sign, cut_x)) + cuff_abs.append(abs(band_x)) + log(f"wrist cut {bone_name}: keep |x| up to {cut_x:.3f} " + f"(elbow {head_x:.3f} -> wrist {tail_x:.3f}), cuff from {band_x:.3f}") + thr["wrist_cut_planes"] = cut_planes + thr["cuff_x_abs"] = min(cuff_abs) if cuff_abs else 1e9 + + # Left-breast logo patch (replaces the base full-chest box). + thr["chest_x"] = (shoulder_x * CHEST_X_LO_FRAC, shoulder_x * CHEST_X_HI_FRAC) + if neck and spine01: + spine_lo = spine01.head_local.z + span = neck.head_local.z - spine_lo + thr["chest_z"] = (spine_lo + CHEST_Z_LO_FRAC * span, + spine_lo + CHEST_Z_HI_FRAC * span) + log(f"outerwear thresholds: zip half {thr['zip_half_x']:.3f} " + f"collar z>={thr['collar_z_min']:.3f} |x|<{thr['collar_x_abs']:.3f} " + f"cuff |x|>={thr['cuff_x_abs']:.3f} " + f"chest x=({thr['chest_x'][0]:.3f},{thr['chest_x'][1]:.3f}) " + f"z=({thr['chest_z'][0]:.3f},{thr['chest_z'][1]:.3f})") + return thr + + +# -------------------------------------------------------------------------- +# Wrist cut (bone-plane, same technique as the base sleeve cut) +# -------------------------------------------------------------------------- + +def wrist_cut(shell, thr): + """Delete forearm verts beyond the wrist plane on each side.""" + cut_planes = thr.get("wrist_cut_planes", []) + if not cut_planes: + log("WARNING: no wrist cut planes — sleeves stay full length") + return + bm = bmesh.new() + bm.from_mesh(shell.data) + bm.verts.ensure_lookup_table() + to_delete = [] + for v in bm.verts: + for sign, thr_x in cut_planes: + if sign > 0 and v.co.x > thr_x: + to_delete.append(v) + break + if sign < 0 and v.co.x < thr_x: + to_delete.append(v) + break + bmesh.ops.delete(bm, geom=to_delete, context='VERTS') + bm.to_mesh(shell.data) + bm.free() + shell.data.update() + log(f"wrist cut removed {len(to_delete)} verts; " + f"{len(shell.data.vertices)} remain") + + +# -------------------------------------------------------------------------- +# 4-region classifier (collar=R, body=G, sleeves=B, zip/trim=A) +# -------------------------------------------------------------------------- + +def classify_region_outerwear(center, thr): + x, y, z = center.x, center.y, center.z + ax = abs(x) + front = y * base.FRONT_Y_SIGN > 0.0 + # Zip placket first: runs hem -> THROUGH the collar (zip-through jacket). + if front and ax < thr["zip_half_x"]: + return (0.0, 0.0, 0.0, 1.0) # zip/trim -> A (tint[3]) + if ax >= thr["cuff_x_abs"]: + return (0.0, 0.0, 0.0, 1.0) # knit cuff trim -> A (tint[3]) + if ax >= thr["sleeve_x_abs"]: + return (0.0, 0.0, 1.0, 0.0) # sleeves -> B (tint[2]) + if z >= thr["collar_z_min"] and ax < thr["collar_x_abs"]: + return (1.0, 0.0, 0.0, 0.0) # collar -> R (tint[0]) + return (0.0, 1.0, 0.0, 0.0) # main body -> G (tint[1]) + + +# -------------------------------------------------------------------------- +# Position-painted albedo (per-body UV0 bake) +# -------------------------------------------------------------------------- + +def _paint_luma(x, y, z, thr, z_min): + """Vectorised luma paint from body-local position (arrays in, array out).""" + v = np.full(x.shape, BASE_LUMA, dtype=np.float32) + ax = np.abs(x) + front = (y * base.FRONT_Y_SIGN) > 0.0 + + # Hem band along the jacket bottom. + v = np.where(z < z_min + HEM_BAND_M, HEM_LUMA, v) + # Knit cuff bands. + v = np.where(ax >= thr["cuff_x_abs"], CUFF_LUMA, v) + # Panel seam lines: shoulder (sleeve boundary), cuff start, collar base. + v = np.where(np.abs(ax - thr["sleeve_x_abs"]) < SEAM_W, SEAM_LUMA, v) + v = np.where(np.abs(ax - thr["cuff_x_abs"]) < SEAM_W, SEAM_LUMA, v) + collar_seam = (np.abs(z - thr["collar_z_min"]) < SEAM_W) \ + & (ax < thr["collar_x_abs"] * 1.2) + v = np.where(collar_seam, SEAM_LUMA, v) + + # Front zip placket: darker band + stitch edges + dashed teeth line. + zh = thr["zip_half_x"] + v = np.where(front & (ax < zh), PLACKET_LUMA, v) + v = np.where(front & (np.abs(ax - zh) < SEAM_W * 0.6), SEAM_LUMA, v) + teeth = front & (ax < TEETH_HALF_W) + dash = np.mod(z, TEETH_PERIOD) < TEETH_DUTY + v = np.where(teeth & dash, TEETH_LUMA, v) + v = np.where(teeth & ~dash, TEETH_GAP_LUMA, v) + return v + + +def _raster_tri_painted(lum, uv_a, uv_b, uv_c, p_a, p_b, p_c, thr, z_min, W, H): + """Barycentric fill of a UV triangle, painting per-texel from the + barycentrically interpolated body-local position.""" + ax_, ay_ = uv_a.x * (W - 1), uv_a.y * (H - 1) + bx_, by_ = uv_b.x * (W - 1), uv_b.y * (H - 1) + cx_, cy_ = uv_c.x * (W - 1), uv_c.y * (H - 1) + minx = max(int(np.floor(min(ax_, bx_, cx_))), 0) + maxx = min(int(np.ceil(max(ax_, bx_, cx_))), W - 1) + miny = max(int(np.floor(min(ay_, by_, cy_))), 0) + maxy = min(int(np.ceil(max(ay_, by_, cy_))), H - 1) + if minx > maxx or miny > maxy: + return + denom = (by_ - cy_) * (ax_ - cx_) + (cx_ - bx_) * (ay_ - cy_) + if abs(denom) < 1e-9: + return + ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1] + px = xs + 0.5 + py = ys + 0.5 + w0 = ((by_ - cy_) * (px - cx_) + (cx_ - bx_) * (py - cy_)) / denom + w1 = ((cy_ - ay_) * (px - cx_) + (ax_ - cx_) * (py - cy_)) / denom + w2 = 1.0 - w0 - w1 + inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) + if not inside.any(): + return + pos_x = w0 * p_a.x + w1 * p_b.x + w2 * p_c.x + pos_y = w0 * p_a.y + w1 * p_b.y + w2 * p_c.y + pos_z = w0 * p_a.z + w1 * p_b.z + w2 * p_c.z + vals = _paint_luma(pos_x, pos_y, pos_z, thr, z_min) + region = lum[miny:maxy + 1, minx:maxx + 1] + region[inside] = vals[inside] + + +def bake_painted_albedo(shell, thr, out_path, seed=1093): + """Bake a per-body albedo: luma-painted detail in this body's UV0 layout.""" + W = H = ALBEDO_SIZE + lum = np.full((H, W), BASE_LUMA, dtype=np.float32) + + me = shell.data + bm = bmesh.new() + bm.from_mesh(me) + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.active + if uv_layer is None: + raise RuntimeError("no active UV layer for albedo bake") + + z_min = min(v.co.z for v in bm.verts) + for face in bm.faces: + loops = face.loops[:] + uvs = [loop[uv_layer].uv.copy() for loop in loops] + pos = [loop.vert.co.copy() for loop in loops] + for i in range(1, len(uvs) - 1): + _raster_tri_painted(lum, uvs[0], uvs[i], uvs[i + 1], + pos[0], pos[i], pos[i + 1], thr, z_min, W, H) + bm.free() + + rng = np.random.default_rng(seed) + lum += (rng.random((H, W), dtype=np.float32) - 0.5) * 2.0 * ALBEDO_NOISE + rgb = np.clip(lum[:, :, None] * FABRIC_HUE[None, None, :], 0.0, 1.0) + rgba = np.concatenate( + [rgb, np.ones((H, W, 1), dtype=np.float32)], axis=2).astype(np.float32) + + img = bpy.data.images.new("garment_painted_albedo", W, H, alpha=False) + img.pixels.foreach_set(rgba.reshape(-1)) + img.update() + img.filepath_raw = out_path + img.file_format = 'PNG' + img.save() + log(f"baked painted albedo -> {out_path}") + return img + + +# -------------------------------------------------------------------------- +# Per-body authoring +# -------------------------------------------------------------------------- + +def author_outerwear(body_dir, out_dir, body, offset): + base.clear_scene() + shell, armature = base.build_covered_mesh(body_dir) + thr = outerwear_thresholds(armature) + wrist_cut(shell, thr) + base.offset_outward(shell, offset) + base.solidify(shell, CLOTH_THICKNESS_M) + + # Shared sidecar name on purpose: the exporter derives the embedded glTF + # image name from the filepath basename, and Godot extracts embedded + # textures as _.png — so this yields _base_albedo.png + # on import, matching the tshirt_modern convention (no doubled names). + albedo_path = os.path.join(out_dir, "base_albedo.png") + albedo_img = bake_painted_albedo(shell, thr, albedo_path) + base.assign_fabric_material(shell, albedo_img) + + base.author_logo_uv(shell, thr) # UV2 before mask bake (mask uses UV0/active) + base.bake_region_mask(shell, os.path.join(out_dir, f"{body}_mask.png"), thr) + + # Honest region tally (the base bake log can't see the A channel). + counts = {"collar": 0, "body": 0, "sleeve": 0, "zip/trim": 0} + bm = bmesh.new() + bm.from_mesh(shell.data) + for face in bm.faces: + c = classify_region_outerwear(face.calc_center_median(), thr) + if c[3] > 0.5: + counts["zip/trim"] += 1 + elif c[2] > 0.5: + counts["sleeve"] += 1 + elif c[0] > 0.5: + counts["collar"] += 1 + else: + counts["body"] += 1 + bm.free() + total = max(sum(counts.values()), 1) + log("outerwear regions: " + " ".join( + f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in counts.items())) + + base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(argv) < 2: + print("Usage: -- " + "[--bodies a,b,c] [--offset M] [--cuff-keep F]") + sys.exit(1) + bodies_root = argv[0] + out_dir = argv[1] + + global CUFF_KEEP_FRAC + offset = OFFSET_M + if "--offset" in argv: + offset = float(argv[argv.index("--offset") + 1]) + if "--cuff-keep" in argv: + CUFF_KEEP_FRAC = float(argv[argv.index("--cuff-keep") + 1]) + bodies = base.BODY_TYPES + if "--bodies" in argv: + bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] + # Author the reference body LAST so the shared base_albedo.png sidecar + # (rewritten per body before each export) ends as average_m's version. + if base.REFERENCE_BODY in bodies: + bodies = [b for b in bodies if b != base.REFERENCE_BODY] + [base.REFERENCE_BODY] + + os.makedirs(out_dir, exist_ok=True) + + # Route the base mask bake through the 4-region outerwear classifier and + # the covered-segment import through the full-arm list. + base._classify_region = classify_region_outerwear + base.COVERED_SEGMENTS = SEGMENTS + + log(f"outerwear per-body mode: {len(bodies)} bodies, " + f"offset {offset*1000:.0f} mm, cuff keep {CUFF_KEEP_FRAC:.2f}") + results = [] + for body in bodies: + body_dir = os.path.join(bodies_root, body) + log(f"=== {body} ===") + if not os.path.isdir(body_dir): + results.append((body, "skipped: body dir missing")) + continue + try: + author_outerwear(body_dir, out_dir, body, offset) + results.append((body, "ok")) + except Exception as exc: + log(f"ERROR {body}: {exc}") + results.append((body, f"error: {exc}")) + + # Runtime fallback: reference_mask.png mirrors average_m's mask. + # (base_albedo.png already holds average_m's albedo — it authored last.) + ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png") + if os.path.isfile(ref_mask): + shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) + log(f"copied {base.REFERENCE_BODY}_mask.png -> reference_mask.png") + + log("=" * 50) + for body, status in results: + log(f" {body:12s} {status}") + ok = sum(1 for _, s in results if s == "ok") + log(f"OK={ok}/{len(results)}") + if ok != len(results): + sys.exit(1) + log("DONE") + + +if __name__ == "__main__": + main() diff --git a/tooling/garment-qa/configs/buttondown_modern.json b/tooling/garment-qa/configs/buttondown_modern.json new file mode 100644 index 000000000..9ca64f7d0 --- /dev/null +++ b/tooling/garment-qa/configs/buttondown_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "buttondown_modern", "slot": "torso"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/buttondown_modern" +} diff --git a/tooling/garment-qa/configs/hoodie_modern.json b/tooling/garment-qa/configs/hoodie_modern.json new file mode 100644 index 000000000..addf921e7 --- /dev/null +++ b/tooling/garment-qa/configs/hoodie_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "hoodie_modern", "slot": "torso"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/hoodie_modern" +} diff --git a/tooling/garment-qa/configs/jacket_modern.json b/tooling/garment-qa/configs/jacket_modern.json new file mode 100644 index 000000000..6ac67fbdc --- /dev/null +++ b/tooling/garment-qa/configs/jacket_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "jacket_modern", "slot": "torso"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/jacket_modern" +} diff --git a/tooling/garment-qa/configs/jeans_modern.json b/tooling/garment-qa/configs/jeans_modern.json new file mode 100644 index 000000000..de5dafdec --- /dev/null +++ b/tooling/garment-qa/configs/jeans_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "jeans_modern", "slot": "legs"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/jeans_modern" +} diff --git a/tooling/garment-qa/configs/pants_formal.json b/tooling/garment-qa/configs/pants_formal.json new file mode 100644 index 000000000..425bfada9 --- /dev/null +++ b/tooling/garment-qa/configs/pants_formal.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "pants_formal", "slot": "legs"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/pants_formal" +} diff --git a/tooling/garment-qa/configs/shorts_modern.json b/tooling/garment-qa/configs/shorts_modern.json new file mode 100644 index 000000000..c8afd6b86 --- /dev/null +++ b/tooling/garment-qa/configs/shorts_modern.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "shorts_modern", "slot": "legs"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/shorts_modern" +} diff --git a/tooling/garment-qa/configs/suit_jacket_black.json b/tooling/garment-qa/configs/suit_jacket_black.json new file mode 100644 index 000000000..7bc30b8bc --- /dev/null +++ b/tooling/garment-qa/configs/suit_jacket_black.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "suit_jacket_black", "slot": "torso"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/suit_jacket_black" +} diff --git a/tooling/garment-qa/configs/tshirt_modern.json b/tooling/garment-qa/configs/tshirt_modern.json index 24b52b3e6..a504d8e5c 100644 --- a/tooling/garment-qa/configs/tshirt_modern.json +++ b/tooling/garment-qa/configs/tshirt_modern.json @@ -2,7 +2,7 @@ "garments": [ {"item_id": "tshirt_modern", "slot": "torso"} ], - "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "teen_m", "teen_f"], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], "clips": ["Walk", "Sprint", "Crouch_Fwd"], "frames_per_clip": 3, "yaws": [0, 90, 180, 270], diff --git a/tooling/garment-qa/configs/uniform_utility.json b/tooling/garment-qa/configs/uniform_utility.json new file mode 100644 index 000000000..c8ac4f4fd --- /dev/null +++ b/tooling/garment-qa/configs/uniform_utility.json @@ -0,0 +1,15 @@ +{ + "garments": [ + {"item_id": "uniform_utility", "slot": "full_body"} + ], + "body_types": ["average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f", "heavy_m", "heavy_f", "teen_m", "teen_f", "child"], + "clips": ["Walk", "Sprint", "Crouch_Fwd"], + "frames_per_clip": 3, + "yaws": [0, 90, 180, 270], + "clip_epsilon_m": 0.03, + "head_id": "head_001", + "hair_id": "buzzed", + "eyebrow_id": "regular", + "skin_tone": 3, + "out_dir": "/var/mnt/data/projects/settled-reach/.cache/garment-qa/uniform_utility" +}