diff --git a/assets/characters/animations/_cmu_locomotion.glb b/assets/characters/animations/_cmu_locomotion.glb new file mode 100644 index 0000000..3b02848 Binary files /dev/null and b/assets/characters/animations/_cmu_locomotion.glb differ diff --git a/assets/characters/animations/_cmu_locomotion_SOURCE.md b/assets/characters/animations/_cmu_locomotion_SOURCE.md new file mode 100644 index 0000000..2e80737 --- /dev/null +++ b/assets/characters/animations/_cmu_locomotion_SOURCE.md @@ -0,0 +1,24 @@ +# CMU backward locomotion source + +`_cmu_locomotion.glb` contains one animation-only clip: + +- `RunBackward` — CMU Graphics Lab Motion Capture Database, subject 76, + trial 11: “quick large steps backwards.” + +Source BVH: +https://github.com/una-dinosauria/cmu-mocap/blob/master/data/076/76_11.bvh + +Original motion database and trial description: +https://mocap.cs.cmu.edu/ + +The BVH is Bruce Hahne’s conversion of the original CMU motion-capture data. +CMU’s FAQ permits copying, modifying, and redistributing the data (including +commercial use), requests acknowledgment, and prohibits selling the database +itself. This project uses a cropped, in-place gait cycle as part of the game, +not as a standalone resale of the database. + +Build command: + +```text +blender --background --python tools/build_cmu_locomotion_library.py -- 76_11.bvh assets/characters/animations/_cmu_locomotion.glb +``` diff --git a/assets/characters/animations/_directional.glb b/assets/characters/animations/_directional.glb new file mode 100644 index 0000000..3c90cae Binary files /dev/null and b/assets/characters/animations/_directional.glb differ diff --git a/assets/characters/animations/_directional_LICENSE.md b/assets/characters/animations/_directional_LICENSE.md new file mode 100644 index 0000000..71c67e7 --- /dev/null +++ b/assets/characters/animations/_directional_LICENSE.md @@ -0,0 +1,17 @@ +# Godot TPS Demo directional animations + +Source: https://github.com/godotengine/tps-demo + +The animation-only `_directional.glb` contains the `strafe_front-cycle`, +`strafe_back-cycle`, `strafe_left-cycle`, and `strafe_right-cycle` actions from +the Godot Third Person Shooter Demo player model. Meshes, textures, and +unrelated animations were removed. The game retargets these authored cycles to +its Quaternius-compatible character skeleton. + +Original assets Copyright (c) 2018 Juan Linietsky and Fernando Miguel Calabró. + +Distributed under the Creative Commons Attribution 3.0 license: +https://creativecommons.org/licenses/by/3.0/ + +Changes: animation-only extraction, retargeting, in-place root motion, and +runtime phase-synchronized blending with the project's authored walk/run clips. diff --git a/assets/characters/animations/_library_v2.glb b/assets/characters/animations/_library_v2.glb new file mode 100644 index 0000000..1ba5b00 Binary files /dev/null and b/assets/characters/animations/_library_v2.glb differ diff --git a/assets/characters/animations/_library_v2_LICENSE.txt b/assets/characters/animations/_library_v2_LICENSE.txt new file mode 100644 index 0000000..5fdd2d5 --- /dev/null +++ b/assets/characters/animations/_library_v2_LICENSE.txt @@ -0,0 +1,12 @@ +Universal Animation Library 2 [Standard] +Author: Quaternius +Source: https://quaternius.itch.io/universal-animation-library-2 + +License: +CC0 1.0 Universal (CC0 1.0) +Public Domain Dedication +https://creativecommons.org/publicdomain/zero/1.0/ + +The committed _library_v2.glb is the non-root-motion Unreal/Godot export from +the free Standard package. The game drives world movement; authored vertical +body motion remains in the clips. diff --git a/assets/characters/animations/_mesh2motion.glb b/assets/characters/animations/_mesh2motion.glb new file mode 100644 index 0000000..bcbd2d7 Binary files /dev/null and b/assets/characters/animations/_mesh2motion.glb differ diff --git a/assets/characters/animations/_mesh2motion_LICENSE.txt b/assets/characters/animations/_mesh2motion_LICENSE.txt new file mode 100644 index 0000000..3a6d655 --- /dev/null +++ b/assets/characters/animations/_mesh2motion_LICENSE.txt @@ -0,0 +1,23 @@ +Mesh2Motion Human Animation Library + +Source: +https://github.com/Mesh2Motion/mesh2motion-app +https://github.com/Mesh2Motion/mesh2motion-assets + +License: +CC0 1.0 Universal (CC0 1.0) +Public Domain Dedication +https://creativecommons.org/publicdomain/zero/1.0/ + +The committed _mesh2motion.glb is an animation-only subset of +static/animations/human-addon-animations.glb. It retains these authored clips: + +- Run_Anime +- Strafe_left +- Strafe_right +- Flying Forward Super + +Meshes, textures, and unrelated actions were removed. Run_Anime is retargeted +offline into the four cardinal high-speed travel headings and the wall-run +loop; the animator's keyframed limb motion is preserved. Flying Forward Super +provides the grapple flight pose. diff --git a/assets/characters/animations/_wallrun.glb b/assets/characters/animations/_wallrun.glb new file mode 100644 index 0000000..d21f47d Binary files /dev/null and b/assets/characters/animations/_wallrun.glb differ diff --git a/assets/characters/animations/_wallrun_SOURCE.md b/assets/characters/animations/_wallrun_SOURCE.md new file mode 100644 index 0000000..b639eb6 --- /dev/null +++ b/assets/characters/animations/_wallrun_SOURCE.md @@ -0,0 +1,14 @@ +# Authored wall-run animation source + +`_wallrun.glb` contains the animation-only UE4 Mannequin clips +`WallRunLeft` and `WallRunRight`. + +- Author: Uisco +- Source: https://uisco.itch.io/wall-running-animations +- Original files: `wall_run_left.FBX`, `wall_run_right.FBX` +- Published: 2021-03-24 +- Listing: free / name-your-own-price game asset + +The source listing does not state a named open-content license. Keep this +provenance file with the embedded game asset, credit Uisco, and do not resell +or redistribute the clips as a standalone animation pack. diff --git a/assets/characters/skins/aria.glb b/assets/characters/skins/aria.glb index f581dc3..f0f3340 100644 Binary files a/assets/characters/skins/aria.glb and b/assets/characters/skins/aria.glb differ diff --git a/assets/characters/skins/aria.rig.json b/assets/characters/skins/aria.rig.json index 4fa8751..40c90cd 100644 --- a/assets/characters/skins/aria.rig.json +++ b/assets/characters/skins/aria.rig.json @@ -1,2673 +1,2673 @@ { - "roles": { - "hips": "Hips", - "head": "Head", - "neck": "Neck", - "spine": [ - "Spine", - "Chest", - "Neck", - "Head" + "roles": { + "hips": "Hips", + "head": "Head", + "neck": "Neck", + "spine": [ + "Spine", + "Chest", + "Neck", + "Head" + ], + "shoulder.L": "Left shoulder", + "shoulder.R": "Right shoulder", + "foot.R": "Right ankle", + "foot.L": "Left ankle", + "hand.L": "Left wrist", + "hand.R": "Right wrist", + "forearm.R": "Right elbow", + "forearm.L": "Left elbow", + "shin.R": "Right knee", + "shin.L": "Left knee", + "toe.R": "Right toe", + "toe.L": "Left toe", + "upper_arm.R": "Right arm", + "upper_arm.L": "Left arm", + "thigh.L": "Left leg", + "thigh.R": "Right leg" + }, + "fingers": { + "thumb.L": [ + "Thumb0_L", + "Thumb1_L", + "Thumb2_L" + ], + "index.L": [ + "IndexFinger1_L", + "IndexFinger2_L", + "IndexFinger3_L" + ], + "middle.L": [ + "MiddleFinger1_L", + "MiddleFinger2_L", + "MiddleFinger3_L" + ], + "ring.L": [ + "RingFinger1_L", + "RingFinger2_L", + "RingFinger3_L" + ], + "pinky.L": [ + "LittleFinger1_L", + "LittleFinger2_L", + "LittleFinger3_L" + ], + "thumb.R": [ + "Thumb0_R", + "Thumb1_R", + "Thumb2_R" + ], + "index.R": [ + "IndexFinger1_R", + "IndexFinger2_R", + "IndexFinger3_R" + ], + "middle.R": [ + "MiddleFinger1_R", + "MiddleFinger2_R", + "MiddleFinger3_R" + ], + "ring.R": [ + "RingFinger1_R", + "RingFinger2_R", + "RingFinger3_R" + ], + "pinky.R": [ + "LittleFinger1_R", + "LittleFinger2_R", + "LittleFinger3_R" + ] + }, + "chains": [ + { + "class": "breast", + "root_parent": "Chest", + "bones": [ + "Breast_L", + "J_Sec_L_Bust2" + ], + "tips": [ + [ + 0.0, + 0.09099, + 0.0 + ], + [ + 0.0, + 0.09099, + 0.0 + ] + ], + "hulls": [ + [ + [ + 0.00917, + 0.0933, + -0.00744 + ], + [ + -0.07418, + 0.03013, + 0.00578 + ], + [ + 0.00133, + 0.03457, + 0.05712 + ], + [ + -0.01478, + 0.03942, + -0.06075 + ], + [ + 0.04781, + 0.06869, + 0.03379 + ], + [ + 0.04463, + 0.0526, + -0.02954 + ], + [ + -0.03647, + 0.06666, + 0.01678 + ], + [ + -0.0479, + 0.0376, + 0.05365 + ], + [ + -0.04619, + 0.06094, + -0.03406 + ], + [ + 0.0027, + 0.07461, + 0.02918 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_L_Bust2": 9.193998838054217, + "Breast_R": 0.3764394436581815, + "J_Sec_R_Bust2": 0.003499964148881679 + }, + { + "Breast_L": 9.193998838054217, + "Breast_R": 0.0034999641116944535 + } + ] + }, + { + "class": "breast", + "root_parent": "Chest", + "bones": [ + "Breast_R", + "J_Sec_R_Bust2" + ], + "tips": [ + [ + 0.0, + 0.09099, + 0.0 + ], + [ + 0.0, + 0.09099, + 0.0 + ] + ], + "hulls": [ + [ + [ + -0.00917, + 0.0933, + -0.00744 + ], + [ + 0.07418, + 0.03013, + 0.00578 + ], + [ + -0.00133, + 0.03457, + 0.05712 + ], + [ + 0.01478, + 0.03942, + -0.06075 + ], + [ + -0.04781, + 0.06869, + 0.03379 + ], + [ + -0.04463, + 0.0526, + -0.02954 + ], + [ + 0.03647, + 0.06666, + 0.01678 + ], + [ + 0.0479, + 0.0376, + 0.05365 + ], + [ + 0.04619, + 0.06094, + -0.03406 + ], + [ + -0.0027, + 0.07461, + 0.02918 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_R_Bust2": 9.193998852471335, + "Breast_L": 0.3764394436581815, + "J_Sec_L_Bust2": 0.0034999641116944535 + }, + { + "Breast_R": 9.193998852471335, + "Breast_L": 0.003499964148881679 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1", + "J_Sec_Hair2" + ], + "tips": [ + [ + 0.0, + 0.05267, + 0.0 + ], + [ + -0.0119, + 0.04793, + 0.0049 + ] + ], + "hulls": [ + [ + [ + 0.02211, + 0.05654, + -0.00737 + ], + [ + -0.0113, + 0.00462, + 0.00886 + ], + [ + 0.02664, + 0.01135, + -0.00762 + ], + [ + -0.01012, + 0.04418, + 0.0082 + ], + [ + 0.00417, + 0.02934, + -0.01676 + ], + [ + 0.00506, + 0.00068, + -0.01228 + ], + [ + 0.02388, + 0.03378, + -0.00218 + ], + [ + 0.00751, + 0.01734, + 0.00221 + ], + [ + 0.00156, + 0.05109, + -0.01153 + ], + [ + -0.01645, + 0.0246, + 0.00309 + ] + ], + [ + [ + -0.01377, + 0.04599, + 0.02311 + ], + [ + 0.01517, + 0.00071, + -0.01566 + ], + [ + -0.01419, + 0.01615, + -0.00058 + ], + [ + -0.00018, + 0.04995, + -0.00016 + ], + [ + 0.00545, + 0.01883, + -0.0109 + ], + [ + -0.00285, + 0.03046, + 0.00971 + ], + [ + -0.00527, + -0.0005, + -0.00852 + ], + [ + -0.00093, + 0.00626, + 0.01055 + ], + [ + -0.02353, + 0.02583, + 0.0126 + ], + [ + -0.00785, + 0.02851, + -0.00513 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2": 22.972438677485403 + }, + { + "J_Sec_Hair1": 22.972438677485403 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_02", + "J_Sec_Hair2_02" + ], + "tips": [ + [ + 0.0, + 0.04592, + -0.0 + ], + [ + -0.01942, + 0.03749, + -0.02487 + ] + ], + "hulls": [ + [ + [ + 0.01072, + 0.04455, + -0.0095 + ], + [ + -0.00524, + 0.00342, + 0.00917 + ], + [ + -0.00971, + 0.01753, + -0.01712 + ], + [ + 0.0014, + 0.02666, + 0.00964 + ], + [ + 0.0114, + 0.01144, + -0.00735 + ], + [ + -0.00834, + 0.03811, + -0.01428 + ], + [ + -0.00347, + 0.00442, + -0.00748 + ], + [ + -0.00105, + 0.04163, + 0.00561 + ], + [ + -0.00107, + 0.02497, + -0.00467 + ], + [ + 0.00312, + 0.01215, + 0.01332 + ] + ], + [ + [ + -0.02844, + 0.02281, + -0.02358 + ], + [ + 0.00321, + 0.00215, + 0.00308 + ], + [ + -4e-05, + 0.04243, + -5e-05 + ], + [ + -0.00393, + 0.02414, + -0.01924 + ], + [ + -0.01625, + 0.00279, + -0.01771 + ], + [ + 0.00542, + 0.00579, + -0.01485 + ], + [ + -0.00338, + 0.01583, + -0.00303 + ], + [ + -0.02929, + 0.00954, + -0.02895 + ], + [ + -0.01711, + 0.02644, + -0.01476 + ], + [ + -0.00247, + 0.03094, + -0.00726 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_02": 17.0205659394362 + }, + { + "J_Sec_Hair1_02": 17.0205659394362 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_03", + "J_Sec_Hair2_03" + ], + "tips": [ + [ + 0.0, + 0.04129, + 0.0 + ], + [ + -0.01587, + 0.04124, + -0.00134 + ] + ], + "hulls": [ + [ + [ + -0.0169, + 0.04361, + -0.00615 + ], + [ + 0.00714, + -6e-05, + -0.03654 + ], + [ + -0.00202, + 0.00152, + 0.00945 + ], + [ + 0.0051, + 0.01985, + -0.01492 + ], + [ + -0.0019, + 0.03072, + 0.00772 + ], + [ + 0.00809, + 0.00197, + -0.00615 + ], + [ + 0.002, + 0.03754, + -0.00902 + ], + [ + -0.00948, + 0.02959, + -0.02025 + ], + [ + -0.01124, + 0.01631, + 0.01027 + ], + [ + -0.00229, + 0.00827, + -0.02559 + ] + ], + [ + [ + -0.00709, + 0.04307, + 0.00464 + ], + [ + -0.01588, + -0.00104, + -0.00701 + ], + [ + 0.00791, + 0.01708, + -0.00818 + ], + [ + -0.01705, + 0.02161, + -0.00507 + ], + [ + 0.00049, + 0.00372, + 0.00191 + ], + [ + -0.00251, + 0.02523, + 0.00203 + ], + [ + -0.02366, + 0.01048, + -0.00113 + ], + [ + -0.00972, + 0.01506, + 0.00574 + ], + [ + -0.01424, + 0.02991, + 0.00429 + ], + [ + -0.01198, + -0.00122, + 0.00261 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_03": 16.98434789938547 + }, + { + "J_Sec_Hair1_03": 16.98434789938547 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_04", + "J_Sec_Hair2_04" + ], + "tips": [ + [ + -0.0, + 0.04697, + -0.0 + ], + [ + -0.0006, + 0.05488, + -0.00393 + ] + ], + "hulls": [ + [ + [ + -0.01127, + 0.035, + -0.02567 + ], + [ + 0.00453, + 0.00637, + 0.01072 + ], + [ + 4e-05, + 0.00157, + -0.02429 + ], + [ + 0.00334, + 0.04059, + 0.00844 + ], + [ + 0.00196, + 0.02267, + -0.00801 + ], + [ + -0.01323, + 0.01409, + -0.02853 + ], + [ + -0.00341, + 0.04004, + -0.00781 + ], + [ + -0.00476, + 0.00727, + -0.01015 + ], + [ + -0.00524, + 0.02935, + 0.00267 + ], + [ + 0.00061, + 0.02384, + -0.02182 + ] + ], + [ + [ + -0.00074, + 0.04395, + 0.00224 + ], + [ + 0.00853, + 0.00043, + 0.00247 + ], + [ + -0.02077, + 0.01285, + -0.01013 + ], + [ + 0.01243, + 0.02414, + 0.00407 + ], + [ + -0.00355, + 0.0276, + -0.01104 + ], + [ + -0.00574, + 0.00296, + -0.00559 + ], + [ + 0.00235, + 0.01536, + -0.00274 + ], + [ + 0.00479, + 0.03287, + 0.00121 + ], + [ + 0.01225, + 0.0115, + 0.00246 + ], + [ + -0.00882, + 0.01747, + -0.00418 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_04": 4.785209337865712 + }, + { + "J_Sec_Hair1_04": 4.785209337865712 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_05", + "J_Sec_Hair2_05" + ], + "tips": [ + [ + 0.0, + 0.04582, + -0.0 + ], + [ + 0.00297, + 0.05445, + -0.00327 + ] + ], + "hulls": [ + [ + [ + 0.00104, + 0.04116, + -0.00917 + ], + [ + -0.00403, + 0.00429, + 0.01074 + ], + [ + 0.01282, + 0.00945, + -0.02911 + ], + [ + -0.00177, + 0.0185, + -0.00832 + ], + [ + 0.01131, + 0.03056, + -0.0264 + ], + [ + -0.00263, + 0.02662, + 0.00963 + ], + [ + 0.0045, + 0.00468, + -0.01046 + ], + [ + -0.0009, + 0.00848, + -0.02331 + ], + [ + 0.01099, + 0.01879, + -0.01386 + ], + [ + 0.00605, + 0.01529, + 0.00243 + ] + ], + [ + [ + 0.00185, + 0.04372, + 0.00167 + ], + [ + -0.00798, + -0.00034, + 0.00237 + ], + [ + 0.02331, + 0.01192, + -0.00883 + ], + [ + -0.01103, + 0.02296, + 0.00238 + ], + [ + 0.00608, + 0.0257, + -0.00906 + ], + [ + 0.00428, + 0.00577, + -0.00371 + ], + [ + -0.00747, + 0.01288, + -0.00384 + ], + [ + 0.01147, + 0.01556, + -0.00318 + ], + [ + -0.00434, + 0.03276, + 0.00103 + ], + [ + 0.00684, + 0.03604, + -0.00429 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_05": 4.838946945472742 + }, + { + "J_Sec_Hair1_05": 4.838946945472742 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_06", + "J_Sec_Hair2_06" + ], + "tips": [ + [ + -0.0, + 0.04688, + -0.0 + ], + [ + 0.00265, + 0.06108, + 0.00993 + ] + ], + "hulls": [ + [ + [ + -0.00523, + 0.04672, + 0.02244 + ], + [ + 0.01617, + 0.00892, + -0.01237 + ], + [ + -0.00503, + 0.00015, + 0.02236 + ], + [ + 0.0147, + 0.04185, + -0.01069 + ], + [ + -0.00602, + 0.01787, + -0.00251 + ], + [ + -0.0074, + 0.02326, + 0.02337 + ], + [ + -0.00488, + 0.04044, + -0.00212 + ], + [ + 0.00346, + 0.01414, + 0.0139 + ], + [ + 0.00522, + 0.02847, + -0.01111 + ], + [ + 0.00527, + 0.00569, + -0.00172 + ] + ], + [ + [ + 0.01408, + 0.05363, + -0.00026 + ], + [ + -0.01232, + 0.00736, + 0.01936 + ], + [ + 0.01496, + 0.01141, + -0.00463 + ], + [ + -0.00556, + 0.03939, + 0.01853 + ], + [ + -0.00482, + 0.02561, + -0.00086 + ], + [ + -0.00358, + 0.00366, + -0.00284 + ], + [ + 0.01186, + 0.03202, + -0.00338 + ], + [ + -0.0002, + 0.04649, + 0.00018 + ], + [ + -0.00834, + 0.01404, + 0.00644 + ], + [ + 0.00555, + 0.01005, + 0.00408 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_06": 8.396507483964648 + }, + { + "J_Sec_Hair1_06": 8.396507483964648 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_07", + "J_Sec_Hair2_07" + ], + "tips": [ + [ + -0.0, + 0.04662, + 0.0 + ], + [ + -0.00378, + 0.06052, + 0.00974 + ] + ], + "hulls": [ + [ + [ + 0.00668, + 0.04005, + 0.0237 + ], + [ + -0.01595, + 0.00928, + -0.01259 + ], + [ + 0.00647, + 0.00349, + 0.0247 + ], + [ + -0.01419, + 0.04194, + -0.00218 + ], + [ + 0.00596, + 0.01773, + -0.00252 + ], + [ + 0.00483, + 0.04013, + -0.00215 + ], + [ + -0.00246, + 0.01728, + 0.0159 + ], + [ + -0.00537, + 0.02827, + -0.01102 + ], + [ + -0.0066, + 0.00744, + 0.00285 + ], + [ + 0.00794, + 0.02899, + 0.01163 + ] + ], + [ + [ + -0.01459, + 0.05372, + -0.00063 + ], + [ + 0.01183, + 0.00123, + 0.01953 + ], + [ + -0.01493, + 0.01132, + -0.00484 + ], + [ + 0.00635, + 0.03459, + 0.01645 + ], + [ + -0.0109, + 0.03244, + 0.00179 + ], + [ + 0.0035, + 0.00359, + -0.0029 + ], + [ + 0.00415, + 0.02554, + -0.00153 + ], + [ + 0.00021, + 0.04647, + 0.00017 + ], + [ + 0.00387, + 0.01318, + 0.00851 + ], + [ + -0.00699, + 0.02219, + -0.00216 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_07": 8.387276555993644 + }, + { + "J_Sec_Hair1_07": 8.387276555993644 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_08", + "J_Sec_Hair2_08", + "J_Sec_Hair3", + "J_Sec_Hair4" + ], + "tips": [ + [ + -0.0, + 0.10496, + -0.0 + ], + [ + -0.0, + 0.10547, + -0.0 + ], + [ + -0.0, + 0.10554, + 0.0 + ], + [ + 0.01644, + 0.09775, + 0.0656 + ] + ], + "hulls": [ + [ + [ + -0.07153, + 0.10743, + 0.01136 + ], + [ + 0.06403, + 0.00327, + 0.00358 + ], + [ + -0.05611, + -0.00021, + 0.00811 + ], + [ + 0.03184, + 0.10221, + -0.00142 + ], + [ + 0.00697, + 0.04137, + 0.03228 + ], + [ + -0.0442, + 0.0594, + -0.01228 + ], + [ + -0.02144, + 0.09165, + 0.02908 + ], + [ + 0.0631, + 0.05723, + -0.00107 + ], + [ + 0.00291, + 0.00036, + 0.00243 + ], + [ + 0.03631, + 0.08221, + 0.03787 + ] + ], + [ + [ + -0.09161, + 0.10225, + -0.00107 + ], + [ + 0.06914, + 0.0124, + 0.01082 + ], + [ + -0.04301, + 0.00087, + 0.02207 + ], + [ + 0.0331, + 0.09992, + 0.06227 + ], + [ + 0.00729, + 0.05557, + 0.00233 + ], + [ + 0.06579, + 0.10144, + -0.00492 + ], + [ + -0.04659, + 0.07612, + 0.04976 + ], + [ + -0.07923, + 0.04122, + -0.00863 + ], + [ + -0.03052, + 0.10194, + -0.00235 + ], + [ + 0.01152, + 0.01006, + 0.03891 + ] + ], + [ + [ + 0.07388, + 0.12158, + 0.057 + ], + [ + -0.09419, + 0.0045, + -0.00035 + ], + [ + 0.06616, + 0.00695, + -0.00356 + ], + [ + -0.05004, + 0.1064, + 0.05473 + ], + [ + -0.01029, + 0.00661, + 0.06143 + ], + [ + 0.02325, + 0.07918, + 0.00043 + ], + [ + -0.12052, + 0.07684, + 0.02644 + ], + [ + 0.05954, + 0.0501, + 0.05852 + ], + [ + -0.04165, + 0.05332, + 0.00092 + ], + [ + 0.00708, + 0.08094, + 0.06213 + ] + ], + [ + [ + -0.10796, + 0.05015, + 0.05267 + ], + [ + 0.09532, + 0.08732, + -0.00437 + ], + [ + 0.01438, + -0.01201, + 0.06067 + ], + [ + -0.01579, + 0.09651, + 0.00338 + ], + [ + 0.08815, + -0.00924, + 0.00912 + ], + [ + -0.04214, + 0.0078, + 0.011 + ], + [ + 0.02619, + 0.06354, + 0.06116 + ], + [ + -0.04094, + 0.06844, + 0.06988 + ], + [ + 0.03418, + 0.0353, + 0.00184 + ], + [ + -0.05753, + 0.00659, + 0.07191 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_08": 84.55824972109777 + }, + { + "J_Sec_Hair1_08": 84.55824972109777, + "J_Sec_Hair3": 86.75028206731109 + }, + { + "J_Sec_Hair4": 87.00580237252505, + "J_Sec_Hair2_08": 86.75028206731109 + }, + { + "J_Sec_Hair3": 87.00580237252505 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_09", + "J_Sec_Hair2_09", + "J_Sec_Hair3_09", + "J_Sec_Hair4_09" + ], + "tips": [ + [ + 0.0, + 0.10543, + 0.0 + ], + [ + 0.0, + 0.10472, + 0.0 + ], + [ + -0.0, + 0.10523, + -0.0 + ], + [ + -0.06402, + 0.13909, + 0.03729 + ] + ], + "hulls": [ + [ + [ + -0.06656, + 0.11026, + 0.04817 + ], + [ + 0.01219, + 0.00692, + -0.04538 + ], + [ + -0.03807, + 0.00521, + 0.05267 + ], + [ + -0.02272, + 0.1011, + -0.04851 + ], + [ + -0.00398, + 0.07477, + 0.0205 + ], + [ + -0.04299, + 0.03936, + -0.03427 + ], + [ + -0.05933, + 0.05738, + 0.02592 + ], + [ + -0.00549, + 0.01549, + 0.01019 + ], + [ + 0.01867, + 0.06818, + -0.04032 + ], + [ + -0.03893, + 0.10635, + 0.00977 + ] + ], + [ + [ + -0.08781, + 0.10466, + 0.03983 + ], + [ + 0.0289, + 0.00755, + -0.033 + ], + [ + -0.05281, + -0.00369, + 0.04056 + ], + [ + -0.02026, + 0.10148, + -0.03905 + ], + [ + -0.01598, + 0.05741, + 0.02002 + ], + [ + -0.03865, + 0.02918, + -0.04654 + ], + [ + 0.03076, + 0.09552, + -0.00202 + ], + [ + -0.07421, + 0.06032, + -0.0021 + ], + [ + -0.0165, + 0.00225, + 0.0018 + ], + [ + 0.00572, + 0.06012, + -0.03105 + ] + ], + [ + [ + -0.10645, + 0.09342, + 0.03535 + ], + [ + 0.03714, + 0.00481, + -0.02017 + ], + [ + -0.07792, + -0.0035, + -0.01176 + ], + [ + 0.00466, + 0.10431, + 0.00641 + ], + [ + -0.06083, + 0.07431, + -0.0248 + ], + [ + -0.0224, + 0.03201, + 0.02291 + ], + [ + -0.00924, + 0.0432, + -0.03742 + ], + [ + -0.09792, + 0.03242, + 0.03269 + ], + [ + -0.04999, + 0.08067, + 0.03692 + ], + [ + 0.04142, + 0.06406, + -0.01977 + ] + ], + [ + [ + -0.09347, + 0.12211, + 0.07242 + ], + [ + 0.03302, + 0.00987, + -0.03583 + ], + [ + -0.0617, + 0.00343, + 0.06316 + ], + [ + -0.00214, + 0.11517, + -0.01309 + ], + [ + -0.03951, + 0.03636, + -0.00902 + ], + [ + -0.03006, + 0.07742, + 0.05733 + ], + [ + -0.09269, + 0.07215, + 0.02499 + ], + [ + -0.08565, + 0.13211, + 0.00698 + ], + [ + 0.02018, + 0.05715, + 0.00097 + ], + [ + -0.00875, + 0.0122, + 0.03632 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_09": 78.90760830260274 + }, + { + "J_Sec_Hair1_09": 78.90760830260274, + "J_Sec_Hair3_09": 79.41734449788348 + }, + { + "J_Sec_Hair4_09": 75.41101903626877, + "J_Sec_Hair2_09": 79.41734449788348 + }, + { + "J_Sec_Hair3_09": 75.41101903626877 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_10", + "J_Sec_Hair2_10", + "J_Sec_Hair3_10", + "J_Sec_Hair4_10" + ], + "tips": [ + [ + -0.0, + 0.11079, + -0.0 + ], + [ + -0.0, + 0.11018, + -0.0 + ], + [ + 0.0, + 0.11069, + -0.0 + ], + [ + 0.05325, + 0.14432, + 0.02356 + ] + ], + "hulls": [ + [ + [ + 0.04231, + 0.11172, + -0.04241 + ], + [ + 0.01589, + 0.002, + 0.04537 + ], + [ + -0.00495, + 0.02182, + -0.04958 + ], + [ + 0.04112, + 0.09499, + 0.04301 + ], + [ + -0.02265, + 0.08699, + -0.02233 + ], + [ + 0.03503, + 0.05655, + -0.01278 + ], + [ + -0.0109, + 0.01586, + 0.00172 + ], + [ + 0.05173, + 0.03639, + 0.03113 + ], + [ + 0.00212, + 0.06956, + 0.0219 + ], + [ + 0.02689, + 0.10063, + -0.0017 + ] + ], + [ + [ + 0.0717, + 0.11257, + -0.00053 + ], + [ + -0.02795, + 0.01097, + -0.04021 + ], + [ + 0.04589, + -0.00093, + 0.03575 + ], + [ + -0.03562, + 0.09933, + -0.00866 + ], + [ + 0.04042, + 0.0573, + -0.04529 + ], + [ + 0.02622, + 0.0704, + 0.02681 + ], + [ + 0.01827, + 0.10505, + -0.02344 + ], + [ + 0.06876, + 0.03913, + 0.00376 + ], + [ + 0.03488, + 0.00573, + -0.02818 + ], + [ + -0.0038, + 0.02825, + 0.00528 + ] + ], + [ + [ + 0.09693, + 0.09353, + -0.02119 + ], + [ + -0.04268, + 0.00881, + -0.02346 + ], + [ + 0.06127, + -0.00235, + 0.02821 + ], + [ + -0.01431, + 0.11112, + 0.00088 + ], + [ + 0.03338, + 0.04036, + -0.0398 + ], + [ + 0.04853, + 0.07291, + 0.03372 + ], + [ + -0.00305, + 0.04895, + 0.00868 + ], + [ + 0.03431, + 0.09654, + -0.02439 + ], + [ + 0.08121, + 0.03538, + -0.01683 + ], + [ + -0.04096, + 0.06884, + -0.01959 + ] + ], + [ + [ + 0.08884, + 0.13524, + 0.0072 + ], + [ + -0.03138, + 0.00574, + -0.03145 + ], + [ + 0.05444, + 0.023, + 0.0668 + ], + [ + -0.00629, + 0.10328, + -0.01684 + ], + [ + 0.04799, + 0.01276, + -0.01855 + ], + [ + 0.04867, + 0.10273, + 0.06538 + ], + [ + 0.09698, + 0.06504, + 0.02541 + ], + [ + 0.00261, + 0.05719, + 0.03199 + ], + [ + 0.05271, + 0.08676, + 0.00187 + ], + [ + 0.01432, + 0.00543, + 0.03469 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_10": 62.44316311540696 + }, + { + "J_Sec_Hair1_10": 62.44316311540696, + "J_Sec_Hair3_10": 63.04460060622763 + }, + { + "J_Sec_Hair2_10": 63.04460060622763, + "J_Sec_Hair4_10": 60.580090488886825 + }, + { + "J_Sec_Hair3_10": 60.580090488886825 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_11", + "J_Sec_Hair2_11", + "J_Sec_Hair3_11" + ], + "tips": [ + [ + 0.0, + 0.09743, + 0.0 + ], + [ + -0.0, + 0.09762, + 0.0 + ], + [ + 0.00462, + 0.10146, + -0.01285 + ] + ], + "hulls": [ + [ + [ + -0.01221, + 0.09678, + 0.00949 + ], + [ + 0.00744, + -0.0005, + -0.00825 + ], + [ + 0.01236, + 0.0531, + -0.01826 + ], + [ + -0.01009, + 0.03598, + 0.01136 + ], + [ + 0.01125, + 0.09408, + -0.02381 + ], + [ + -0.01357, + 0.07188, + -0.02131 + ], + [ + -0.0047, + 0.0666, + 0.01137 + ], + [ + -0.01289, + 0.02218, + -0.01468 + ], + [ + -0.00933, + 0.00608, + 0.01185 + ], + [ + -0.01118, + 0.04888, + -0.01063 + ] + ], + [ + [ + 0.00784, + 0.09956, + 0.01381 + ], + [ + -0.01848, + 0.00315, + -0.01861 + ], + [ + -0.0151, + 0.06007, + -0.02411 + ], + [ + 0.00118, + 0.03907, + 0.0139 + ], + [ + 0.02421, + 0.07537, + -0.01587 + ], + [ + 0.01089, + 0.02775, + -0.02591 + ], + [ + -0.01747, + 0.08986, + -0.0102 + ], + [ + 0.00209, + 0.00872, + 0.00641 + ], + [ + 0.00358, + 0.06946, + 0.00799 + ], + [ + -0.02051, + 0.03341, + -0.01797 + ] + ], + [ + [ + -0.01959, + 0.09326, + -0.01552 + ], + [ + 0.02314, + 0.00277, + -0.01465 + ], + [ + 0.01473, + 0.05659, + 0.01522 + ], + [ + -0.02419, + 0.00698, + -0.01011 + ], + [ + 0.02832, + 0.04425, + -0.02271 + ], + [ + -0.0122, + 0.04538, + -0.00939 + ], + [ + 0.01049, + 0.01673, + 0.01335 + ], + [ + 0.00356, + 0.08581, + 0.00547 + ], + [ + 0.01037, + 0.06843, + -0.01763 + ], + [ + 0.00866, + 0.02404, + -0.02235 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_11": 38.39318372494317 + }, + { + "J_Sec_Hair1_11": 38.39318372494317, + "J_Sec_Hair3_11": 38.646242415686416 + }, + { + "J_Sec_Hair2_11": 38.646242415686416 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_12", + "J_Sec_Hair2_12", + "J_Sec_Hair3_12" + ], + "tips": [ + [ + -0.0, + 0.09903, + 0.0 + ], + [ + -0.0, + 0.09923, + 0.0 + ], + [ + -0.00432, + 0.10073, + -0.01342 + ] + ], + "hulls": [ + [ + [ + -0.01146, + 0.0976, + -0.02407 + ], + [ + 0.00785, + 0.00682, + 0.01347 + ], + [ + 0.01436, + 0.05683, + -0.00543 + ], + [ + 0.01163, + 0.09772, + 0.01073 + ], + [ + -0.01269, + 0.02645, + -0.0143 + ], + [ + -0.01314, + 0.06793, + -0.01422 + ], + [ + 0.00134, + 0.03735, + 0.01269 + ], + [ + 0.01321, + 0.08248, + -0.01929 + ], + [ + 0.01237, + 0.01593, + -0.01419 + ], + [ + 0.00027, + 0.07762, + 0.00362 + ] + ], + [ + [ + 0.00535, + 0.09915, + 0.02057 + ], + [ + -0.01048, + 0.00348, + -0.02003 + ], + [ + 0.02038, + 0.05674, + -0.01906 + ], + [ + -0.02382, + 0.08467, + -0.01369 + ], + [ + 0.00891, + 0.02845, + 0.01502 + ], + [ + -0.01737, + 0.04394, + -0.01686 + ], + [ + 0.01498, + 0.09232, + -0.02066 + ], + [ + -0.00218, + 0.06717, + 0.00628 + ], + [ + 0.01722, + 0.02052, + -0.02388 + ], + [ + 0.01373, + 0.00671, + 0.00156 + ] + ], + [ + [ + 0.01692, + 0.09514, + -0.01021 + ], + [ + -0.02117, + -0.00181, + -0.01261 + ], + [ + -0.01463, + 0.04746, + 0.01881 + ], + [ + 0.02529, + 0.00831, + -0.0068 + ], + [ + -0.01671, + 0.06707, + -0.02926 + ], + [ + -0.00603, + 0.00833, + 0.02114 + ], + [ + 0.01362, + 0.0465, + -0.00653 + ], + [ + -0.01052, + 0.07779, + 0.00235 + ], + [ + -0.01405, + 0.03121, + -0.01671 + ], + [ + -0.00321, + 0.0294, + 0.00566 + ] + ] + ], + "neighbours": [ + { + "J_Sec_Hair2_12": 39.49819143834376 + }, + { + "J_Sec_Hair1_12": 39.49819143834376, + "J_Sec_Hair3_12": 39.1444003614134 + }, + { + "J_Sec_Hair2_12": 39.1444003614134 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "J_Sec_Hair1_13" + ], + "tips": [ + [ + -0.01148, + 0.09019, + 0.00471 + ] + ], + "hulls": [ + [ + [ + -0.04045, + 0.06763, + 0.00296 + ], + [ + 0.04104, + 0.03863, + -0.00205 + ], + [ + -0.01976, + 0.00024, + 0.0052 + ], + [ + 0.00695, + 0.07097, + -0.00076 + ], + [ + -0.05262, + 0.02583, + 0.00729 + ], + [ + 0.03523, + 0.06718, + -0.00387 + ], + [ + 0.01979, + 0.02217, + 0.00226 + ], + [ + -0.01592, + 0.05624, + 0.00297 + ], + [ + -0.04328, + 0.04153, + 0.0058 + ], + [ + -0.03547, + 0.01986, + 0.00731 + ] + ] + ], + "neighbours": [ + {} + ] + } ], - "shoulder.R": "Right shoulder", - "shoulder.L": "Left shoulder", - "foot.R": "Right ankle", - "foot.L": "Left ankle", - "hand.L": "Left wrist", - "hand.R": "Right wrist", - "forearm.R": "Right elbow", - "forearm.L": "Left elbow", - "shin.L": "Left knee", - "shin.R": "Right knee", - "toe.R": "Right toe", - "toe.L": "Left toe", - "upper_arm.L": "Left arm", - "upper_arm.R": "Right arm", - "thigh.L": "Left leg", - "thigh.R": "Right leg" - }, - "fingers": { - "thumb.L": [ - "Thumb0_L", - "Thumb1_L", - "Thumb2_L" + "twist": [], + "surfaces": [ + { + "mesh": "Object_103", + "surface": 0, + "material": "N00_000_Hair_00_HAIR_Instance", + "class": "hair", + "detail": "hair", + "why": "material name says 'hair'", + "verts": 19771, + "textured": true, + "chain_share": { + "hair": 0.619 + } + }, + { + "mesh": "Object_104", + "surface": 0, + "material": "N00_000_00_FaceMouth_00_FACE_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 1053, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_105", + "surface": 0, + "material": "N00_000_00_EyeIris_00_EYE_Instance", + "class": "body", + "detail": "eyes", + "why": "material name says 'eyes'", + "verts": 212, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_106", + "surface": 0, + "material": "N00_000_00_EyeHighlight_00_EYE_Instance", + "class": "body", + "detail": "eyes", + "why": "material name says 'eyes'", + "verts": 26, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_107", + "surface": 0, + "material": "N00_000_00_Face_00_SKIN_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 2266, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_108", + "surface": 0, + "material": "N00_000_00_EyeWhite_00_EYE_Instance", + "class": "body", + "detail": "eyes", + "why": "material name says 'eyes'", + "verts": 290, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_109", + "surface": 0, + "material": "N00_000_00_FaceBrow_00_FACE_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 50, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_110", + "surface": 0, + "material": "N00_000_00_FaceEyelash_00_FACE_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 18, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_111", + "surface": 0, + "material": "N00_000_00_FaceEyeline_00_FACE_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 118, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_112", + "surface": 0, + "material": "N00_000_00_Body_00_SKIN_Instance", + "class": "body", + "detail": "skin", + "why": "material name says 'skin'", + "verts": 5870, + "textured": true, + "chain_share": { + "breast": 0.038 + } + }, + { + "mesh": "Object_113", + "surface": 0, + "material": "N00_001_03_Bottoms_01_CLOTH_Instance", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 742, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_114", + "surface": 0, + "material": "N00_004_01_Shoes_01_CLOTH_Instance", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 600, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_115", + "surface": 0, + "material": "N00_000_00_HairBack_00_HAIR_Instance", + "class": "hair", + "detail": "hair", + "why": "material name says 'hair'", + "verts": 760, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_116", + "surface": 0, + "material": "N00_002_02_Tops_01_CLOTH_Instance", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 512, + "textured": true, + "chain_share": { + "breast": 0.117 + } + }, + { + "mesh": "Object_117", + "surface": 0, + "material": "N00_010_01_Onepiece_00_CLOTH_Instance", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 52, + "textured": true, + "chain_share": { + "breast": 0.115 + } + } ], - "index.L": [ - "IndexFinger1_L", - "IndexFinger2_L", - "IndexFinger3_L" + "colliders": [ + { + "bone": "Hips", + "child": "Spine", + "from": 0.0, + "radius_head": 0.1498, + "radius_tail": 0.1498, + "radius": 0.1498, + "lid": true + }, + { + "bone": "Left leg", + "child": "Left knee", + "from": 0.1, + "radius_head": 0.102, + "radius_tail": 0.057, + "radius": 0.057 + }, + { + "bone": "Right leg", + "child": "Right knee", + "from": 0.1, + "radius_head": 0.102, + "radius_tail": 0.057, + "radius": 0.057 + }, + { + "bone": "Left knee", + "child": "Left ankle", + "from": 0.1, + "radius_head": 0.0883, + "radius_tail": 0.0474, + "radius": 0.0474 + }, + { + "bone": "Right knee", + "child": "Right ankle", + "from": 0.1, + "radius_head": 0.0883, + "radius_tail": 0.0474, + "radius": 0.0474 + } ], - "middle.L": [ - "MiddleFinger1_L", - "MiddleFinger2_L", - "MiddleFinger3_L" - ], - "ring.L": [ - "RingFinger1_L", - "RingFinger2_L", - "RingFinger3_L" - ], - "pinky.L": [ - "LittleFinger1_L", - "LittleFinger2_L", - "LittleFinger3_L" - ], - "thumb.R": [ - "Thumb0_R", - "Thumb1_R", - "Thumb2_R" - ], - "index.R": [ - "IndexFinger1_R", - "IndexFinger2_R", - "IndexFinger3_R" - ], - "middle.R": [ - "MiddleFinger1_R", - "MiddleFinger2_R", - "MiddleFinger3_R" - ], - "ring.R": [ - "RingFinger1_R", - "RingFinger2_R", - "RingFinger3_R" - ], - "pinky.R": [ - "LittleFinger1_R", - "LittleFinger2_R", - "LittleFinger3_R" + "weights_authored": true, + "driven_bones": [ + "Chest", + "Head", + "Hips", + "Left ankle", + "Left arm", + "Left elbow", + "Left knee", + "Left leg", + "Left shoulder", + "Left toe", + "Left wrist", + "Neck", + "Right ankle", + "Right arm", + "Right elbow", + "Right knee", + "Right leg", + "Right shoulder", + "Right toe", + "Right wrist", + "Spine" ] - }, - "chains": [ - { - "class": "breast", - "root_parent": "Chest", - "bones": [ - "Breast_L", - "J_Sec_L_Bust2" - ], - "tips": [ - [ - 0.0, - 0.09099, - -0.0 - ], - [ - 0.0, - 0.09099, - -0.0 - ] - ], - "hulls": [ - [ - [ - 0.00917, - 0.0933, - -0.00744 - ], - [ - -0.07418, - 0.03013, - 0.00578 - ], - [ - 0.00133, - 0.03457, - 0.05712 - ], - [ - -0.01478, - 0.03942, - -0.06075 - ], - [ - 0.04781, - 0.06869, - 0.03379 - ], - [ - 0.04463, - 0.0526, - -0.02954 - ], - [ - -0.03647, - 0.06666, - 0.01678 - ], - [ - -0.0479, - 0.0376, - 0.05365 - ], - [ - -0.04619, - 0.06094, - -0.03406 - ], - [ - 0.0027, - 0.07461, - 0.02918 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_L_Bust2": 9.193998822705257, - "Breast_R": 0.3764394436581815, - "J_Sec_R_Bust2": 0.003499964148881679 - }, - { - "Breast_L": 9.193998822705257, - "Breast_R": 0.0034999641116944535 - } - ] - }, - { - "class": "breast", - "root_parent": "Chest", - "bones": [ - "Breast_R", - "J_Sec_R_Bust2" - ], - "tips": [ - [ - 0.0, - 0.09099, - 0.0 - ], - [ - 0.0, - 0.09099, - 0.0 - ] - ], - "hulls": [ - [ - [ - -0.00917, - 0.0933, - -0.00744 - ], - [ - 0.07418, - 0.03013, - 0.00578 - ], - [ - -0.00133, - 0.03457, - 0.05712 - ], - [ - 0.01478, - 0.03942, - -0.06075 - ], - [ - -0.04781, - 0.06869, - 0.03379 - ], - [ - -0.04463, - 0.0526, - -0.02954 - ], - [ - 0.03647, - 0.06666, - 0.01678 - ], - [ - 0.0479, - 0.0376, - 0.05365 - ], - [ - 0.04619, - 0.06094, - -0.03406 - ], - [ - -0.0027, - 0.07461, - 0.02918 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_R_Bust2": 9.193998837122376, - "Breast_L": 0.3764394436581815, - "J_Sec_L_Bust2": 0.0034999641116944535 - }, - { - "Breast_R": 9.193998837122376, - "Breast_L": 0.003499964148881679 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_01", - "J_Sec_Hair2_01" - ], - "tips": [ - [ - 0.0, - 0.05267, - -0.0 - ], - [ - -0.01175, - 0.0474, - 0.0045 - ] - ], - "hulls": [ - [ - [ - 0.02211, - 0.05654, - -0.00737 - ], - [ - -0.0113, - 0.00462, - 0.00886 - ], - [ - 0.02664, - 0.01135, - -0.00762 - ], - [ - -0.01012, - 0.04418, - 0.0082 - ], - [ - 0.00417, - 0.02934, - -0.01676 - ], - [ - 0.00506, - 0.00069, - -0.01228 - ], - [ - 0.02388, - 0.03378, - -0.00218 - ], - [ - 0.00751, - 0.01734, - 0.00221 - ], - [ - 0.00156, - 0.05109, - -0.01154 - ], - [ - -0.01645, - 0.0246, - 0.00309 - ] - ], - [ - [ - -0.01377, - 0.04599, - 0.02311 - ], - [ - 0.01517, - 0.00071, - -0.01566 - ], - [ - -0.01419, - 0.01615, - -0.00058 - ], - [ - -0.00018, - 0.04996, - -0.00016 - ], - [ - 0.00545, - 0.01883, - -0.0109 - ], - [ - -0.00285, - 0.03046, - 0.00971 - ], - [ - -0.00527, - -0.0005, - -0.00852 - ], - [ - -0.00093, - 0.00626, - 0.01055 - ], - [ - -0.02353, - 0.02583, - 0.0126 - ], - [ - -0.00785, - 0.02851, - -0.00514 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_01": 22.972438677485403 - }, - { - "J_Sec_Hair1_01": 22.972438677485403 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_02", - "J_Sec_Hair2_02" - ], - "tips": [ - [ - 0.0, - 0.04592, - -0.0 - ], - [ - -0.01915, - 0.03667, - -0.02491 - ] - ], - "hulls": [ - [ - [ - 0.01072, - 0.04455, - -0.0095 - ], - [ - -0.00524, - 0.00342, - 0.00917 - ], - [ - -0.00971, - 0.01753, - -0.01712 - ], - [ - 0.0014, - 0.02666, - 0.00964 - ], - [ - 0.0114, - 0.01144, - -0.00735 - ], - [ - -0.00834, - 0.03811, - -0.01428 - ], - [ - -0.00347, - 0.00442, - -0.00748 - ], - [ - -0.00105, - 0.04163, - 0.00561 - ], - [ - -0.00107, - 0.02497, - -0.00467 - ], - [ - 0.00312, - 0.01215, - 0.01332 - ] - ], - [ - [ - -0.02844, - 0.02281, - -0.02358 - ], - [ - 0.00321, - 0.00215, - 0.00308 - ], - [ - -4e-05, - 0.04244, - -5e-05 - ], - [ - -0.00393, - 0.02414, - -0.01924 - ], - [ - -0.01625, - 0.00279, - -0.01771 - ], - [ - 0.00542, - 0.00579, - -0.01485 - ], - [ - -0.00338, - 0.01583, - -0.00303 - ], - [ - -0.02929, - 0.00954, - -0.02894 - ], - [ - -0.01711, - 0.02644, - -0.01476 - ], - [ - -0.00247, - 0.03094, - -0.00726 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_02": 17.0205659394362 - }, - { - "J_Sec_Hair1_02": 17.0205659394362 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_03", - "J_Sec_Hair2_03" - ], - "tips": [ - [ - -0.0, - 0.04129, - -0.0 - ], - [ - -0.01612, - 0.04011, - -0.00151 - ] - ], - "hulls": [ - [ - [ - -0.0169, - 0.04361, - -0.00615 - ], - [ - 0.00714, - -6e-05, - -0.03654 - ], - [ - -0.00202, - 0.00152, - 0.00945 - ], - [ - 0.0051, - 0.01985, - -0.01492 - ], - [ - -0.0019, - 0.03072, - 0.00772 - ], - [ - 0.00809, - 0.00197, - -0.00615 - ], - [ - 0.002, - 0.03754, - -0.00902 - ], - [ - -0.00948, - 0.02959, - -0.02025 - ], - [ - -0.01124, - 0.01631, - 0.01027 - ], - [ - -0.00229, - 0.00827, - -0.02559 - ] - ], - [ - [ - -0.00709, - 0.04307, - 0.00464 - ], - [ - -0.01588, - -0.00104, - -0.00701 - ], - [ - 0.00791, - 0.01708, - -0.00818 - ], - [ - -0.01705, - 0.02161, - -0.00507 - ], - [ - 0.00049, - 0.00372, - 0.00191 - ], - [ - -0.00251, - 0.02523, - 0.00203 - ], - [ - -0.02366, - 0.01048, - -0.00113 - ], - [ - -0.00972, - 0.01506, - 0.00574 - ], - [ - -0.01424, - 0.02991, - 0.00429 - ], - [ - -0.01198, - -0.00122, - 0.00261 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_03": 16.98434789938547 - }, - { - "J_Sec_Hair1_03": 16.98434789938547 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_04", - "J_Sec_Hair2_04" - ], - "tips": [ - [ - 0.0, - 0.04697, - 0.0 - ], - [ - -0.00062, - 0.05408, - -0.00405 - ] - ], - "hulls": [ - [ - [ - -0.01127, - 0.03501, - -0.02567 - ], - [ - 0.00453, - 0.00637, - 0.01072 - ], - [ - 4e-05, - 0.00157, - -0.02429 - ], - [ - 0.00334, - 0.04059, - 0.00844 - ], - [ - 0.00196, - 0.02267, - -0.00801 - ], - [ - -0.01323, - 0.01409, - -0.02853 - ], - [ - -0.00341, - 0.04004, - -0.00781 - ], - [ - -0.00476, - 0.00727, - -0.01015 - ], - [ - -0.00524, - 0.02935, - 0.00267 - ], - [ - 0.00061, - 0.02384, - -0.02182 - ] - ], - [ - [ - -0.00073, - 0.04395, - 0.00224 - ], - [ - 0.00853, - 0.00043, - 0.00247 - ], - [ - -0.02077, - 0.01285, - -0.01013 - ], - [ - 0.01243, - 0.02414, - 0.00407 - ], - [ - -0.00355, - 0.0276, - -0.01104 - ], - [ - -0.00574, - 0.00296, - -0.00559 - ], - [ - 0.00235, - 0.01536, - -0.00274 - ], - [ - 0.00479, - 0.03287, - 0.00121 - ], - [ - 0.01225, - 0.0115, - 0.00246 - ], - [ - -0.00882, - 0.01747, - -0.00418 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_04": 4.785209337865712 - }, - { - "J_Sec_Hair1_04": 4.785209337865712 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_05", - "J_Sec_Hair2_05" - ], - "tips": [ - [ - 0.0, - 0.04582, - 0.0 - ], - [ - 0.00306, - 0.05362, - -0.00337 - ] - ], - "hulls": [ - [ - [ - 0.00104, - 0.04116, - -0.00917 - ], - [ - -0.00403, - 0.0043, - 0.01074 - ], - [ - 0.01282, - 0.00945, - -0.02911 - ], - [ - -0.00177, - 0.0185, - -0.00832 - ], - [ - 0.01131, - 0.03056, - -0.0264 - ], - [ - -0.00263, - 0.02662, - 0.00963 - ], - [ - 0.0045, - 0.00468, - -0.01046 - ], - [ - -0.0009, - 0.00848, - -0.02331 - ], - [ - 0.01099, - 0.01879, - -0.01386 - ], - [ - 0.00605, - 0.01529, - 0.00243 - ] - ], - [ - [ - 0.00185, - 0.04373, - 0.00167 - ], - [ - -0.00798, - -0.00034, - 0.00237 - ], - [ - 0.02331, - 0.01192, - -0.00883 - ], - [ - -0.01103, - 0.02296, - 0.00238 - ], - [ - 0.00608, - 0.0257, - -0.00906 - ], - [ - 0.00428, - 0.00577, - -0.00371 - ], - [ - -0.00747, - 0.01288, - -0.00384 - ], - [ - 0.01147, - 0.01556, - -0.00318 - ], - [ - -0.00434, - 0.03276, - 0.00103 - ], - [ - 0.00684, - 0.03604, - -0.00429 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_05": 4.838946945472742 - }, - { - "J_Sec_Hair1_05": 4.838946945472742 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_06", - "J_Sec_Hair2_06" - ], - "tips": [ - [ - 0.0, - 0.04688, - -0.0 - ], - [ - 0.0024, - 0.05976, - 0.00959 - ] - ], - "hulls": [ - [ - [ - -0.00523, - 0.04672, - 0.02244 - ], - [ - 0.01617, - 0.00892, - -0.01237 - ], - [ - -0.00503, - 0.00015, - 0.02236 - ], - [ - 0.0147, - 0.04185, - -0.01069 - ], - [ - -0.00602, - 0.01787, - -0.00251 - ], - [ - -0.0074, - 0.02326, - 0.02337 - ], - [ - -0.00488, - 0.04044, - -0.00212 - ], - [ - 0.00346, - 0.01414, - 0.0139 - ], - [ - 0.00522, - 0.02847, - -0.01111 - ], - [ - 0.00527, - 0.00569, - -0.00172 - ] - ], - [ - [ - 0.01408, - 0.05363, - -0.00026 - ], - [ - -0.01232, - 0.00736, - 0.01936 - ], - [ - 0.01496, - 0.01141, - -0.00463 - ], - [ - -0.00556, - 0.03939, - 0.01853 - ], - [ - -0.00482, - 0.02561, - -0.00086 - ], - [ - -0.00358, - 0.00366, - -0.00284 - ], - [ - 0.01186, - 0.03202, - -0.00338 - ], - [ - -0.0002, - 0.04649, - 0.00018 - ], - [ - -0.00834, - 0.01404, - 0.00644 - ], - [ - 0.00555, - 0.01005, - 0.00408 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_06": 8.396507483964648 - }, - { - "J_Sec_Hair1_06": 8.396507483964648 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_07", - "J_Sec_Hair2_07" - ], - "tips": [ - [ - -0.0, - 0.04662, - -0.0 - ], - [ - -0.00341, - 0.05907, - 0.00954 - ] - ], - "hulls": [ - [ - [ - 0.00668, - 0.04005, - 0.0237 - ], - [ - -0.01595, - 0.00928, - -0.01259 - ], - [ - 0.00647, - 0.00349, - 0.0247 - ], - [ - -0.01419, - 0.04194, - -0.00218 - ], - [ - 0.00596, - 0.01773, - -0.00252 - ], - [ - 0.00483, - 0.04013, - -0.00215 - ], - [ - -0.00246, - 0.01728, - 0.0159 - ], - [ - -0.00537, - 0.02827, - -0.01102 - ], - [ - -0.0066, - 0.00744, - 0.00285 - ], - [ - 0.00794, - 0.02899, - 0.01163 - ] - ], - [ - [ - -0.01459, - 0.05372, - -0.00063 - ], - [ - 0.01183, - 0.00123, - 0.01952 - ], - [ - -0.01493, - 0.01132, - -0.00484 - ], - [ - 0.00635, - 0.03459, - 0.01645 - ], - [ - -0.0109, - 0.03244, - 0.00179 - ], - [ - 0.0035, - 0.00359, - -0.0029 - ], - [ - 0.00415, - 0.02554, - -0.00153 - ], - [ - 0.00021, - 0.04647, - 0.00017 - ], - [ - 0.00387, - 0.01318, - 0.00851 - ], - [ - -0.00699, - 0.0222, - -0.00216 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_07": 8.387276555993644 - }, - { - "J_Sec_Hair1_07": 8.387276555993644 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_08", - "J_Sec_Hair2_08", - "J_Sec_Hair3_08", - "J_Sec_Hair4_08" - ], - "tips": [ - [ - -0.0, - 0.10496, - 0.0 - ], - [ - -0.0, - 0.10547, - -0.0 - ], - [ - -0.0, - 0.10554, - -0.0 - ], - [ - 0.01605, - 0.09703, - 0.06583 - ] - ], - "hulls": [ - [ - [ - -0.07153, - 0.10743, - 0.01136 - ], - [ - 0.06404, - 0.00327, - 0.00358 - ], - [ - -0.05611, - -0.00021, - 0.00811 - ], - [ - 0.03184, - 0.10221, - -0.00142 - ], - [ - 0.00697, - 0.04137, - 0.03228 - ], - [ - -0.0442, - 0.0594, - -0.01228 - ], - [ - -0.02144, - 0.09165, - 0.02908 - ], - [ - 0.0631, - 0.05723, - -0.00107 - ], - [ - 0.00291, - 0.00036, - 0.00243 - ], - [ - 0.03631, - 0.08221, - 0.03787 - ] - ], - [ - [ - -0.09161, - 0.10225, - -0.00107 - ], - [ - 0.06914, - 0.0124, - 0.01082 - ], - [ - -0.04301, - 0.00087, - 0.02207 - ], - [ - 0.0331, - 0.09992, - 0.06227 - ], - [ - 0.00729, - 0.05557, - 0.00233 - ], - [ - 0.06579, - 0.10144, - -0.00492 - ], - [ - -0.04659, - 0.07612, - 0.04976 - ], - [ - -0.07923, - 0.04122, - -0.00863 - ], - [ - -0.03052, - 0.10194, - -0.00235 - ], - [ - 0.01152, - 0.01006, - 0.03891 - ] - ], - [ - [ - 0.07388, - 0.12158, - 0.057 - ], - [ - -0.09419, - 0.0045, - -0.00035 - ], - [ - 0.06616, - 0.00696, - -0.00356 - ], - [ - -0.05004, - 0.1064, - 0.05473 - ], - [ - -0.01029, - 0.00661, - 0.06143 - ], - [ - 0.02326, - 0.07918, - 0.00042 - ], - [ - -0.12052, - 0.07684, - 0.02644 - ], - [ - 0.05954, - 0.0501, - 0.05852 - ], - [ - -0.04165, - 0.05332, - 0.00092 - ], - [ - 0.00708, - 0.08094, - 0.06213 - ] - ], - [ - [ - -0.10796, - 0.05015, - 0.05267 - ], - [ - 0.09532, - 0.08732, - -0.00437 - ], - [ - 0.01438, - -0.01201, - 0.06067 - ], - [ - -0.01579, - 0.09651, - 0.00338 - ], - [ - 0.08815, - -0.00924, - 0.00912 - ], - [ - -0.04214, - 0.0078, - 0.011 - ], - [ - 0.02619, - 0.06354, - 0.06116 - ], - [ - -0.04095, - 0.06844, - 0.06988 - ], - [ - 0.03418, - 0.0353, - 0.00184 - ], - [ - -0.05753, - 0.00659, - 0.07191 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_08": 84.55824972109777 - }, - { - "J_Sec_Hair1_08": 84.55824972109777, - "J_Sec_Hair3_08": 86.75028206731109 - }, - { - "J_Sec_Hair4_08": 87.00580237252505, - "J_Sec_Hair2_08": 86.75028206731109 - }, - { - "J_Sec_Hair3_08": 87.00580237252505 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_09", - "J_Sec_Hair2_09", - "J_Sec_Hair3_09", - "J_Sec_Hair4_09" - ], - "tips": [ - [ - 0.0, - 0.10543, - 0.0 - ], - [ - 0.0, - 0.10472, - 0.0 - ], - [ - 0.0, - 0.10523, - 0.0 - ], - [ - -0.06421, - 0.13784, - 0.03773 - ] - ], - "hulls": [ - [ - [ - -0.06656, - 0.11026, - 0.04817 - ], - [ - 0.01219, - 0.00692, - -0.04538 - ], - [ - -0.03807, - 0.00521, - 0.05267 - ], - [ - -0.02272, - 0.1011, - -0.04851 - ], - [ - -0.00398, - 0.07477, - 0.0205 - ], - [ - -0.04299, - 0.03936, - -0.03427 - ], - [ - -0.05933, - 0.05738, - 0.02592 - ], - [ - -0.00549, - 0.01549, - 0.01019 - ], - [ - 0.01867, - 0.06818, - -0.04032 - ], - [ - -0.03893, - 0.10635, - 0.00977 - ] - ], - [ - [ - -0.0878, - 0.10466, - 0.03983 - ], - [ - 0.0289, - 0.00755, - -0.033 - ], - [ - -0.05281, - -0.00369, - 0.04056 - ], - [ - -0.02026, - 0.10148, - -0.03905 - ], - [ - -0.01598, - 0.05741, - 0.02002 - ], - [ - -0.03865, - 0.02918, - -0.04654 - ], - [ - 0.03076, - 0.09552, - -0.00202 - ], - [ - -0.07421, - 0.06032, - -0.0021 - ], - [ - -0.01649, - 0.00225, - 0.0018 - ], - [ - 0.00572, - 0.06012, - -0.03105 - ] - ], - [ - [ - -0.10645, - 0.09342, - 0.03535 - ], - [ - 0.03714, - 0.00481, - -0.02017 - ], - [ - -0.07792, - -0.0035, - -0.01175 - ], - [ - 0.00466, - 0.10431, - 0.00641 - ], - [ - -0.06083, - 0.07431, - -0.0248 - ], - [ - -0.0224, - 0.03201, - 0.02291 - ], - [ - -0.00924, - 0.0432, - -0.03742 - ], - [ - -0.09792, - 0.03242, - 0.03269 - ], - [ - -0.04999, - 0.08067, - 0.03692 - ], - [ - 0.04142, - 0.06406, - -0.01977 - ] - ], - [ - [ - -0.09347, - 0.12211, - 0.07242 - ], - [ - 0.03302, - 0.00987, - -0.03583 - ], - [ - -0.0617, - 0.00343, - 0.06316 - ], - [ - -0.00214, - 0.11517, - -0.01309 - ], - [ - -0.03951, - 0.03636, - -0.00902 - ], - [ - -0.03006, - 0.07742, - 0.05733 - ], - [ - -0.09269, - 0.07215, - 0.02499 - ], - [ - -0.08565, - 0.13211, - 0.00698 - ], - [ - 0.02019, - 0.05715, - 0.00097 - ], - [ - -0.00875, - 0.0122, - 0.03632 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_09": 78.90760830260274 - }, - { - "J_Sec_Hair1_09": 78.90760830260274, - "J_Sec_Hair3_09": 79.41734449788348 - }, - { - "J_Sec_Hair4_09": 75.41101903626877, - "J_Sec_Hair2_09": 79.41734449788348 - }, - { - "J_Sec_Hair3_09": 75.41101903626877 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_10", - "J_Sec_Hair2_10", - "J_Sec_Hair3_10", - "J_Sec_Hair4_10" - ], - "tips": [ - [ - 0.0, - 0.11079, - 0.0 - ], - [ - -0.0, - 0.11018, - -0.0 - ], - [ - 0.0, - 0.11069, - 0.0 - ], - [ - 0.0529, - 0.14342, - 0.02356 - ] - ], - "hulls": [ - [ - [ - 0.04231, - 0.11172, - -0.04241 - ], - [ - 0.01589, - 0.002, - 0.04537 - ], - [ - -0.00495, - 0.02182, - -0.04958 - ], - [ - 0.04112, - 0.09499, - 0.04301 - ], - [ - -0.02265, - 0.08699, - -0.02233 - ], - [ - 0.03503, - 0.05655, - -0.01278 - ], - [ - -0.0109, - 0.01586, - 0.00172 - ], - [ - 0.05173, - 0.03639, - 0.03113 - ], - [ - 0.00212, - 0.06956, - 0.02191 - ], - [ - 0.02689, - 0.10063, - -0.0017 - ] - ], - [ - [ - 0.0717, - 0.11257, - -0.00053 - ], - [ - -0.02795, - 0.01097, - -0.04021 - ], - [ - 0.04589, - -0.00093, - 0.03575 - ], - [ - -0.03562, - 0.09933, - -0.00866 - ], - [ - 0.04042, - 0.0573, - -0.04529 - ], - [ - 0.02622, - 0.0704, - 0.02681 - ], - [ - 0.01827, - 0.10505, - -0.02344 - ], - [ - 0.06876, - 0.03913, - 0.00376 - ], - [ - 0.03488, - 0.00573, - -0.02818 - ], - [ - -0.0038, - 0.02825, - 0.00528 - ] - ], - [ - [ - 0.09693, - 0.09353, - -0.02119 - ], - [ - -0.04268, - 0.00881, - -0.02346 - ], - [ - 0.06127, - -0.00235, - 0.02821 - ], - [ - -0.01431, - 0.11112, - 0.00088 - ], - [ - 0.03338, - 0.04036, - -0.0398 - ], - [ - 0.04853, - 0.07291, - 0.03372 - ], - [ - -0.00305, - 0.04895, - 0.00868 - ], - [ - 0.03431, - 0.09654, - -0.02439 - ], - [ - 0.08121, - 0.03538, - -0.01683 - ], - [ - -0.04096, - 0.06884, - -0.01959 - ] - ], - [ - [ - 0.08884, - 0.13524, - 0.0072 - ], - [ - -0.03138, - 0.00574, - -0.03145 - ], - [ - 0.05444, - 0.023, - 0.0668 - ], - [ - -0.00629, - 0.10328, - -0.01684 - ], - [ - 0.04799, - 0.01276, - -0.01855 - ], - [ - 0.04867, - 0.10273, - 0.06538 - ], - [ - 0.09698, - 0.06504, - 0.02541 - ], - [ - 0.00261, - 0.05719, - 0.03199 - ], - [ - 0.05271, - 0.08676, - 0.00187 - ], - [ - 0.01432, - 0.00543, - 0.03469 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_10": 62.44316311540696 - }, - { - "J_Sec_Hair1_10": 62.44316311540696, - "J_Sec_Hair3_10": 63.04460060622763 - }, - { - "J_Sec_Hair2_10": 63.04460060622763, - "J_Sec_Hair4_10": 60.580090488886825 - }, - { - "J_Sec_Hair3_10": 60.580090488886825 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_11", - "J_Sec_Hair2_11", - "J_Sec_Hair3_11" - ], - "tips": [ - [ - 0.0, - 0.09743, - -0.0 - ], - [ - 0.0, - 0.09762, - 0.0 - ], - [ - 0.00462, - 0.10146, - -0.01285 - ] - ], - "hulls": [ - [ - [ - -0.01221, - 0.09678, - 0.00949 - ], - [ - 0.00744, - -0.0005, - -0.00825 - ], - [ - 0.01236, - 0.0531, - -0.01826 - ], - [ - -0.01009, - 0.03598, - 0.01136 - ], - [ - 0.01125, - 0.09408, - -0.02381 - ], - [ - -0.01357, - 0.07188, - -0.02131 - ], - [ - -0.0047, - 0.0666, - 0.01137 - ], - [ - -0.01289, - 0.02218, - -0.01468 - ], - [ - -0.00933, - 0.00608, - 0.01185 - ], - [ - -0.01118, - 0.04888, - -0.01063 - ] - ], - [ - [ - 0.00784, - 0.09956, - 0.01381 - ], - [ - -0.01848, - 0.00315, - -0.01861 - ], - [ - -0.0151, - 0.06007, - -0.02411 - ], - [ - 0.00118, - 0.03907, - 0.0139 - ], - [ - 0.02421, - 0.07537, - -0.01586 - ], - [ - 0.01089, - 0.02775, - -0.02591 - ], - [ - -0.01747, - 0.08986, - -0.0102 - ], - [ - 0.00209, - 0.00872, - 0.00641 - ], - [ - 0.00358, - 0.06946, - 0.00799 - ], - [ - -0.02051, - 0.03341, - -0.01797 - ] - ], - [ - [ - -0.01959, - 0.09327, - -0.01552 - ], - [ - 0.02314, - 0.00277, - -0.01465 - ], - [ - 0.01473, - 0.05659, - 0.01522 - ], - [ - -0.02419, - 0.00699, - -0.01011 - ], - [ - 0.02832, - 0.04425, - -0.02271 - ], - [ - -0.0122, - 0.04538, - -0.00939 - ], - [ - 0.01049, - 0.01673, - 0.01335 - ], - [ - 0.00356, - 0.08581, - 0.00547 - ], - [ - 0.01037, - 0.06844, - -0.01763 - ], - [ - 0.00866, - 0.02404, - -0.02235 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_11": 38.39318372494317 - }, - { - "J_Sec_Hair1_11": 38.39318372494317, - "J_Sec_Hair3_11": 38.646242415686416 - }, - { - "J_Sec_Hair2_11": 38.646242415686416 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_12", - "J_Sec_Hair2_12", - "J_Sec_Hair3_12" - ], - "tips": [ - [ - 0.0, - 0.09903, - -0.0 - ], - [ - -0.0, - 0.09923, - 0.0 - ], - [ - -0.00434, - 0.10025, - -0.01349 - ] - ], - "hulls": [ - [ - [ - -0.01146, - 0.0976, - -0.02407 - ], - [ - 0.00785, - 0.00682, - 0.01347 - ], - [ - 0.01436, - 0.05683, - -0.00543 - ], - [ - 0.01163, - 0.09772, - 0.01073 - ], - [ - -0.01269, - 0.02645, - -0.0143 - ], - [ - -0.01314, - 0.06793, - -0.01422 - ], - [ - 0.00134, - 0.03735, - 0.01269 - ], - [ - 0.01321, - 0.08248, - -0.01929 - ], - [ - 0.01237, - 0.01593, - -0.01419 - ], - [ - 0.00027, - 0.07762, - 0.00362 - ] - ], - [ - [ - 0.00535, - 0.09915, - 0.02057 - ], - [ - -0.01048, - 0.00348, - -0.02003 - ], - [ - 0.02038, - 0.05674, - -0.01906 - ], - [ - -0.02382, - 0.08467, - -0.01369 - ], - [ - 0.00891, - 0.02845, - 0.01502 - ], - [ - -0.01737, - 0.04394, - -0.01686 - ], - [ - 0.01498, - 0.09232, - -0.02066 - ], - [ - -0.00218, - 0.06717, - 0.00628 - ], - [ - 0.01722, - 0.02052, - -0.02388 - ], - [ - 0.01373, - 0.00671, - 0.00156 - ] - ], - [ - [ - 0.01692, - 0.09514, - -0.01021 - ], - [ - -0.02117, - -0.00181, - -0.01261 - ], - [ - -0.01463, - 0.04746, - 0.01881 - ], - [ - 0.02529, - 0.00831, - -0.0068 - ], - [ - -0.01671, - 0.06707, - -0.02926 - ], - [ - -0.00603, - 0.00833, - 0.02114 - ], - [ - 0.01362, - 0.0465, - -0.00653 - ], - [ - -0.01052, - 0.07779, - 0.00235 - ], - [ - -0.01405, - 0.03121, - -0.01671 - ], - [ - -0.00321, - 0.0294, - 0.00566 - ] - ] - ], - "neighbours": [ - { - "J_Sec_Hair2_12": 39.49819143834376 - }, - { - "J_Sec_Hair1_12": 39.49819143834376, - "J_Sec_Hair3_12": 39.1444003614134 - }, - { - "J_Sec_Hair2_12": 39.1444003614134 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "J_Sec_Hair1_13" - ], - "tips": [ - [ - -0.01152, - 0.08995, - 0.00465 - ] - ], - "hulls": [ - [ - [ - -0.04045, - 0.06763, - 0.00296 - ], - [ - 0.04104, - 0.03863, - -0.00205 - ], - [ - -0.01976, - 0.00024, - 0.0052 - ], - [ - 0.00695, - 0.07097, - -0.00076 - ], - [ - -0.05262, - 0.02583, - 0.00729 - ], - [ - 0.03523, - 0.06718, - -0.00387 - ], - [ - 0.01979, - 0.02217, - 0.00226 - ], - [ - -0.01592, - 0.05624, - 0.00297 - ], - [ - -0.04328, - 0.04153, - 0.0058 - ], - [ - -0.03547, - 0.01986, - 0.00731 - ] - ] - ], - "neighbours": [ - {} - ] - } - ], - "twist": [], - "colliders": [ - { - "bone": "Hips", - "child": "Spine", - "from": 0.0, - "radius_head": 0.1498, - "radius_tail": 0.1498, - "radius": 0.1498, - "lid": true - }, - { - "bone": "Left leg", - "child": "Left knee", - "from": 0.1, - "radius_head": 0.102, - "radius_tail": 0.057, - "radius": 0.057 - }, - { - "bone": "Right leg", - "child": "Right knee", - "from": 0.1, - "radius_head": 0.102, - "radius_tail": 0.057, - "radius": 0.057 - }, - { - "bone": "Left knee", - "child": "Left ankle", - "from": 0.1, - "radius_head": 0.0883, - "radius_tail": 0.0474, - "radius": 0.0474 - }, - { - "bone": "Right knee", - "child": "Right ankle", - "from": 0.1, - "radius_head": 0.0883, - "radius_tail": 0.0474, - "radius": 0.0474 - } - ], - "weights_authored": true, - "driven_bones": [ - "Chest", - "Head", - "Hips", - "Left ankle", - "Left arm", - "Left elbow", - "Left knee", - "Left leg", - "Left shoulder", - "Left toe", - "Left wrist", - "Neck", - "Right ankle", - "Right arm", - "Right elbow", - "Right knee", - "Right leg", - "Right shoulder", - "Right toe", - "Right wrist", - "Spine" - ], - "surfaces": [ - { - "mesh": "Object_103", - "surface": 0, - "material": "N00_000_Hair_00_HAIR_Instance", - "class": "hair", - "detail": "hair", - "why": "material name says 'hair'", - "verts": 19753, - "textured": true, - "chain_share": { - "hair": 0.619 - } - }, - { - "mesh": "Object_104", - "surface": 0, - "material": "N00_000_00_FaceMouth_00_FACE_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 1053, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_105", - "surface": 0, - "material": "N00_000_00_EyeIris_00_EYE_Instance", - "class": "body", - "detail": "eyes", - "why": "material name says 'eyes'", - "verts": 212, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_106", - "surface": 0, - "material": "N00_000_00_EyeHighlight_00_EYE_Instance", - "class": "body", - "detail": "eyes", - "why": "material name says 'eyes'", - "verts": 26, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_107", - "surface": 0, - "material": "N00_000_00_Face_00_SKIN_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 2266, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_108", - "surface": 0, - "material": "N00_000_00_EyeWhite_00_EYE_Instance", - "class": "body", - "detail": "eyes", - "why": "material name says 'eyes'", - "verts": 290, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_109", - "surface": 0, - "material": "N00_000_00_FaceBrow_00_FACE_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 50, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_110", - "surface": 0, - "material": "N00_000_00_FaceEyelash_00_FACE_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 18, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_111", - "surface": 0, - "material": "N00_000_00_FaceEyeline_00_FACE_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 118, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_112", - "surface": 0, - "material": "N00_000_00_Body_00_SKIN_Instance", - "class": "body", - "detail": "skin", - "why": "material name says 'skin'", - "verts": 5870, - "textured": true, - "chain_share": { - "breast": 0.038 - } - }, - { - "mesh": "Object_113", - "surface": 0, - "material": "N00_001_03_Bottoms_01_CLOTH_Instance", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 742, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_114", - "surface": 0, - "material": "N00_004_01_Shoes_01_CLOTH_Instance", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 600, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_115", - "surface": 0, - "material": "N00_000_00_HairBack_00_HAIR_Instance", - "class": "hair", - "detail": "hair", - "why": "material name says 'hair'", - "verts": 760, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_116", - "surface": 0, - "material": "N00_002_02_Tops_01_CLOTH_Instance", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 512, - "textured": true, - "chain_share": { - "breast": 0.117 - } - }, - { - "mesh": "Object_117", - "surface": 0, - "material": "N00_010_01_Onepiece_00_CLOTH_Instance", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 52, - "textured": true, - "chain_share": { - "breast": 0.115 - } - } - ] } \ No newline at end of file diff --git a/assets/characters/skins/kiyoko.glb b/assets/characters/skins/kiyoko.glb index a41e624..1332e41 100644 Binary files a/assets/characters/skins/kiyoko.glb and b/assets/characters/skins/kiyoko.glb differ diff --git a/assets/characters/skins/kiyoko.rig.json b/assets/characters/skins/kiyoko.rig.json index cd91178..f6b3ee7 100644 --- a/assets/characters/skins/kiyoko.rig.json +++ b/assets/characters/skins/kiyoko.rig.json @@ -1,3274 +1,3274 @@ { - "roles": { - "hips": "J_Bip_C_Hips", - "head": "J_Adj_L_FaceEye", - "neck": "J_Bip_C_Neck", - "spine": [ - "J_Bip_C_Spine", - "J_Bip_C_Chest", - "J_Bip_C_UpperChest", - "J_Bip_C_Neck", - "J_Bip_C_Head", - "J_Adj_L_FaceEye" + "roles": { + "hips": "J_Bip_C_Hips", + "head": "J_Adj_L_FaceEye", + "neck": "J_Bip_C_Neck", + "spine": [ + "J_Bip_C_Spine", + "J_Bip_C_Chest", + "J_Bip_C_UpperChest", + "J_Bip_C_Neck", + "J_Bip_C_Head", + "J_Adj_L_FaceEye" + ], + "shin.R": "J_Bip_R_LowerLeg", + "shin.L": "J_Bip_L_LowerLeg", + "thigh.R": "J_Bip_R_UpperLeg", + "thigh.L": "J_Bip_L_UpperLeg", + "shoulder.R": "J_Bip_R_Shoulder", + "shoulder.L": "J_Bip_L_Shoulder", + "forearm.R": "J_Bip_R_LowerArm", + "forearm.L": "J_Bip_L_LowerArm", + "upper_arm.R": "J_Bip_R_UpperArm", + "upper_arm.L": "J_Bip_L_UpperArm", + "toe.L": "J_Bip_L_ToeBase", + "toe.R": "J_Bip_R_ToeBase", + "foot.R": "J_Bip_R_Foot", + "foot.L": "J_Bip_L_Foot", + "hand.L": "J_Bip_L_Hand", + "hand.R": "J_Bip_R_Hand" + }, + "fingers": { + "thumb.L": [ + "J_Bip_L_Thumb1", + "J_Bip_L_Thumb2", + "J_Bip_L_Thumb3" + ], + "index.L": [ + "J_Bip_L_Index1", + "J_Bip_L_Index2", + "J_Bip_L_Index3" + ], + "middle.L": [ + "J_Bip_L_Middle1", + "J_Bip_L_Middle2", + "J_Bip_L_Middle3" + ], + "ring.L": [ + "J_Bip_L_Ring1", + "J_Bip_L_Ring2", + "J_Bip_L_Ring3" + ], + "pinky.L": [ + "J_Bip_L_Little1", + "J_Bip_L_Little2", + "J_Bip_L_Little3" + ], + "thumb.R": [ + "J_Bip_R_Thumb1", + "J_Bip_R_Thumb2", + "J_Bip_R_Thumb3" + ], + "index.R": [ + "J_Bip_R_Index1", + "J_Bip_R_Index2", + "J_Bip_R_Index3" + ], + "middle.R": [ + "J_Bip_R_Middle1", + "J_Bip_R_Middle2", + "J_Bip_R_Middle3" + ], + "ring.R": [ + "J_Bip_R_Ring1", + "J_Bip_R_Ring2", + "J_Bip_R_Ring3" + ], + "pinky.R": [ + "J_Bip_R_Little1", + "J_Bip_R_Little2", + "J_Bip_R_Little3" + ] + }, + "chains": [ + { + "class": "bust", + "root_parent": "J_Bip_C_UpperChest", + "bones": [ + "J_Sec_L_Bust1", + "J_Sec_L_Bust2" + ], + "tips": [ + [ + -0.02822, + 0.05487, + 0.00675 + ], + [ + -0.02822, + 0.05487, + 0.00675 + ] + ], + "hulls": [ + [ + [ + -0.06907, + 0.01709, + 0.03093 + ], + [ + 0.01313, + 0.06773, + -0.02414 + ], + [ + -0.0464, + 0.04364, + -0.02387 + ], + [ + -0.01596, + 0.05683, + 0.03057 + ], + [ + -0.04145, + 0.02998, + 0.04945 + ], + [ + -0.05482, + 0.0409, + 0.01062 + ], + [ + 0.01149, + 0.06713, + 0.01269 + ], + [ + -0.01336, + 0.06822, + -0.00634 + ], + [ + -0.00726, + 0.05979, + -0.04593 + ], + [ + -0.03102, + 0.0483, + -0.04574 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_L_Bust2": 1.1574203608896996, + "J_Sec_R_Bust1": 0.11661945163003545 + }, + { + "J_Sec_L_Bust1": 1.1574203608896996 + } + ] + }, + { + "class": "bust", + "root_parent": "J_Bip_C_UpperChest", + "bones": [ + "J_Sec_R_Bust1", + "J_Sec_R_Bust2" + ], + "tips": [ + [ + 0.02822, + 0.05487, + 0.00675 + ], + [ + 0.02822, + 0.05487, + 0.00675 + ] + ], + "hulls": [ + [ + [ + 0.06907, + 0.01709, + 0.03093 + ], + [ + -0.01313, + 0.06773, + -0.02414 + ], + [ + 0.0464, + 0.04364, + -0.02387 + ], + [ + 0.01596, + 0.05683, + 0.03057 + ], + [ + 0.04145, + 0.02998, + 0.04945 + ], + [ + 0.05482, + 0.0409, + 0.01062 + ], + [ + -0.01149, + 0.06713, + 0.01269 + ], + [ + 0.01336, + 0.06822, + -0.00635 + ], + [ + 0.00726, + 0.05979, + -0.04593 + ], + [ + 0.03102, + 0.0483, + -0.04574 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_R_Bust2": 1.1574203608896996, + "J_Sec_L_Bust1": 0.11661945163003545 + }, + { + "J_Sec_R_Bust1": 1.1574203608896996 + } + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1", + "J_Sec_Hair2", + "J_Sec_Hair3" + ], + "tips": [ + [ + -0.00179, + 0.03486, + 0.00055 + ], + [ + -0.01116, + 0.03272, + 0.00086 + ], + [ + -0.01116, + 0.03272, + 0.00086 + ] + ], + "hulls": [ + [ + [ + -0.00846, + 0.02453, + 0.00427 + ], + [ + -0.00252, + 0.00336, + -0.00789 + ], + [ + -0.00858, + 0.00443, + 0.00697 + ], + [ + -0.00097, + 0.01453, + 0.00074 + ], + [ + -0.00426, + 0.02468, + -0.00724 + ], + [ + -0.0001, + 0.00419, + 0.00175 + ], + [ + -0.0085, + 0.01455, + 0.00554 + ], + [ + -0.00327, + 0.01394, + -0.00787 + ], + [ + -0.00215, + 0.02485, + 5e-05 + ] + ], + [ + [ + -0.01247, + 0.03034, + -0.00101 + ], + [ + -0.00214, + 0.00042, + -0.00073 + ], + [ + -0.00604, + 0.0106, + -0.00492 + ], + [ + -0.00989, + 0.02021, + 0.00049 + ], + [ + -0.00806, + 0.01005, + 0.00167 + ], + [ + -0.00397, + 0.00035, + -0.00646 + ], + [ + -0.007, + -0.00021, + 0.00274 + ], + [ + -0.00467, + 0.01074, + -0.00086 + ], + [ + -0.0089, + 0.02064, + -0.00308 + ], + [ + -0.00813, + 0.02077, + -0.0009 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2": 1.806053266471615 + }, + { + "J_Sec_Hair1": 1.806053266471615 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_02", + "J_Sec_Hair2_02", + "J_Sec_Hair3_02" + ], + "tips": [ + [ + 0.0112, + 0.03153, + 0.00487 + ], + [ + -0.01067, + 0.02305, + -0.02196 + ], + [ + -0.01067, + 0.02305, + -0.02196 + ] + ], + "hulls": [ + [ + [ + 0.0016, + 0.04157, + -0.01528 + ], + [ + 0.00699, + 0.00051, + 0.01678 + ], + [ + -0.01119, + 0.0059, + -0.01882 + ], + [ + 0.00854, + 0.02338, + 0.00235 + ], + [ + 0.01364, + 0.01905, + 0.02425 + ], + [ + 0.00383, + 0.02047, + -0.01766 + ], + [ + -0.01111, + 0.0194, + -0.00462 + ], + [ + -8e-05, + 0.00404, + -0.0011 + ], + [ + -0.00373, + 0.03054, + -0.01763 + ], + [ + 0.00353, + 0.0308, + -0.00627 + ] + ], + [ + [ + -0.00745, + 0.02625, + -0.04543 + ], + [ + -0.02883, + 0.00742, + 0.0102 + ], + [ + 0.02313, + -0.00623, + -0.01403 + ], + [ + -0.00785, + 0.01457, + -0.01602 + ], + [ + 0.00493, + 0.00057, + -0.02812 + ], + [ + 0.00058, + 0.00063, + -0.00206 + ], + [ + 0.01191, + 0.00336, + -0.0137 + ], + [ + 0.0002, + 0.01312, + -0.03115 + ], + [ + 0.00719, + -0.00665, + -0.01941 + ], + [ + 0.01758, + 0.00074, + -0.02281 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_02": 9.416776602017269 + }, + { + "J_Sec_Hair1_02": 9.416776602017269 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_03", + "J_Sec_Hair2_03", + "J_Sec_Hair3_03" + ], + "tips": [ + [ + 0.01112, + 0.04107, + 0.00338 + ], + [ + 0.00799, + 0.02983, + -0.02835 + ], + [ + 0.00799, + 0.02983, + -0.02835 + ] + ], + "hulls": [ + [ + [ + -0.0201, + 0.0489, + 0.00678 + ], + [ + 0.03387, + 0.00448, + -0.00477 + ], + [ + -0.02309, + 0.00126, + 0.00401 + ], + [ + 0.01553, + 0.03579, + 0.00278 + ], + [ + -0.0101, + 0.02591, + -0.00442 + ], + [ + 0.01098, + 0.00463, + 0.00707 + ], + [ + 0.02709, + 0.01896, + 0.00995 + ], + [ + -0.00051, + 0.04773, + 0.00211 + ], + [ + -0.00605, + 0.00842, + -0.00207 + ], + [ + -0.02312, + 0.02503, + 0.00687 + ] + ], + [ + [ + 0.01449, + 0.05794, + -0.02204 + ], + [ + -0.00187, + -0.02794, + -0.0191 + ], + [ + -0.00591, + 0.02187, + 0.00675 + ], + [ + 0.00484, + 0.01429, + -0.02831 + ], + [ + 0.00502, + -0.00808, + -0.00465 + ], + [ + 0.00022, + 0.03428, + -0.01267 + ], + [ + 0.0103, + 0.03444, + -0.03175 + ], + [ + 0.0049, + 0.0122, + -0.00825 + ], + [ + -0.00244, + -0.01093, + -0.02778 + ], + [ + 0.00672, + 0.04546, + -0.02065 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_03": 28.652466934563993 + }, + { + "J_Sec_Hair1_03": 28.652466934563993 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_04", + "J_Sec_Hair2_04", + "J_Sec_Hair3_04" + ], + "tips": [ + [ + 0.01086, + 0.03113, + -0.00479 + ], + [ + 0.01071, + 0.02966, + -0.01083 + ], + [ + 0.01071, + 0.02966, + -0.01083 + ] + ], + "hulls": [ + [ + [ + -0.00945, + 0.03644, + 0.00941 + ], + [ + 0.02187, + -0.0017, + -0.01224 + ], + [ + 0.01478, + 0.02758, + -0.00153 + ], + [ + -0.00945, + 0.00935, + 0.00977 + ], + [ + 0.01038, + 0.00411, + 0.0024 + ], + [ + 0.02431, + 0.02073, + -0.01319 + ], + [ + -0.01538, + 0.0237, + 0.00337 + ], + [ + -0.0216, + 0.01668, + 0.01518 + ], + [ + 0.02096, + 0.01062, + -0.00436 + ], + [ + -0.00229, + 0.02614, + 0.00381 + ] + ], + [ + [ + 0.01085, + 0.03502, + -0.01287 + ], + [ + -0.00752, + 0.00494, + 0.0055 + ], + [ + 0.0142, + 0.00072, + -0.00943 + ], + [ + -0.00839, + 0.02898, + 0.00731 + ], + [ + 0.01006, + 0.01734, + -0.00898 + ], + [ + 0.00142, + 0.02481, + -0.0021 + ], + [ + 0.00507, + 0.00977, + -0.00136 + ], + [ + -0.00395, + 0.01829, + 0.00677 + ], + [ + -0.01318, + 0.01289, + 0.00909 + ], + [ + 0.00095, + 0.00791, + 0.00626 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_04": 9.739047031025505 + }, + { + "J_Sec_Hair1_04": 9.739047031025505 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_05", + "J_Sec_Hair2_05", + "J_Sec_Hair3_05" + ], + "tips": [ + [ + -0.00267, + 0.03995, + 2e-05 + ], + [ + 0.01236, + 0.03728, + 0.00232 + ], + [ + 0.01236, + 0.03728, + 0.00232 + ] + ], + "hulls": [ + [ + [ + 0.01342, + 0.03693, + 0.01143 + ], + [ + -0.01062, + -0.00285, + -0.00904 + ], + [ + -0.01069, + 0.03265, + -0.01642 + ], + [ + 0.02722, + 0.00967, + 0.02045 + ], + [ + 0.01016, + 0.00729, + 0.00108 + ], + [ + -0.00572, + 0.02244, + 0.00357 + ], + [ + 0.01374, + 0.01868, + 0.01467 + ], + [ + -0.00023, + 0.01371, + -0.00984 + ], + [ + -0.00337, + -0.00127, + 0.00427 + ], + [ + 0.00218, + 0.03439, + 0.00068 + ] + ], + [ + [ + 0.01231, + 0.03733, + 0.00226 + ], + [ + -0.0092, + 0.00498, + -0.0157 + ], + [ + 0.02241, + 0.00086, + 0.0176 + ], + [ + 0.00663, + 0.01264, + 0.00269 + ], + [ + -0.00152, + 0.01836, + -0.00391 + ], + [ + 0.00801, + 0.0264, + 0.00212 + ], + [ + 0.00515, + 0.00107, + 0.00152 + ], + [ + 0.02508, + 0.01179, + 0.01744 + ], + [ + -0.00541, + 0.00665, + -0.00533 + ], + [ + 0.0129, + 0.00733, + 0.00845 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_05": 12.136693552290065 + }, + { + "J_Sec_Hair1_05": 12.136693552290065 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_06", + "J_Sec_Hair2_06", + "J_Sec_Hair3_06" + ], + "tips": [ + [ + 0.00979, + 0.06992, + -0.00277 + ], + [ + 0.03558, + 0.05906, + 0.00267 + ], + [ + 0.03558, + 0.05906, + 0.00267 + ] + ], + "hulls": [ + [ + [ + -0.00796, + 0.07334, + -0.01193 + ], + [ + 0.00803, + 0.00714, + 0.03115 + ], + [ + -0.01274, + 0.01999, + -0.01263 + ], + [ + 0.02153, + 0.05703, + 0.02492 + ], + [ + 0.00966, + 0.04728, + -0.00447 + ], + [ + 0.00102, + 0.00127, + 0.00487 + ], + [ + 0.01548, + 0.02331, + 0.01261 + ], + [ + 0.00672, + 0.03904, + 0.02892 + ], + [ + -0.01272, + 0.04146, + -0.0089 + ], + [ + -0.01088, + -0.00143, + -0.01251 + ] + ], + [ + [ + 0.05446, + 0.06037, + 0.01606 + ], + [ + -0.01448, + 0.01371, + -0.00892 + ], + [ + 0.01615, + 0.01833, + 0.03113 + ], + [ + 0.01785, + 0.04612, + -0.00383 + ], + [ + 0.03933, + 0.03937, + 0.03083 + ], + [ + 0.00474, + 0.00066, + 0.01014 + ], + [ + 0.01234, + 0.02208, + -0.00204 + ], + [ + 0.03558, + 0.05902, + 0.00277 + ], + [ + 0.02745, + 0.02677, + 0.01376 + ], + [ + 0.00781, + -0.00083, + 0.03146 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_06": 17.151499337137746 + }, + { + "J_Sec_Hair1_06": 17.151499337137746 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_07", + "J_Sec_Hair2_07", + "J_Sec_Hair3_07" + ], + "tips": [ + [ + -0.01413, + 0.04253, + 0.0056 + ], + [ + 0.01383, + 0.03096, + -0.02833 + ], + [ + 0.01383, + 0.03096, + -0.02833 + ] + ], + "hulls": [ + [ + [ + -0.00417, + 0.04397, + -0.00147 + ], + [ + -0.0007, + 6e-05, + 0.00679 + ], + [ + -0.0121, + 0.0206, + 0.00982 + ], + [ + 0.00261, + 0.0234, + -0.00292 + ], + [ + -0.01812, + 0.03811, + 0.01188 + ], + [ + 0.0051, + 0.00909, + -0.00314 + ], + [ + -0.0076, + 0.01139, + 0.00272 + ], + [ + -0.00912, + 0.03703, + 0.00536 + ], + [ + -0.00468, + 0.02742, + 0.00401 + ], + [ + -0.00924, + 0.00382, + 0.00885 + ] + ], + [ + [ + 0.00322, + 0.03511, + -0.0422 + ], + [ + -0.00339, + -0.00787, + -0.00791 + ], + [ + 0.00161, + 0.00906, + -0.03076 + ], + [ + 0.00874, + 0.00816, + 0.00108 + ], + [ + 0.01385, + 0.03095, + -0.02832 + ], + [ + 0.00455, + 0.01593, + -0.01755 + ], + [ + -0.00295, + -7e-05, + -0.02029 + ], + [ + 0.00259, + 0.02129, + -0.03849 + ], + [ + 0.00315, + 0.00665, + -0.01192 + ], + [ + -0.00018, + 0.00311, + -0.00208 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_07": 12.95251969089636 + }, + { + "J_Sec_Hair1_07": 12.95251969089636 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_08", + "J_Sec_Hair2_08", + "J_Sec_Hair3_08" + ], + "tips": [ + [ + -0.01414, + 0.06968, + -0.00406 + ], + [ + -0.04212, + 0.05622, + 0.0006 + ], + [ + -0.04212, + 0.05622, + 0.0006 + ] + ], + "hulls": [ + [ + [ + 0.01293, + 0.07861, + -0.02833 + ], + [ + -0.01331, + 0.00442, + 0.01001 + ], + [ + 0.02195, + 0.02537, + -0.02522 + ], + [ + -0.02324, + 0.05659, + 0.00872 + ], + [ + 0.00267, + 0.03296, + 0.00146 + ], + [ + 0.01345, + -0.00147, + -0.0128 + ], + [ + -0.00786, + 0.0649, + -0.01448 + ], + [ + 0.01031, + 0.04654, + -0.02964 + ], + [ + -0.02007, + 0.03646, + -0.0058 + ], + [ + -0.00207, + 0.01708, + -0.01415 + ] + ], + [ + [ + -0.04208, + 0.05627, + 0.00061 + ], + [ + 0.0224, + 0.01875, + -0.02406 + ], + [ + -0.01816, + 0.00616, + 0.014 + ], + [ + -0.00929, + 0.04589, + -0.02215 + ], + [ + -0.03867, + 0.03033, + 0.02021 + ], + [ + -0.00358, + 0.01706, + -0.00987 + ], + [ + -0.0157, + 0.03652, + 0.00136 + ], + [ + -0.01592, + 0.02578, + 0.02254 + ], + [ + 0.00962, + 0.03631, + -0.02138 + ], + [ + -0.0233, + 0.05486, + -0.01258 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_08": 22.67897996497351 + }, + { + "J_Sec_Hair1_08": 22.67897996497351 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_09", + "J_Sec_Hair2_09", + "J_Sec_Hair3_09", + "J_Sec_Hair4" + ], + "tips": [ + [ + 0.00656, + 0.0474, + 0.00209 + ], + [ + 0.01706, + 0.0446, + 0.00299 + ], + [ + 0.02854, + 0.03812, + 0.00482 + ], + [ + 0.02854, + 0.03812, + 0.00482 + ] + ], + "hulls": [ + [ + [ + 0.0041, + 0.04654, + -0.04275 + ], + [ + -0.00212, + 0.00195, + 0.02043 + ], + [ + -0.01037, + 0.00453, + -0.03435 + ], + [ + 0.01098, + 0.04256, + 0.01272 + ], + [ + 0.00989, + 0.01657, + -0.00669 + ], + [ + 0.00547, + 0.02398, + -0.03392 + ], + [ + 0.01109, + 0.02124, + 0.02163 + ], + [ + -7e-05, + 0.04024, + -0.02371 + ], + [ + -0.00138, + 0.03938, + -0.00174 + ], + [ + -0.0025, + 0.01464, + -0.04513 + ] + ], + [ + [ + 0.01155, + 0.04899, + -0.02492 + ], + [ + 0.01373, + 0.01336, + 0.01953 + ], + [ + 0.00042, + 0.00972, + -0.04317 + ], + [ + 0.00737, + 0.03226, + -0.00042 + ], + [ + 0.00988, + 0.00928, + -0.00588 + ], + [ + 0.01089, + 0.02663, + -0.03046 + ], + [ + 0.01694, + 0.03447, + 0.01905 + ], + [ + -0.00177, + 0.00778, + -0.02453 + ], + [ + 0.00883, + 0.0337, + -0.01582 + ], + [ + -0.00258, + 0.02069, + -0.0326 + ] + ], + [ + [ + 0.02849, + 0.0382, + 0.00456 + ], + [ + -0.00149, + 0.00458, + -0.03428 + ], + [ + 0.00607, + 0.00195, + 0.00792 + ], + [ + 0.01332, + 0.02759, + -0.01492 + ], + [ + 0.017, + 0.0198, + 0.00588 + ], + [ + 0.0021, + 0.00722, + -0.01197 + ], + [ + -0.00011, + 0.01283, + -0.02387 + ], + [ + 0.02521, + 0.02306, + 0.0148 + ], + [ + 0.00731, + 0.01733, + -0.01594 + ], + [ + 0.01358, + 0.00607, + 0.01594 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_09": 6.085030456593955 + }, + { + "J_Sec_Hair1_09": 6.085030456593955, + "J_Sec_Hair3_09": 6.217334650162728 + }, + { + "J_Sec_Hair2_09": 6.217334650162728 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_10", + "J_Sec_Hair2_10", + "J_Sec_Hair3_10", + "J_Sec_Hair4_10" + ], + "tips": [ + [ + -0.00523, + 0.04681, + 0.00168 + ], + [ + -0.01541, + 0.04425, + 0.00372 + ], + [ + -0.02703, + 0.03779, + 0.00697 + ], + [ + -0.02703, + 0.03779, + 0.00697 + ] + ], + "hulls": [ + [ + [ + 0.00254, + 0.03996, + -0.03884 + ], + [ + -0.0085, + 0.00606, + 0.02523 + ], + [ + 0.01355, + -0.00332, + -0.03069 + ], + [ + 0.00262, + 0.04386, + 0.00354 + ], + [ + 0.00857, + 0.00125, + 0.00306 + ], + [ + -0.00882, + 0.02119, + -0.00174 + ], + [ + -0.00108, + 0.01702, + -0.03077 + ], + [ + -0.00035, + 0.02781, + 0.02349 + ], + [ + 0.00106, + 0.03773, + -0.01836 + ], + [ + -0.00997, + 0.04078, + 0.02213 + ] + ], + [ + [ + -0.02137, + 0.03805, + 0.02335 + ], + [ + 0.00232, + 0.01406, + -0.03706 + ], + [ + -0.00047, + 0.00152, + 0.02164 + ], + [ + -0.00768, + 0.03901, + -0.01181 + ], + [ + -0.00924, + 0.01478, + -0.00068 + ], + [ + -0.00484, + 0.03417, + -0.03234 + ], + [ + -0.00787, + 0.02115, + 0.02217 + ], + [ + 0.00087, + 0.01924, + -0.01671 + ], + [ + -0.00652, + 0.03796, + 0.0041 + ], + [ + 0.00204, + 0.01813, + 0.00269 + ] + ], + [ + [ + -0.03056, + 0.04392, + 0.00527 + ], + [ + 0.0016, + 0.0091, + -0.02955 + ], + [ + -0.00748, + 0.00801, + 0.01125 + ], + [ + -0.00885, + 0.02284, + -0.01325 + ], + [ + -0.01882, + 0.02575, + 0.00806 + ], + [ + -0.02055, + 0.04065, + -0.00934 + ], + [ + -0.02723, + 0.02881, + 0.0177 + ], + [ + 0.00206, + 0.00531, + -0.01729 + ], + [ + -0.00303, + 0.01298, + -0.00933 + ], + [ + -0.01558, + 0.01187, + 0.01886 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_10": 4.348037023628208 + }, + { + "J_Sec_Hair1_10": 4.348037023628208, + "J_Sec_Hair3_10": 4.307469985860632 + }, + { + "J_Sec_Hair2_10": 4.307469985860632 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_11", + "J_Sec_Hair2_11", + "J_Sec_Hair3_11", + "J_Sec_Hair4_11" + ], + "tips": [ + [ + 0.01001, + 0.05484, + -0.0104 + ], + [ + 0.00594, + 0.05596, + -0.00663 + ], + [ + 0.00284, + 0.05652, + -0.00375 + ], + [ + 0.00284, + 0.05652, + -0.00375 + ] + ], + "hulls": [ + [ + [ + -0.0336, + 0.05072, + -0.02585 + ], + [ + 0.03431, + 0.00309, + 0.02653 + ], + [ + 0.03101, + 0.04289, + -0.01762 + ], + [ + -0.00462, + -0.0013, + -0.01728 + ], + [ + -0.05014, + 0.00712, + -0.03009 + ], + [ + 0.00588, + 0.02005, + 0.0094 + ], + [ + -0.00129, + 0.04205, + -0.0166 + ], + [ + 0.03881, + 0.03532, + 0.01253 + ], + [ + 0.02742, + 0.00285, + -0.00672 + ], + [ + -0.02026, + 0.01387, + -0.03785 + ] + ], + [ + [ + 0.01152, + 0.05435, + -0.01431 + ], + [ + -0.03654, + 0.00851, + -0.01442 + ], + [ + 0.02489, + 0.00155, + 0.01242 + ], + [ + -0.00134, + 0.01164, + -0.01049 + ], + [ + -0.01899, + 0.03823, + -0.01455 + ], + [ + 0.02563, + 0.03094, + 0.0062 + ], + [ + -0.00124, + 0.04081, + 0.0012 + ], + [ + 0.02009, + 0.02067, + -0.0136 + ], + [ + -0.01384, + -0.00133, + -0.02476 + ], + [ + 0.0111, + 0.01937, + 0.01747 + ] + ], + [ + [ + 0.01809, + 0.06474, + 0.00451 + ], + [ + -0.00252, + -0.00034, + -0.00919 + ], + [ + -0.01443, + 0.04126, + -0.0075 + ], + [ + 0.02423, + 0.02907, + 0.00268 + ], + [ + 0.01752, + 0.00581, + 0.0013 + ], + [ + -0.01333, + 0.01982, + -0.00015 + ], + [ + 0.01209, + 0.04862, + -0.00886 + ], + [ + 0.00612, + 0.03733, + 0.00917 + ], + [ + -1e-05, + 0.02768, + -0.01425 + ], + [ + -0.01285, + 0.06118, + -0.00357 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_11": 20.1695800288937 + }, + { + "J_Sec_Hair1_11": 20.1695800288937, + "J_Sec_Hair3_11": 18.79479094472742 + }, + { + "J_Sec_Hair2_11": 18.79479094472742 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_12", + "J_Sec_Hair2_12", + "J_Sec_Hair3_12", + "J_Sec_Hair4_12" + ], + "tips": [ + [ + -0.0098, + 0.05611, + -0.01118 + ], + [ + -0.00567, + 0.05729, + -0.00697 + ], + [ + -0.0024, + 0.05789, + -0.00363 + ], + [ + -0.0024, + 0.05789, + -0.00363 + ] + ], + "hulls": [ + [ + [ + 0.035, + 0.05517, + -0.02733 + ], + [ + -0.03495, + 0.00451, + 0.02597 + ], + [ + 0.00651, + 0.00181, + -0.01847 + ], + [ + -0.03053, + 0.04456, + -0.01868 + ], + [ + 0.05194, + 0.01062, + -0.031 + ], + [ + -0.00549, + 0.02111, + 0.00883 + ], + [ + -0.02727, + 0.00374, + -0.00708 + ], + [ + -0.03845, + 0.03768, + 0.01136 + ], + [ + 0.00835, + 0.03947, + -0.0201 + ], + [ + -0.01009, + 0.05696, + 0.00016 + ] + ], + [ + [ + -0.01724, + 0.05565, + -0.0071 + ], + [ + 0.03655, + 0.01314, + -0.01521 + ], + [ + -0.02476, + 0.00201, + 0.01212 + ], + [ + 0.00092, + 0.01441, + -0.01105 + ], + [ + 0.01819, + 0.04209, + -0.01466 + ], + [ + -0.00506, + 0.0267, + 0.01481 + ], + [ + 0.0236, + 0.00453, + 0.00578 + ], + [ + -0.02433, + 0.01445, + -0.00795 + ], + [ + 0.01407, + 0.00225, + -0.02558 + ], + [ + 0.00066, + 0.04337, + 0.00041 + ] + ], + [ + [ + -0.00061, + 0.06864, + -0.00452 + ], + [ + -0.00632, + -0.00079, + -0.00813 + ], + [ + 0.00826, + 0.03388, + -0.01943 + ], + [ + -0.02358, + 0.03881, + 0.00234 + ], + [ + 0.0134, + 0.01708, + -0.00064 + ], + [ + 0.0104, + 0.04449, + 0.00057 + ], + [ + -0.00671, + 0.01917, + 0.00805 + ], + [ + -0.01297, + 0.01892, + -0.01268 + ], + [ + -0.01932, + 0.06422, + 0.00449 + ], + [ + -0.01306, + 0.04876, + -0.00903 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_12": 20.797115504793354 + }, + { + "J_Sec_Hair1_12": 20.797115504793354, + "J_Sec_Hair3_12": 20.753898724733332 + }, + { + "J_Sec_Hair2_12": 20.753898724733332 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_13", + "J_Sec_Hair2_13", + "J_Sec_Hair3_13" + ], + "tips": [ + [ + 0.02011, + 0.09181, + -0.00286 + ], + [ + 0.03975, + 0.05486, + -0.00267 + ], + [ + 0.03975, + 0.05486, + -0.00267 + ] + ], + "hulls": [ + [ + [ + 0.01278, + 0.09432, + 0.00743 + ], + [ + 0.00503, + -0.00394, + -0.02307 + ], + [ + 0.03257, + 0.04406, + -0.00514 + ], + [ + 0.01058, + 0.01845, + 0.00721 + ], + [ + 0.01238, + 0.07186, + -0.0156 + ], + [ + 0.01675, + 0.02279, + -0.02178 + ], + [ + 0.02799, + 0.0669, + 0.0055 + ], + [ + 0.0045, + -0.00404, + 0.00515 + ], + [ + 0.0101, + 0.04025, + -0.00087 + ], + [ + 0.03093, + 0.08205, + -0.01168 + ] + ], + [ + [ + 0.03347, + 0.06049, + -0.00477 + ], + [ + -0.01279, + 0.00778, + -0.00465 + ], + [ + -0.00957, + 0.05038, + 0.01515 + ], + [ + 0.02852, + 0.02198, + -0.0276 + ], + [ + -0.00084, + 0.03441, + -0.00966 + ], + [ + -0.00561, + 0.02163, + 0.01707 + ], + [ + 0.00605, + 0.05881, + -0.00897 + ], + [ + 0.00839, + -0.00286, + -0.00961 + ], + [ + 0.00835, + 0.06527, + 0.00993 + ], + [ + 0.01485, + 0.03518, + -0.02093 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_13": 28.349896885998273 + }, + { + "J_Sec_Hair1_13": 28.349896885998273 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_14", + "J_Sec_Hair2_14", + "J_Sec_Hair3_14" + ], + "tips": [ + [ + -0.02011, + 0.09181, + -0.00286 + ], + [ + -0.03975, + 0.05486, + -0.00267 + ], + [ + -0.03975, + 0.05486, + -0.00267 + ] + ], + "hulls": [ + [ + [ + -0.01256, + 0.0943, + 0.00048 + ], + [ + -0.00487, + -0.00404, + -0.02413 + ], + [ + -0.02955, + 0.03699, + 0.00229 + ], + [ + -0.01589, + 0.06506, + -0.01716 + ], + [ + -0.00892, + 0.01129, + 0.00738 + ], + [ + -0.01583, + 0.02999, + -0.0233 + ], + [ + -0.03173, + 0.067, + 0.00648 + ], + [ + -0.01225, + 0.0508, + 0.00085 + ], + [ + -0.02915, + 0.08182, + -0.00816 + ], + [ + -0.00797, + 0.03032, + 0.00049 + ] + ], + [ + [ + -0.02436, + 0.06451, + -0.00364 + ], + [ + -0.00762, + -0.00295, + -0.00432 + ], + [ + 0.01095, + 0.03578, + 0.01561 + ], + [ + -0.02764, + 0.02899, + -0.02757 + ], + [ + 0.00397, + 0.0245, + -0.01162 + ], + [ + 0.00189, + 0.06375, + 0.01059 + ], + [ + -0.004, + 0.05114, + -0.00914 + ], + [ + 0.0006, + 0.01537, + 0.0133 + ], + [ + 0.0135, + 0.00781, + -0.00586 + ], + [ + -0.03979, + 0.05491, + -0.00254 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_14": 28.360807644433685 + }, + { + "J_Sec_Hair1_14": 28.360807644433685 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_15", + "J_Sec_Hair2_15", + "J_Sec_Hair3_15" + ], + "tips": [ + [ + -0.01211, + 0.05321, + 0.01422 + ], + [ + -0.01309, + 0.05293, + 0.01456 + ], + [ + -0.01309, + 0.05293, + 0.01456 + ] + ], + "hulls": [ + [ + [ + -0.0267, + 0.05046, + -0.00606 + ], + [ + -0.00062, + 0.00413, + -0.00789 + ], + [ + -0.00667, + 0.03155, + 0.00125 + ], + [ + -0.01534, + 0.01552, + -0.00251 + ], + [ + -0.013, + 0.04884, + 0.00424 + ], + [ + -0.02028, + 0.03313, + -0.00813 + ], + [ + -0.01922, + 0.03774, + 0.00385 + ], + [ + -0.00317, + 0.01852, + -0.00357 + ], + [ + -0.02513, + 0.0499, + 0.0063 + ], + [ + -0.01121, + 0.02662, + -0.00923 + ] + ], + [ + [ + -0.03068, + 0.05762, + 0.02615 + ], + [ + -0.0165, + 0.0015, + -0.01958 + ], + [ + 0.00662, + 0.03992, + -0.00269 + ], + [ + -0.03151, + 0.03456, + -0.00473 + ], + [ + -0.00954, + 0.05841, + 0.00746 + ], + [ + -0.02504, + 0.01517, + -0.00152 + ], + [ + -0.02627, + 0.04302, + 0.01308 + ], + [ + -0.02356, + 0.04756, + -0.00138 + ], + [ + -0.01502, + 0.05268, + 0.02041 + ], + [ + -0.02537, + 0.02337, + -0.01417 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_15": 26.225395679503343 + }, + { + "J_Sec_Hair1_15": 26.225395679503343 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_16", + "J_Sec_Hair2_16", + "J_Sec_Hair3_16" + ], + "tips": [ + [ + 0.01211, + 0.05321, + 0.01422 + ], + [ + 0.01309, + 0.05293, + 0.01456 + ], + [ + 0.01309, + 0.05293, + 0.01456 + ] + ], + "hulls": [ + [ + [ + 0.02113, + 0.05354, + -0.00554 + ], + [ + 0.00076, + 0.00423, + -0.00969 + ], + [ + 0.01547, + 0.02449, + 0.00187 + ], + [ + 0.00814, + 0.04121, + 0.00081 + ], + [ + 0.01089, + 0.01685, + -0.0105 + ], + [ + 0.01191, + 0.00638, + -0.0002 + ], + [ + 0.02305, + 0.04064, + 0.00039 + ], + [ + 0.00967, + 0.0323, + -0.00837 + ], + [ + 0.00387, + 0.02312, + -0.00284 + ], + [ + 0.01576, + 0.0526, + 0.00473 + ] + ], + [ + [ + 0.03068, + 0.05762, + 0.02615 + ], + [ + 0.01647, + 0.0008, + -0.01661 + ], + [ + -0.00663, + 0.03986, + -0.00258 + ], + [ + 0.03067, + 0.03647, + -0.00831 + ], + [ + 0.00894, + 0.0606, + 0.01156 + ], + [ + 0.02504, + 0.01517, + -0.00152 + ], + [ + 0.03677, + 0.04015, + 0.01336 + ], + [ + 0.02325, + 0.05038, + 0.00229 + ], + [ + 0.00499, + 0.04553, + 0.01218 + ], + [ + 0.02024, + 0.01552, + -0.01657 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_16": 26.322509970647936 + }, + { + "J_Sec_Hair1_16": 26.322509970647936 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_17", + "J_Sec_Hair2_17", + "J_Sec_Hair3_17" + ], + "tips": [ + [ + 0.02348, + 0.0957, + -0.02381 + ], + [ + -0.00217, + 0.10077, + -0.01388 + ], + [ + -0.00217, + 0.10077, + -0.01388 + ] + ], + "hulls": [ + [ + [ + 0.05658, + 0.08838, + -0.03346 + ], + [ + 0.00273, + -0.00066, + -5e-05 + ], + [ + 0.00587, + 0.05197, + -0.03052 + ], + [ + 0.02929, + 0.03536, + 0.00239 + ], + [ + 0.01178, + 0.09133, + -0.04513 + ], + [ + 0.02602, + 0.08072, + -0.00753 + ], + [ + 0.04559, + 0.05799, + -0.02396 + ], + [ + 0.01376, + 0.02351, + -0.02037 + ], + [ + 0.02304, + 0.07156, + -0.03101 + ], + [ + 0.03541, + 0.0901, + -0.04576 + ] + ], + [ + [ + -0.00135, + 0.11115, + -0.01126 + ], + [ + 0.02807, + 0.00058, + 0.01304 + ], + [ + -0.00773, + 0.04691, + -0.03614 + ], + [ + 0.03907, + 0.07185, + -0.02125 + ], + [ + 0.01188, + 0.04645, + 0.01195 + ], + [ + -0.01439, + 0.00065, + -0.00596 + ], + [ + 0.01794, + 0.01334, + -0.02856 + ], + [ + -0.02031, + 0.08882, + -0.02963 + ], + [ + 0.04174, + 0.03605, + -0.01935 + ], + [ + 0.00605, + 0.07751, + 0.00105 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_17": 20.076729074867217 + }, + { + "J_Sec_Hair1_17": 20.076729074867217 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "J_Bip_C_Head", + "bones": [ + "J_Sec_Hair1_18", + "J_Sec_Hair2_18", + "J_Sec_Hair3_18" + ], + "tips": [ + [ + -0.02373, + 0.0961, + -0.02398 + ], + [ + 0.00214, + 0.10123, + -0.01396 + ], + [ + 0.00214, + 0.10123, + -0.01396 + ] + ], + "hulls": [ + [ + [ + -0.05372, + 0.0887, + -0.03821 + ], + [ + -0.00155, + 0.0015, + 0.0044 + ], + [ + -0.0045, + 0.05349, + -0.02979 + ], + [ + -0.04112, + 0.05469, + -0.00352 + ], + [ + -0.00889, + 0.09313, + -0.04332 + ], + [ + -0.02936, + 0.09161, + -0.00708 + ], + [ + -0.02751, + 0.02093, + -0.01345 + ], + [ + 0.00019, + 0.02766, + -0.00731 + ], + [ + -0.02593, + 0.07378, + -0.03643 + ], + [ + -0.05391, + 0.08442, + -0.01154 + ] + ], + [ + [ + 0.00123, + 0.1116, + -0.01115 + ], + [ + -0.03379, + -0.00147, + 0.00928 + ], + [ + 0.01417, + 0.03707, + -0.03033 + ], + [ + -0.04251, + 0.06226, + -0.012 + ], + [ + -0.00944, + 0.03602, + 0.01124 + ], + [ + 0.01272, + 0.00165, + -0.00377 + ], + [ + 0.01934, + 0.07901, + -0.0333 + ], + [ + -0.03593, + 0.02393, + -0.02433 + ], + [ + -0.00213, + 0.07633, + 0.00866 + ], + [ + -0.03081, + 0.09597, + -0.01974 + ] + ], + [] + ], + "neighbours": [ + { + "J_Sec_Hair2_18": 20.05058973935537 + }, + { + "J_Sec_Hair1_18": 20.05058973935537 + }, + {} + ] + } ], - "shin.R": "J_Bip_R_LowerLeg", - "shin.L": "J_Bip_L_LowerLeg", - "thigh.L": "J_Bip_L_UpperLeg", - "thigh.R": "J_Bip_R_UpperLeg", - "shoulder.R": "J_Bip_R_Shoulder", - "shoulder.L": "J_Bip_L_Shoulder", - "forearm.L": "J_Bip_L_LowerArm", - "forearm.R": "J_Bip_R_LowerArm", - "upper_arm.R": "J_Bip_R_UpperArm", - "upper_arm.L": "J_Bip_L_UpperArm", - "toe.R": "J_Bip_R_ToeBase", - "toe.L": "J_Bip_L_ToeBase", - "foot.L": "J_Bip_L_Foot", - "foot.R": "J_Bip_R_Foot", - "hand.R": "J_Bip_R_Hand", - "hand.L": "J_Bip_L_Hand" - }, - "fingers": { - "thumb.L": [ - "J_Bip_L_Thumb1", - "J_Bip_L_Thumb2", - "J_Bip_L_Thumb3" + "twist": [], + "surfaces": [ + { + "mesh": "Object_10", + "surface": 0, + "material": "N00_001_03_Bottoms_01_CLOTH_Instance", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 742, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_11", + "surface": 0, + "material": "N00_001_01_Shoes_01_CLOTH_Instance", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 410, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_13", + "surface": 0, + "material": "N00_000_00_FaceMouth_00_FACE_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 1073, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_14", + "surface": 0, + "material": "N00_000_00_EyeIris_00_EYE_Instance", + "class": "body", + "detail": "eyes", + "why": "material name says 'eyes'", + "verts": 268, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_15", + "surface": 0, + "material": "N00_000_00_EyeHighlight_00_EYE_Instance", + "class": "body", + "detail": "eyes", + "why": "material name says 'eyes'", + "verts": 200, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_16", + "surface": 0, + "material": "N00_000_00_Face_00_SKIN_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 2266, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_17", + "surface": 0, + "material": "N00_000_00_EyeWhite_00_EYE_Instance", + "class": "body", + "detail": "eyes", + "why": "material name says 'eyes'", + "verts": 290, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_18", + "surface": 0, + "material": "N00_000_00_FaceBrow_00_FACE_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 52, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_19", + "surface": 0, + "material": "N00_000_00_FaceEyeline_00_FACE_Instance", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 168, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_21", + "surface": 0, + "material": "N00_000_Hair_00_HAIR_Instance", + "class": "hair", + "detail": "hair", + "why": "material name says 'hair'", + "verts": 22436, + "textured": true, + "chain_share": { + "hair": 0.281 + } + }, + { + "mesh": "Object_7", + "surface": 0, + "material": "N00_000_00_Body_00_SKIN_Instance", + "class": "body", + "detail": "skin", + "why": "material name says 'skin'", + "verts": 5103, + "textured": true, + "chain_share": { + "bust": 0.001 + } + }, + { + "mesh": "Object_8", + "surface": 0, + "material": "N00_000_00_HairBack_00_HAIR_Instance", + "class": "hair", + "detail": "hair", + "why": "material name says 'hair'", + "verts": 700, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_9", + "surface": 0, + "material": "N00_007_01_Tops_01_CLOTH_Instance", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 814, + "textured": true, + "chain_share": { + "bust": 0.061 + } + } ], - "index.L": [ - "J_Bip_L_Index1", - "J_Bip_L_Index2", - "J_Bip_L_Index3" + "colliders": [ + { + "bone": "J_Bip_C_Hips", + "child": "J_Bip_C_Spine", + "from": 0.0, + "radius_head": 0.1447, + "radius_tail": 0.1447, + "radius": 0.1447, + "lid": true + }, + { + "bone": "J_Bip_L_UpperLeg", + "child": "J_Bip_L_LowerLeg", + "from": 0.1, + "radius_head": 0.0974, + "radius_tail": 0.0554, + "radius": 0.0554 + }, + { + "bone": "J_Bip_R_UpperLeg", + "child": "J_Bip_R_LowerLeg", + "from": 0.1, + "radius_head": 0.0974, + "radius_tail": 0.0554, + "radius": 0.0554 + }, + { + "bone": "J_Bip_L_LowerLeg", + "child": "J_Bip_L_Foot", + "from": 0.1, + "radius_head": 0.0922, + "radius_tail": 0.0493, + "radius": 0.0493 + }, + { + "bone": "J_Bip_R_LowerLeg", + "child": "J_Bip_R_Foot", + "from": 0.1, + "radius_head": 0.0922, + "radius_tail": 0.0493, + "radius": 0.0493 + } ], - "middle.L": [ - "J_Bip_L_Middle1", - "J_Bip_L_Middle2", - "J_Bip_L_Middle3" - ], - "ring.L": [ - "J_Bip_L_Ring1", - "J_Bip_L_Ring2", - "J_Bip_L_Ring3" - ], - "pinky.L": [ - "J_Bip_L_Little1", - "J_Bip_L_Little2", - "J_Bip_L_Little3" - ], - "thumb.R": [ - "J_Bip_R_Thumb1", - "J_Bip_R_Thumb2", - "J_Bip_R_Thumb3" - ], - "index.R": [ - "J_Bip_R_Index1", - "J_Bip_R_Index2", - "J_Bip_R_Index3" - ], - "middle.R": [ - "J_Bip_R_Middle1", - "J_Bip_R_Middle2", - "J_Bip_R_Middle3" - ], - "ring.R": [ - "J_Bip_R_Ring1", - "J_Bip_R_Ring2", - "J_Bip_R_Ring3" - ], - "pinky.R": [ - "J_Bip_R_Little1", - "J_Bip_R_Little2", - "J_Bip_R_Little3" + "weights_authored": true, + "driven_bones": [ + "J_Adj_L_FaceEye", + "J_Bip_C_Chest", + "J_Bip_C_Head", + "J_Bip_C_Hips", + "J_Bip_C_Neck", + "J_Bip_C_Spine", + "J_Bip_C_UpperChest", + "J_Bip_L_Foot", + "J_Bip_L_Hand", + "J_Bip_L_LowerArm", + "J_Bip_L_LowerLeg", + "J_Bip_L_Shoulder", + "J_Bip_L_ToeBase", + "J_Bip_L_UpperArm", + "J_Bip_L_UpperLeg", + "J_Bip_R_Foot", + "J_Bip_R_Hand", + "J_Bip_R_LowerArm", + "J_Bip_R_LowerLeg", + "J_Bip_R_Shoulder", + "J_Bip_R_ToeBase", + "J_Bip_R_UpperArm", + "J_Bip_R_UpperLeg", + "Root" ] - }, - "chains": [ - { - "class": "bust", - "root_parent": "J_Bip_C_UpperChest", - "bones": [ - "J_Sec_L_Bust1", - "J_Sec_L_Bust2" - ], - "tips": [ - [ - -0.02822, - 0.05487, - 0.00675 - ], - [ - -0.02822, - 0.05487, - 0.00675 - ] - ], - "hulls": [ - [ - [ - -0.06907, - 0.01709, - 0.03093 - ], - [ - 0.01313, - 0.06773, - -0.02415 - ], - [ - -0.0464, - 0.04364, - -0.02388 - ], - [ - -0.01596, - 0.05683, - 0.03057 - ], - [ - -0.04145, - 0.02998, - 0.04945 - ], - [ - -0.05482, - 0.0409, - 0.01062 - ], - [ - 0.01149, - 0.06713, - 0.01269 - ], - [ - -0.01336, - 0.06822, - -0.00635 - ], - [ - -0.00726, - 0.05979, - -0.04593 - ], - [ - -0.03102, - 0.0483, - -0.04574 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_L_Bust2": 1.1574203600849464, - "J_Sec_R_Bust1": 0.11661945163003545 - }, - { - "J_Sec_L_Bust1": 1.1574203600849464 - } - ] - }, - { - "class": "bust", - "root_parent": "J_Bip_C_UpperChest", - "bones": [ - "J_Sec_R_Bust1", - "J_Sec_R_Bust2" - ], - "tips": [ - [ - 0.02822, - 0.05487, - 0.00675 - ], - [ - 0.02822, - 0.05487, - 0.00675 - ] - ], - "hulls": [ - [ - [ - 0.06907, - 0.01709, - 0.03093 - ], - [ - -0.01313, - 0.06773, - -0.02415 - ], - [ - 0.0464, - 0.04364, - -0.02388 - ], - [ - 0.01596, - 0.05683, - 0.03057 - ], - [ - 0.04145, - 0.02998, - 0.04945 - ], - [ - 0.05482, - 0.0409, - 0.01062 - ], - [ - -0.01149, - 0.06713, - 0.01269 - ], - [ - 0.01336, - 0.06822, - -0.00635 - ], - [ - 0.00726, - 0.05979, - -0.04593 - ], - [ - 0.03102, - 0.0483, - -0.04574 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_R_Bust2": 1.1574203600849464, - "J_Sec_L_Bust1": 0.11661945163003545 - }, - { - "J_Sec_R_Bust1": 1.1574203600849464 - } - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_01", - "J_Sec_Hair2_01", - "J_Sec_Hair3_01" - ], - "tips": [ - [ - -0.00179, - 0.03486, - 0.00055 - ], - [ - -0.01116, - 0.03272, - 0.00086 - ], - [ - -0.01116, - 0.03272, - 0.00086 - ] - ], - "hulls": [ - [ - [ - -0.00846, - 0.02453, - 0.00427 - ], - [ - -0.00252, - 0.00336, - -0.00789 - ], - [ - -0.00858, - 0.00443, - 0.00697 - ], - [ - -0.00097, - 0.01453, - 0.00074 - ], - [ - -0.00426, - 0.02468, - -0.00724 - ], - [ - -0.0001, - 0.00419, - 0.00175 - ], - [ - -0.0085, - 0.01455, - 0.00554 - ], - [ - -0.00327, - 0.01394, - -0.00788 - ], - [ - -0.00215, - 0.02485, - 5e-05 - ] - ], - [ - [ - -0.01247, - 0.03034, - -0.00101 - ], - [ - -0.00214, - 0.00042, - -0.00073 - ], - [ - -0.00604, - 0.0106, - -0.00492 - ], - [ - -0.00989, - 0.02021, - 0.00049 - ], - [ - -0.00806, - 0.01005, - 0.00167 - ], - [ - -0.00397, - 0.00036, - -0.00646 - ], - [ - -0.007, - -0.00021, - 0.00274 - ], - [ - -0.00467, - 0.01074, - -0.00086 - ], - [ - -0.0089, - 0.02064, - -0.00308 - ], - [ - -0.00813, - 0.02077, - -0.0009 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_01": 1.806053266471615 - }, - { - "J_Sec_Hair1_01": 1.806053266471615 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_02", - "J_Sec_Hair2_02", - "J_Sec_Hair3_02" - ], - "tips": [ - [ - 0.0112, - 0.03153, - 0.00487 - ], - [ - -0.01067, - 0.02305, - -0.02196 - ], - [ - -0.01067, - 0.02305, - -0.02196 - ] - ], - "hulls": [ - [ - [ - 0.0016, - 0.04157, - -0.01528 - ], - [ - 0.00699, - 0.00051, - 0.01678 - ], - [ - -0.01119, - 0.0059, - -0.01882 - ], - [ - 0.00854, - 0.02338, - 0.00234 - ], - [ - 0.01364, - 0.01905, - 0.02425 - ], - [ - 0.00383, - 0.02047, - -0.01766 - ], - [ - -0.01111, - 0.0194, - -0.00462 - ], - [ - -8e-05, - 0.00405, - -0.0011 - ], - [ - -0.00373, - 0.03054, - -0.01763 - ], - [ - 0.00353, - 0.0308, - -0.00627 - ] - ], - [ - [ - -0.00745, - 0.02625, - -0.04543 - ], - [ - -0.02883, - 0.00742, - 0.0102 - ], - [ - 0.02313, - -0.00623, - -0.01403 - ], - [ - -0.00785, - 0.01457, - -0.01602 - ], - [ - 0.00493, - 0.00057, - -0.02812 - ], - [ - 0.00058, - 0.00063, - -0.00206 - ], - [ - 0.01191, - 0.00336, - -0.0137 - ], - [ - 0.0002, - 0.01312, - -0.03115 - ], - [ - 0.00719, - -0.00665, - -0.01942 - ], - [ - 0.01758, - 0.00074, - -0.02281 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_02": 9.416776602017269 - }, - { - "J_Sec_Hair1_02": 9.416776602017269 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_03", - "J_Sec_Hair2_03", - "J_Sec_Hair3_03" - ], - "tips": [ - [ - 0.01112, - 0.04107, - 0.00338 - ], - [ - 0.00799, - 0.02983, - -0.02835 - ], - [ - 0.00799, - 0.02983, - -0.02835 - ] - ], - "hulls": [ - [ - [ - -0.0201, - 0.0489, - 0.00678 - ], - [ - 0.03387, - 0.00448, - -0.00477 - ], - [ - -0.02309, - 0.00126, - 0.00401 - ], - [ - 0.01553, - 0.03579, - 0.00278 - ], - [ - -0.0101, - 0.02591, - -0.00442 - ], - [ - 0.01098, - 0.00463, - 0.00707 - ], - [ - 0.02709, - 0.01896, - 0.00995 - ], - [ - -0.00051, - 0.04773, - 0.00211 - ], - [ - -0.00605, - 0.00842, - -0.00208 - ], - [ - -0.02312, - 0.02504, - 0.00687 - ] - ], - [ - [ - 0.01449, - 0.05794, - -0.02204 - ], - [ - -0.00187, - -0.02794, - -0.0191 - ], - [ - -0.00591, - 0.02187, - 0.00675 - ], - [ - 0.00484, - 0.01429, - -0.02831 - ], - [ - 0.00503, - -0.00808, - -0.00465 - ], - [ - 0.00022, - 0.03427, - -0.01267 - ], - [ - 0.0103, - 0.03444, - -0.03176 - ], - [ - 0.0049, - 0.01219, - -0.00826 - ], - [ - -0.00244, - -0.01093, - -0.02778 - ], - [ - 0.00672, - 0.04546, - -0.02065 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_03": 28.652466934563993 - }, - { - "J_Sec_Hair1_03": 28.652466934563993 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_04", - "J_Sec_Hair2_04", - "J_Sec_Hair3_04" - ], - "tips": [ - [ - 0.01086, - 0.03113, - -0.00479 - ], - [ - 0.01071, - 0.02966, - -0.01083 - ], - [ - 0.01071, - 0.02966, - -0.01083 - ] - ], - "hulls": [ - [ - [ - -0.00945, - 0.03644, - 0.00941 - ], - [ - 0.02187, - -0.0017, - -0.01224 - ], - [ - 0.01478, - 0.02758, - -0.00153 - ], - [ - -0.00945, - 0.00935, - 0.00976 - ], - [ - 0.01038, - 0.00411, - 0.0024 - ], - [ - 0.02431, - 0.02073, - -0.01319 - ], - [ - -0.01538, - 0.02371, - 0.00337 - ], - [ - -0.0216, - 0.01668, - 0.01518 - ], - [ - 0.02096, - 0.01062, - -0.00436 - ], - [ - -0.00229, - 0.02614, - 0.00381 - ] - ], - [ - [ - 0.01085, - 0.03503, - -0.01287 - ], - [ - -0.00752, - 0.00495, - 0.0055 - ], - [ - 0.0142, - 0.00072, - -0.00944 - ], - [ - -0.00839, - 0.02898, - 0.00731 - ], - [ - 0.01006, - 0.01734, - -0.00898 - ], - [ - 0.00142, - 0.02481, - -0.0021 - ], - [ - 0.00507, - 0.00977, - -0.00136 - ], - [ - -0.00395, - 0.01829, - 0.00677 - ], - [ - -0.01318, - 0.01289, - 0.00909 - ], - [ - 0.00095, - 0.00791, - 0.00626 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_04": 9.739047031025505 - }, - { - "J_Sec_Hair1_04": 9.739047031025505 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_05", - "J_Sec_Hair2_05", - "J_Sec_Hair3_05" - ], - "tips": [ - [ - -0.00267, - 0.03995, - 2e-05 - ], - [ - 0.01236, - 0.03728, - 0.00232 - ], - [ - 0.01236, - 0.03728, - 0.00232 - ] - ], - "hulls": [ - [ - [ - 0.01342, - 0.03693, - 0.01143 - ], - [ - -0.01062, - -0.00285, - -0.00904 - ], - [ - -0.01069, - 0.03266, - -0.01642 - ], - [ - 0.02722, - 0.00967, - 0.02045 - ], - [ - 0.01016, - 0.00729, - 0.00108 - ], - [ - -0.00572, - 0.02245, - 0.00357 - ], - [ - 0.01374, - 0.01868, - 0.01467 - ], - [ - -0.00023, - 0.01371, - -0.00985 - ], - [ - -0.00337, - -0.00126, - 0.00426 - ], - [ - 0.00218, - 0.0344, - 0.00068 - ] - ], - [ - [ - 0.01231, - 0.03734, - 0.00225 - ], - [ - -0.0092, - 0.00498, - -0.0157 - ], - [ - 0.02241, - 0.00086, - 0.0176 - ], - [ - 0.00663, - 0.01264, - 0.00269 - ], - [ - -0.00152, - 0.01836, - -0.00391 - ], - [ - 0.00801, - 0.0264, - 0.00212 - ], - [ - 0.00515, - 0.00107, - 0.00152 - ], - [ - 0.02508, - 0.01179, - 0.01744 - ], - [ - -0.00541, - 0.00665, - -0.00533 - ], - [ - 0.0129, - 0.00733, - 0.00845 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_05": 12.136693552290065 - }, - { - "J_Sec_Hair1_05": 12.136693552290065 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_06", - "J_Sec_Hair2_06", - "J_Sec_Hair3_06" - ], - "tips": [ - [ - 0.00979, - 0.06992, - -0.00277 - ], - [ - 0.03558, - 0.05906, - 0.00267 - ], - [ - 0.03558, - 0.05906, - 0.00267 - ] - ], - "hulls": [ - [ - [ - -0.00796, - 0.07334, - -0.01193 - ], - [ - 0.00803, - 0.00715, - 0.03115 - ], - [ - -0.01274, - 0.01999, - -0.01263 - ], - [ - 0.02153, - 0.05703, - 0.02492 - ], - [ - 0.00966, - 0.04728, - -0.00447 - ], - [ - 0.00102, - 0.00127, - 0.00487 - ], - [ - 0.01548, - 0.02331, - 0.01261 - ], - [ - 0.00672, - 0.03904, - 0.02892 - ], - [ - -0.01272, - 0.04146, - -0.0089 - ], - [ - -0.01088, - -0.00143, - -0.01251 - ] - ], - [ - [ - 0.05446, - 0.06037, - 0.01606 - ], - [ - -0.01448, - 0.01371, - -0.00892 - ], - [ - 0.01615, - 0.01833, - 0.03113 - ], - [ - 0.01785, - 0.04612, - -0.00383 - ], - [ - 0.03933, - 0.03937, - 0.03083 - ], - [ - 0.00474, - 0.00067, - 0.01014 - ], - [ - 0.01234, - 0.02208, - -0.00204 - ], - [ - 0.03558, - 0.05902, - 0.00277 - ], - [ - 0.02745, - 0.02677, - 0.01376 - ], - [ - 0.00781, - -0.00083, - 0.03146 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_06": 17.151499337137746 - }, - { - "J_Sec_Hair1_06": 17.151499337137746 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_07", - "J_Sec_Hair2_07", - "J_Sec_Hair3_07" - ], - "tips": [ - [ - -0.01413, - 0.04253, - 0.0056 - ], - [ - 0.01383, - 0.03096, - -0.02833 - ], - [ - 0.01383, - 0.03096, - -0.02833 - ] - ], - "hulls": [ - [ - [ - -0.00417, - 0.04397, - -0.00148 - ], - [ - -0.0007, - 6e-05, - 0.00679 - ], - [ - -0.0121, - 0.0206, - 0.00982 - ], - [ - 0.00261, - 0.0234, - -0.00292 - ], - [ - -0.01812, - 0.03812, - 0.01188 - ], - [ - 0.0051, - 0.00909, - -0.00314 - ], - [ - -0.0076, - 0.01139, - 0.00272 - ], - [ - -0.00912, - 0.03704, - 0.00536 - ], - [ - -0.00468, - 0.02742, - 0.00401 - ], - [ - -0.00924, - 0.00382, - 0.00885 - ] - ], - [ - [ - 0.00322, - 0.03511, - -0.0422 - ], - [ - -0.00339, - -0.00787, - -0.00791 - ], - [ - 0.00161, - 0.00906, - -0.03076 - ], - [ - 0.00874, - 0.00816, - 0.00108 - ], - [ - 0.01385, - 0.03095, - -0.02832 - ], - [ - 0.00455, - 0.01593, - -0.01755 - ], - [ - -0.00295, - -7e-05, - -0.02029 - ], - [ - 0.00259, - 0.02129, - -0.03849 - ], - [ - 0.00314, - 0.00665, - -0.01192 - ], - [ - -0.00018, - 0.00311, - -0.00208 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_07": 12.95251969089636 - }, - { - "J_Sec_Hair1_07": 12.95251969089636 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_08", - "J_Sec_Hair2_08", - "J_Sec_Hair3_08" - ], - "tips": [ - [ - -0.01414, - 0.06968, - -0.00406 - ], - [ - -0.04212, - 0.05622, - 0.0006 - ], - [ - -0.04212, - 0.05622, - 0.0006 - ] - ], - "hulls": [ - [ - [ - 0.01293, - 0.07861, - -0.02833 - ], - [ - -0.01331, - 0.00442, - 0.01001 - ], - [ - 0.02195, - 0.02537, - -0.02522 - ], - [ - -0.02324, - 0.05659, - 0.00872 - ], - [ - 0.00267, - 0.03296, - 0.00146 - ], - [ - 0.01345, - -0.00146, - -0.01281 - ], - [ - -0.00786, - 0.06491, - -0.01448 - ], - [ - 0.01031, - 0.04654, - -0.02964 - ], - [ - -0.02007, - 0.03646, - -0.0058 - ], - [ - -0.00207, - 0.01709, - -0.01415 - ] - ], - [ - [ - -0.04208, - 0.05627, - 0.00061 - ], - [ - 0.0224, - 0.01875, - -0.02406 - ], - [ - -0.01816, - 0.00616, - 0.014 - ], - [ - -0.00929, - 0.04589, - -0.02215 - ], - [ - -0.03867, - 0.03033, - 0.02021 - ], - [ - -0.00358, - 0.01706, - -0.00987 - ], - [ - -0.0157, - 0.03652, - 0.00136 - ], - [ - -0.01592, - 0.02578, - 0.02254 - ], - [ - 0.00962, - 0.03631, - -0.02138 - ], - [ - -0.0233, - 0.05486, - -0.01258 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_08": 22.67897996497351 - }, - { - "J_Sec_Hair1_08": 22.67897996497351 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_09", - "J_Sec_Hair2_09", - "J_Sec_Hair3_09", - "J_Sec_Hair4_09" - ], - "tips": [ - [ - 0.00656, - 0.0474, - 0.00209 - ], - [ - 0.01706, - 0.0446, - 0.00299 - ], - [ - 0.02854, - 0.03812, - 0.00482 - ], - [ - 0.02854, - 0.03812, - 0.00482 - ] - ], - "hulls": [ - [ - [ - 0.0041, - 0.04654, - -0.04275 - ], - [ - -0.00212, - 0.00195, - 0.02043 - ], - [ - -0.01037, - 0.00453, - -0.03435 - ], - [ - 0.01098, - 0.04256, - 0.01272 - ], - [ - 0.00989, - 0.01657, - -0.00669 - ], - [ - 0.00547, - 0.02398, - -0.03392 - ], - [ - 0.01109, - 0.02125, - 0.02163 - ], - [ - -7e-05, - 0.04024, - -0.02371 - ], - [ - -0.00138, - 0.03938, - -0.00175 - ], - [ - -0.0025, - 0.01464, - -0.04513 - ] - ], - [ - [ - 0.01155, - 0.04899, - -0.02492 - ], - [ - 0.01373, - 0.01337, - 0.01953 - ], - [ - 0.00042, - 0.00972, - -0.04317 - ], - [ - 0.00737, - 0.03226, - -0.00042 - ], - [ - 0.00988, - 0.00928, - -0.00588 - ], - [ - 0.01089, - 0.02663, - -0.03046 - ], - [ - 0.01694, - 0.03448, - 0.01905 - ], - [ - -0.00177, - 0.00778, - -0.02453 - ], - [ - 0.00883, - 0.03371, - -0.01582 - ], - [ - -0.00258, - 0.0207, - -0.0326 - ] - ], - [ - [ - 0.02849, - 0.0382, - 0.00456 - ], - [ - -0.00149, - 0.00458, - -0.03428 - ], - [ - 0.00607, - 0.00195, - 0.00791 - ], - [ - 0.01332, - 0.02759, - -0.01492 - ], - [ - 0.017, - 0.0198, - 0.00588 - ], - [ - 0.0021, - 0.00722, - -0.01197 - ], - [ - -0.00011, - 0.01283, - -0.02387 - ], - [ - 0.02521, - 0.02306, - 0.0148 - ], - [ - 0.00731, - 0.01733, - -0.01594 - ], - [ - 0.01358, - 0.00607, - 0.01594 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_09": 6.085030456593955 - }, - { - "J_Sec_Hair1_09": 6.085030456593955, - "J_Sec_Hair3_09": 6.217334650162728 - }, - { - "J_Sec_Hair2_09": 6.217334650162728 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_10", - "J_Sec_Hair2_10", - "J_Sec_Hair3_10", - "J_Sec_Hair4_10" - ], - "tips": [ - [ - -0.00523, - 0.04681, - 0.00168 - ], - [ - -0.01541, - 0.04425, - 0.00372 - ], - [ - -0.02703, - 0.03779, - 0.00697 - ], - [ - -0.02703, - 0.03779, - 0.00697 - ] - ], - "hulls": [ - [ - [ - 0.00254, - 0.03996, - -0.03885 - ], - [ - -0.0085, - 0.00607, - 0.02523 - ], - [ - 0.01355, - -0.00332, - -0.03069 - ], - [ - 0.00262, - 0.04386, - 0.00354 - ], - [ - 0.00857, - 0.00125, - 0.00306 - ], - [ - -0.00882, - 0.02119, - -0.00174 - ], - [ - -0.00108, - 0.01702, - -0.03077 - ], - [ - -0.00035, - 0.02781, - 0.02349 - ], - [ - 0.00106, - 0.03774, - -0.01836 - ], - [ - -0.00997, - 0.04078, - 0.02213 - ] - ], - [ - [ - -0.02137, - 0.03805, - 0.02335 - ], - [ - 0.00232, - 0.01406, - -0.03706 - ], - [ - -0.00047, - 0.00152, - 0.02164 - ], - [ - -0.00768, - 0.03901, - -0.01181 - ], - [ - -0.00924, - 0.01479, - -0.00068 - ], - [ - -0.00484, - 0.03418, - -0.03234 - ], - [ - -0.00787, - 0.02115, - 0.02217 - ], - [ - 0.00087, - 0.01924, - -0.01671 - ], - [ - -0.00652, - 0.03796, - 0.0041 - ], - [ - 0.00204, - 0.01813, - 0.00269 - ] - ], - [ - [ - -0.03056, - 0.04392, - 0.00527 - ], - [ - 0.0016, - 0.0091, - -0.02955 - ], - [ - -0.00748, - 0.00801, - 0.01125 - ], - [ - -0.00885, - 0.02284, - -0.01325 - ], - [ - -0.01882, - 0.02575, - 0.00806 - ], - [ - -0.02055, - 0.04066, - -0.00934 - ], - [ - -0.02723, - 0.02881, - 0.0177 - ], - [ - 0.00206, - 0.00531, - -0.01729 - ], - [ - -0.00303, - 0.01298, - -0.00933 - ], - [ - -0.01558, - 0.01188, - 0.01886 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_10": 4.348037023628208 - }, - { - "J_Sec_Hair1_10": 4.348037023628208, - "J_Sec_Hair3_10": 4.307469985860632 - }, - { - "J_Sec_Hair2_10": 4.307469985860632 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_11", - "J_Sec_Hair2_11", - "J_Sec_Hair3_11", - "J_Sec_Hair4_11" - ], - "tips": [ - [ - 0.01001, - 0.05484, - -0.0104 - ], - [ - 0.00594, - 0.05596, - -0.00663 - ], - [ - 0.00284, - 0.05652, - -0.00375 - ], - [ - 0.00284, - 0.05652, - -0.00375 - ] - ], - "hulls": [ - [ - [ - -0.0336, - 0.05073, - -0.02585 - ], - [ - 0.03431, - 0.00309, - 0.02653 - ], - [ - 0.03101, - 0.04289, - -0.01762 - ], - [ - -0.00462, - -0.0013, - -0.01728 - ], - [ - -0.05014, - 0.00712, - -0.03009 - ], - [ - 0.00588, - 0.02005, - 0.0094 - ], - [ - -0.00129, - 0.04205, - -0.0166 - ], - [ - 0.03881, - 0.03532, - 0.01253 - ], - [ - 0.02742, - 0.00285, - -0.00672 - ], - [ - -0.02026, - 0.01388, - -0.03785 - ] - ], - [ - [ - 0.01152, - 0.05436, - -0.0143 - ], - [ - -0.03654, - 0.00851, - -0.01442 - ], - [ - 0.02489, - 0.00155, - 0.01241 - ], - [ - -0.00134, - 0.01164, - -0.01049 - ], - [ - -0.01899, - 0.03823, - -0.01455 - ], - [ - 0.02563, - 0.03094, - 0.0062 - ], - [ - -0.00124, - 0.04081, - 0.0012 - ], - [ - 0.02009, - 0.02067, - -0.0136 - ], - [ - -0.01384, - -0.00132, - -0.02476 - ], - [ - 0.0111, - 0.01937, - 0.01747 - ] - ], - [ - [ - 0.01809, - 0.06474, - 0.00451 - ], - [ - -0.00252, - -0.00034, - -0.00919 - ], - [ - -0.01443, - 0.04127, - -0.00749 - ], - [ - 0.02423, - 0.02907, - 0.00268 - ], - [ - 0.01752, - 0.00581, - 0.0013 - ], - [ - -0.01333, - 0.01982, - -0.00015 - ], - [ - 0.01209, - 0.04863, - -0.00886 - ], - [ - 0.00612, - 0.03733, - 0.00917 - ], - [ - -1e-05, - 0.02768, - -0.01425 - ], - [ - -0.01285, - 0.06118, - -0.00357 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_11": 20.1695800288937 - }, - { - "J_Sec_Hair1_11": 20.1695800288937, - "J_Sec_Hair3_11": 18.79479094472742 - }, - { - "J_Sec_Hair2_11": 18.79479094472742 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_12", - "J_Sec_Hair2_12", - "J_Sec_Hair3_12", - "J_Sec_Hair4_12" - ], - "tips": [ - [ - -0.0098, - 0.05611, - -0.01118 - ], - [ - -0.00567, - 0.05729, - -0.00697 - ], - [ - -0.0024, - 0.05789, - -0.00363 - ], - [ - -0.0024, - 0.05789, - -0.00363 - ] - ], - "hulls": [ - [ - [ - 0.035, - 0.05517, - -0.02733 - ], - [ - -0.03495, - 0.00451, - 0.02597 - ], - [ - 0.00651, - 0.00181, - -0.01847 - ], - [ - -0.03053, - 0.04456, - -0.01868 - ], - [ - 0.05194, - 0.01062, - -0.031 - ], - [ - -0.00549, - 0.02112, - 0.00883 - ], - [ - -0.02727, - 0.00374, - -0.00708 - ], - [ - -0.03845, - 0.03769, - 0.01136 - ], - [ - 0.00835, - 0.03947, - -0.0201 - ], - [ - -0.01009, - 0.05696, - 0.00016 - ] - ], - [ - [ - -0.01724, - 0.05565, - -0.0071 - ], - [ - 0.03655, - 0.01314, - -0.01521 - ], - [ - -0.02476, - 0.00201, - 0.01213 - ], - [ - 0.00092, - 0.01441, - -0.01105 - ], - [ - 0.01819, - 0.04209, - -0.01466 - ], - [ - -0.00506, - 0.0267, - 0.01481 - ], - [ - 0.0236, - 0.00453, - 0.00578 - ], - [ - -0.02433, - 0.01446, - -0.00795 - ], - [ - 0.01407, - 0.00225, - -0.02557 - ], - [ - 0.00066, - 0.04337, - 0.00041 - ] - ], - [ - [ - -0.00061, - 0.06864, - -0.00452 - ], - [ - -0.00632, - -0.00079, - -0.00813 - ], - [ - 0.00826, - 0.03388, - -0.01943 - ], - [ - -0.02358, - 0.03881, - 0.00234 - ], - [ - 0.0134, - 0.01709, - -0.00064 - ], - [ - 0.0104, - 0.04449, - 0.00057 - ], - [ - -0.00671, - 0.01917, - 0.00805 - ], - [ - -0.01297, - 0.01892, - -0.01268 - ], - [ - -0.01932, - 0.06422, - 0.00449 - ], - [ - -0.01306, - 0.04876, - -0.00903 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_12": 20.797115504793354 - }, - { - "J_Sec_Hair1_12": 20.797115504793354, - "J_Sec_Hair3_12": 20.753898724733332 - }, - { - "J_Sec_Hair2_12": 20.753898724733332 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_13", - "J_Sec_Hair2_13", - "J_Sec_Hair3_13" - ], - "tips": [ - [ - 0.02011, - 0.09181, - -0.00286 - ], - [ - 0.03975, - 0.05486, - -0.00267 - ], - [ - 0.03975, - 0.05486, - -0.00267 - ] - ], - "hulls": [ - [ - [ - 0.01278, - 0.09433, - 0.00743 - ], - [ - 0.00503, - -0.00394, - -0.02307 - ], - [ - 0.03257, - 0.04406, - -0.00514 - ], - [ - 0.01058, - 0.01845, - 0.00721 - ], - [ - 0.01238, - 0.07186, - -0.0156 - ], - [ - 0.01675, - 0.02279, - -0.02178 - ], - [ - 0.02799, - 0.0669, - 0.0055 - ], - [ - 0.0045, - -0.00404, - 0.00515 - ], - [ - 0.0101, - 0.04025, - -0.00087 - ], - [ - 0.03093, - 0.08206, - -0.01168 - ] - ], - [ - [ - 0.03347, - 0.06049, - -0.00477 - ], - [ - -0.01279, - 0.00778, - -0.00465 - ], - [ - -0.00957, - 0.05038, - 0.01515 - ], - [ - 0.02852, - 0.02198, - -0.0276 - ], - [ - -0.00084, - 0.03441, - -0.00966 - ], - [ - -0.00561, - 0.02163, - 0.01707 - ], - [ - 0.00605, - 0.05881, - -0.00897 - ], - [ - 0.00839, - -0.00286, - -0.00961 - ], - [ - 0.00835, - 0.06527, - 0.00993 - ], - [ - 0.01485, - 0.03519, - -0.02093 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_13": 28.349896885998273 - }, - { - "J_Sec_Hair1_13": 28.349896885998273 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_14", - "J_Sec_Hair2_14", - "J_Sec_Hair3_14" - ], - "tips": [ - [ - -0.02011, - 0.09181, - -0.00286 - ], - [ - -0.03975, - 0.05486, - -0.00267 - ], - [ - -0.03975, - 0.05486, - -0.00267 - ] - ], - "hulls": [ - [ - [ - -0.01256, - 0.0943, - 0.00048 - ], - [ - -0.00487, - -0.00404, - -0.02413 - ], - [ - -0.02955, - 0.03699, - 0.00229 - ], - [ - -0.01589, - 0.06506, - -0.01716 - ], - [ - -0.00892, - 0.0113, - 0.00738 - ], - [ - -0.01583, - 0.02999, - -0.0233 - ], - [ - -0.03173, - 0.067, - 0.00648 - ], - [ - -0.01225, - 0.0508, - 0.00085 - ], - [ - -0.02915, - 0.08183, - -0.00816 - ], - [ - -0.00797, - 0.03032, - 0.00049 - ] - ], - [ - [ - -0.02436, - 0.06451, - -0.00364 - ], - [ - -0.00762, - -0.00295, - -0.00432 - ], - [ - 0.01095, - 0.03578, - 0.01561 - ], - [ - -0.02764, - 0.02899, - -0.02757 - ], - [ - 0.00397, - 0.0245, - -0.01162 - ], - [ - 0.00189, - 0.06375, - 0.01059 - ], - [ - -0.004, - 0.05114, - -0.00914 - ], - [ - 0.0006, - 0.01537, - 0.0133 - ], - [ - 0.0135, - 0.00781, - -0.00586 - ], - [ - -0.03979, - 0.05491, - -0.00254 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_14": 28.360807644433685 - }, - { - "J_Sec_Hair1_14": 28.360807644433685 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_15", - "J_Sec_Hair2_15", - "J_Sec_Hair3_15" - ], - "tips": [ - [ - -0.01211, - 0.05321, - 0.01422 - ], - [ - -0.01309, - 0.05293, - 0.01456 - ], - [ - -0.01309, - 0.05293, - 0.01456 - ] - ], - "hulls": [ - [ - [ - -0.0267, - 0.05046, - -0.00606 - ], - [ - -0.00062, - 0.00414, - -0.00789 - ], - [ - -0.00667, - 0.03155, - 0.00125 - ], - [ - -0.01534, - 0.01553, - -0.00251 - ], - [ - -0.013, - 0.04884, - 0.00424 - ], - [ - -0.02028, - 0.03313, - -0.00813 - ], - [ - -0.01922, - 0.03774, - 0.00385 - ], - [ - -0.00317, - 0.01852, - -0.00357 - ], - [ - -0.02513, - 0.0499, - 0.0063 - ], - [ - -0.01121, - 0.02662, - -0.00923 - ] - ], - [ - [ - -0.03068, - 0.05762, - 0.02615 - ], - [ - -0.0165, - 0.0015, - -0.01958 - ], - [ - 0.00662, - 0.03993, - -0.00269 - ], - [ - -0.03151, - 0.03456, - -0.00473 - ], - [ - -0.00954, - 0.05841, - 0.00746 - ], - [ - -0.02504, - 0.01517, - -0.00152 - ], - [ - -0.02627, - 0.04302, - 0.01308 - ], - [ - -0.02356, - 0.04756, - -0.00138 - ], - [ - -0.01502, - 0.05269, - 0.02041 - ], - [ - -0.02537, - 0.02337, - -0.01417 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_15": 26.225395679503343 - }, - { - "J_Sec_Hair1_15": 26.225395679503343 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_16", - "J_Sec_Hair2_16", - "J_Sec_Hair3_16" - ], - "tips": [ - [ - 0.01211, - 0.05321, - 0.01422 - ], - [ - 0.01309, - 0.05293, - 0.01456 - ], - [ - 0.01309, - 0.05293, - 0.01456 - ] - ], - "hulls": [ - [ - [ - 0.02113, - 0.05354, - -0.00554 - ], - [ - 0.00076, - 0.00423, - -0.00969 - ], - [ - 0.01547, - 0.02449, - 0.00187 - ], - [ - 0.00814, - 0.04121, - 0.00081 - ], - [ - 0.01089, - 0.01686, - -0.0105 - ], - [ - 0.01191, - 0.00639, - -0.0002 - ], - [ - 0.02305, - 0.04065, - 0.00039 - ], - [ - 0.00967, - 0.0323, - -0.00837 - ], - [ - 0.00387, - 0.02312, - -0.00284 - ], - [ - 0.01576, - 0.0526, - 0.00473 - ] - ], - [ - [ - 0.03068, - 0.05762, - 0.02615 - ], - [ - 0.01647, - 0.0008, - -0.01661 - ], - [ - -0.00663, - 0.03986, - -0.00258 - ], - [ - 0.03067, - 0.03647, - -0.00831 - ], - [ - 0.00894, - 0.0606, - 0.01156 - ], - [ - 0.02504, - 0.01517, - -0.00152 - ], - [ - 0.03677, - 0.04015, - 0.01336 - ], - [ - 0.02325, - 0.05038, - 0.00229 - ], - [ - 0.00499, - 0.04553, - 0.01218 - ], - [ - 0.02024, - 0.01552, - -0.01657 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_16": 26.322509970647936 - }, - { - "J_Sec_Hair1_16": 26.322509970647936 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_17", - "J_Sec_Hair2_17", - "J_Sec_Hair3_17" - ], - "tips": [ - [ - 0.02348, - 0.0957, - -0.02381 - ], - [ - -0.00217, - 0.10077, - -0.01388 - ], - [ - -0.00217, - 0.10077, - -0.01388 - ] - ], - "hulls": [ - [ - [ - 0.05658, - 0.08838, - -0.03346 - ], - [ - 0.00273, - -0.00065, - -5e-05 - ], - [ - 0.00587, - 0.05197, - -0.03052 - ], - [ - 0.02929, - 0.03537, - 0.00239 - ], - [ - 0.01178, - 0.09133, - -0.04513 - ], - [ - 0.02602, - 0.08072, - -0.00753 - ], - [ - 0.04559, - 0.05799, - -0.02396 - ], - [ - 0.01376, - 0.02351, - -0.02037 - ], - [ - 0.02303, - 0.07156, - -0.03101 - ], - [ - 0.03541, - 0.0901, - -0.04576 - ] - ], - [ - [ - -0.00135, - 0.11116, - -0.01126 - ], - [ - 0.02807, - 0.00059, - 0.01304 - ], - [ - -0.00773, - 0.04692, - -0.03614 - ], - [ - 0.03907, - 0.07185, - -0.02125 - ], - [ - 0.01188, - 0.04645, - 0.01195 - ], - [ - -0.01439, - 0.00065, - -0.00596 - ], - [ - 0.01794, - 0.01334, - -0.02856 - ], - [ - -0.02031, - 0.08883, - -0.02963 - ], - [ - 0.04174, - 0.03605, - -0.01935 - ], - [ - 0.00605, - 0.07751, - 0.00105 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_17": 20.076729074867217 - }, - { - "J_Sec_Hair1_17": 20.076729074867217 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "J_Bip_C_Head", - "bones": [ - "J_Sec_Hair1_18", - "J_Sec_Hair2_18", - "J_Sec_Hair3_18" - ], - "tips": [ - [ - -0.02373, - 0.0961, - -0.02398 - ], - [ - 0.00214, - 0.10123, - -0.01396 - ], - [ - 0.00214, - 0.10123, - -0.01396 - ] - ], - "hulls": [ - [ - [ - -0.05372, - 0.0887, - -0.03821 - ], - [ - -0.00155, - 0.0015, - 0.0044 - ], - [ - -0.0045, - 0.05349, - -0.02979 - ], - [ - -0.04112, - 0.0547, - -0.00352 - ], - [ - -0.00889, - 0.09313, - -0.04332 - ], - [ - -0.02936, - 0.09161, - -0.00708 - ], - [ - -0.02751, - 0.02093, - -0.01345 - ], - [ - 0.00019, - 0.02767, - -0.00731 - ], - [ - -0.02593, - 0.07379, - -0.03643 - ], - [ - -0.05391, - 0.08442, - -0.01154 - ] - ], - [ - [ - 0.00123, - 0.11161, - -0.01115 - ], - [ - -0.03379, - -0.00147, - 0.00928 - ], - [ - 0.01417, - 0.03707, - -0.03033 - ], - [ - -0.04251, - 0.06227, - -0.012 - ], - [ - -0.00944, - 0.03602, - 0.01124 - ], - [ - 0.01272, - 0.00165, - -0.00377 - ], - [ - 0.01934, - 0.07901, - -0.0333 - ], - [ - -0.03593, - 0.02393, - -0.02433 - ], - [ - -0.00213, - 0.07633, - 0.00866 - ], - [ - -0.03081, - 0.09597, - -0.01973 - ] - ], - [] - ], - "neighbours": [ - { - "J_Sec_Hair2_18": 20.05058973935537 - }, - { - "J_Sec_Hair1_18": 20.05058973935537 - }, - {} - ] - } - ], - "twist": [], - "colliders": [ - { - "bone": "J_Bip_C_Hips", - "child": "J_Bip_C_Spine", - "from": 0.0, - "radius_head": 0.1447, - "radius_tail": 0.1447, - "radius": 0.1447, - "lid": true - }, - { - "bone": "J_Bip_L_UpperLeg", - "child": "J_Bip_L_LowerLeg", - "from": 0.1, - "radius_head": 0.0974, - "radius_tail": 0.0554, - "radius": 0.0554 - }, - { - "bone": "J_Bip_R_UpperLeg", - "child": "J_Bip_R_LowerLeg", - "from": 0.1, - "radius_head": 0.0974, - "radius_tail": 0.0554, - "radius": 0.0554 - }, - { - "bone": "J_Bip_L_LowerLeg", - "child": "J_Bip_L_Foot", - "from": 0.1, - "radius_head": 0.0922, - "radius_tail": 0.0493, - "radius": 0.0493 - }, - { - "bone": "J_Bip_R_LowerLeg", - "child": "J_Bip_R_Foot", - "from": 0.1, - "radius_head": 0.0922, - "radius_tail": 0.0493, - "radius": 0.0493 - } - ], - "weights_authored": true, - "driven_bones": [ - "J_Adj_L_FaceEye", - "J_Bip_C_Chest", - "J_Bip_C_Head", - "J_Bip_C_Hips", - "J_Bip_C_Neck", - "J_Bip_C_Spine", - "J_Bip_C_UpperChest", - "J_Bip_L_Foot", - "J_Bip_L_Hand", - "J_Bip_L_LowerArm", - "J_Bip_L_LowerLeg", - "J_Bip_L_Shoulder", - "J_Bip_L_ToeBase", - "J_Bip_L_UpperArm", - "J_Bip_L_UpperLeg", - "J_Bip_R_Foot", - "J_Bip_R_Hand", - "J_Bip_R_LowerArm", - "J_Bip_R_LowerLeg", - "J_Bip_R_Shoulder", - "J_Bip_R_ToeBase", - "J_Bip_R_UpperArm", - "J_Bip_R_UpperLeg", - "Root" - ], - "surfaces": [ - { - "mesh": "Object_10", - "surface": 0, - "material": "N00_001_03_Bottoms_01_CLOTH_Instance", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 742, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_11", - "surface": 0, - "material": "N00_001_01_Shoes_01_CLOTH_Instance", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 410, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_13", - "surface": 0, - "material": "N00_000_00_FaceMouth_00_FACE_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 1073, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_14", - "surface": 0, - "material": "N00_000_00_EyeIris_00_EYE_Instance", - "class": "body", - "detail": "eyes", - "why": "material name says 'eyes'", - "verts": 268, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_15", - "surface": 0, - "material": "N00_000_00_EyeHighlight_00_EYE_Instance", - "class": "body", - "detail": "eyes", - "why": "material name says 'eyes'", - "verts": 200, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_16", - "surface": 0, - "material": "N00_000_00_Face_00_SKIN_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 2266, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_17", - "surface": 0, - "material": "N00_000_00_EyeWhite_00_EYE_Instance", - "class": "body", - "detail": "eyes", - "why": "material name says 'eyes'", - "verts": 290, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_18", - "surface": 0, - "material": "N00_000_00_FaceBrow_00_FACE_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 52, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_19", - "surface": 0, - "material": "N00_000_00_FaceEyeline_00_FACE_Instance", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 168, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_21", - "surface": 0, - "material": "N00_000_Hair_00_HAIR_Instance", - "class": "hair", - "detail": "hair", - "why": "material name says 'hair'", - "verts": 22432, - "textured": true, - "chain_share": { - "hair": 0.281 - } - }, - { - "mesh": "Object_7", - "surface": 0, - "material": "N00_000_00_Body_00_SKIN_Instance", - "class": "body", - "detail": "skin", - "why": "material name says 'skin'", - "verts": 5103, - "textured": true, - "chain_share": { - "bust": 0.001 - } - }, - { - "mesh": "Object_8", - "surface": 0, - "material": "N00_000_00_HairBack_00_HAIR_Instance", - "class": "hair", - "detail": "hair", - "why": "material name says 'hair'", - "verts": 700, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_9", - "surface": 0, - "material": "N00_007_01_Tops_01_CLOTH_Instance", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 814, - "textured": true, - "chain_share": { - "bust": 0.061 - } - } - ] } \ No newline at end of file diff --git a/assets/characters/skins/mannequin.glb b/assets/characters/skins/mannequin.glb index c6e997a..5f49d2e 100644 Binary files a/assets/characters/skins/mannequin.glb and b/assets/characters/skins/mannequin.glb differ diff --git a/assets/characters/skins/mannequin.rig.json b/assets/characters/skins/mannequin.rig.json index da5493e..cb04d71 100644 --- a/assets/characters/skins/mannequin.rig.json +++ b/assets/characters/skins/mannequin.rig.json @@ -1,207 +1,207 @@ { - "roles": { - "hips": "DEF-hips", - "head": "DEF-head", - "neck": "DEF-neck", - "spine": [ - "DEF-spine.001", - "DEF-spine.002", - "DEF-spine.003", - "DEF-neck", - "DEF-head" + "roles": { + "hips": "DEF-hips", + "head": "DEF-head", + "neck": "DEF-neck", + "spine": [ + "DEF-spine.001", + "DEF-spine.002", + "DEF-spine.003", + "DEF-neck", + "DEF-head" + ], + "shoulder.L": "DEF-shoulder.L", + "shoulder.R": "DEF-shoulder.R", + "upper_arm.L": "DEF-upper_arm.L", + "upper_arm.R": "DEF-upper_arm.R", + "forearm.R": "DEF-forearm.R", + "forearm.L": "DEF-forearm.L", + "thigh.L": "DEF-thigh.L", + "thigh.R": "DEF-thigh.R", + "foot.R": "DEF-foot.R", + "foot.L": "DEF-foot.L", + "shin.L": "DEF-shin.L", + "shin.R": "DEF-shin.R", + "hand.R": "DEF-hand.R", + "hand.L": "DEF-hand.L", + "toe.L": "DEF-toe.L", + "toe.R": "DEF-toe.R" + }, + "fingers": { + "thumb.L": [ + "DEF-thumb.01.L", + "DEF-thumb.02.L", + "DEF-thumb.03.L" + ], + "index.L": [ + "DEF-f_index.01.L", + "DEF-f_index.02.L", + "DEF-f_index.03.L" + ], + "middle.L": [ + "DEF-f_middle.01.L", + "DEF-f_middle.02.L", + "DEF-f_middle.03.L" + ], + "ring.L": [ + "DEF-f_ring.01.L", + "DEF-f_ring.02.L", + "DEF-f_ring.03.L" + ], + "pinky.L": [ + "DEF-f_pinky.01.L", + "DEF-f_pinky.02.L", + "DEF-f_pinky.03.L" + ], + "thumb.R": [ + "DEF-thumb.01.R", + "DEF-thumb.02.R", + "DEF-thumb.03.R" + ], + "index.R": [ + "DEF-f_index.01.R", + "DEF-f_index.02.R", + "DEF-f_index.03.R" + ], + "middle.R": [ + "DEF-f_middle.01.R", + "DEF-f_middle.02.R", + "DEF-f_middle.03.R" + ], + "ring.R": [ + "DEF-f_ring.01.R", + "DEF-f_ring.02.R", + "DEF-f_ring.03.R" + ], + "pinky.R": [ + "DEF-f_pinky.01.R", + "DEF-f_pinky.02.R", + "DEF-f_pinky.03.R" + ] + }, + "chains": [], + "twist": [], + "surfaces": [ + { + "mesh": "Mannequin", + "surface": 0, + "material": "M_Main", + "class": "body", + "detail": "skin", + "why": "no name or weight evidence \u2014 treated as body", + "verts": 3391, + "textured": false, + "chain_share": {} + }, + { + "mesh": "Mannequin", + "surface": 1, + "material": "M_Joints", + "class": "body", + "detail": "skin", + "why": "no name or weight evidence \u2014 treated as body", + "verts": 5157, + "textured": false, + "chain_share": {} + } ], - "shoulder.L": "DEF-shoulder.L", - "shoulder.R": "DEF-shoulder.R", - "upper_arm.R": "DEF-upper_arm.R", - "upper_arm.L": "DEF-upper_arm.L", - "forearm.L": "DEF-forearm.L", - "forearm.R": "DEF-forearm.R", - "thigh.L": "DEF-thigh.L", - "thigh.R": "DEF-thigh.R", - "foot.R": "DEF-foot.R", - "foot.L": "DEF-foot.L", - "shin.R": "DEF-shin.R", - "shin.L": "DEF-shin.L", - "hand.R": "DEF-hand.R", - "hand.L": "DEF-hand.L", - "toe.R": "DEF-toe.R", - "toe.L": "DEF-toe.L" - }, - "fingers": { - "thumb.L": [ - "DEF-thumb.01.L", - "DEF-thumb.02.L", - "DEF-thumb.03.L" + "colliders": [ + { + "bone": "DEF-hips", + "child": "DEF-spine.001", + "from": 0.0, + "radius_head": 0.1381, + "radius_tail": 0.1381, + "radius": 0.1381, + "lid": true + }, + { + "bone": "DEF-thigh.L", + "child": "DEF-shin.L", + "from": 0.1, + "radius_head": 0.1037, + "radius_tail": 0.0733, + "radius": 0.0733 + }, + { + "bone": "DEF-thigh.R", + "child": "DEF-shin.R", + "from": 0.1, + "radius_head": 0.1037, + "radius_tail": 0.0733, + "radius": 0.0733 + }, + { + "bone": "DEF-shin.L", + "child": "DEF-foot.L", + "from": 0.1, + "radius_head": 0.0927, + "radius_tail": 0.0516, + "radius": 0.0516 + }, + { + "bone": "DEF-shin.R", + "child": "DEF-foot.R", + "from": 0.1, + "radius_head": 0.0927, + "radius_tail": 0.0516, + "radius": 0.0516 + } ], - "index.L": [ - "DEF-f_index.01.L", - "DEF-f_index.02.L", - "DEF-f_index.03.L" - ], - "middle.L": [ - "DEF-f_middle.01.L", - "DEF-f_middle.02.L", - "DEF-f_middle.03.L" - ], - "ring.L": [ - "DEF-f_ring.01.L", - "DEF-f_ring.02.L", - "DEF-f_ring.03.L" - ], - "pinky.L": [ - "DEF-f_pinky.01.L", - "DEF-f_pinky.02.L", - "DEF-f_pinky.03.L" - ], - "thumb.R": [ - "DEF-thumb.01.R", - "DEF-thumb.02.R", - "DEF-thumb.03.R" - ], - "index.R": [ - "DEF-f_index.01.R", - "DEF-f_index.02.R", - "DEF-f_index.03.R" - ], - "middle.R": [ - "DEF-f_middle.01.R", - "DEF-f_middle.02.R", - "DEF-f_middle.03.R" - ], - "ring.R": [ - "DEF-f_ring.01.R", - "DEF-f_ring.02.R", - "DEF-f_ring.03.R" - ], - "pinky.R": [ - "DEF-f_pinky.01.R", - "DEF-f_pinky.02.R", - "DEF-f_pinky.03.R" + "weights_authored": true, + "driven_bones": [ + "DEF-f_index.01.L", + "DEF-f_index.01.R", + "DEF-f_index.02.L", + "DEF-f_index.02.R", + "DEF-f_index.03.L", + "DEF-f_index.03.R", + "DEF-f_middle.01.L", + "DEF-f_middle.01.R", + "DEF-f_middle.02.L", + "DEF-f_middle.02.R", + "DEF-f_middle.03.L", + "DEF-f_middle.03.R", + "DEF-f_pinky.01.L", + "DEF-f_pinky.01.R", + "DEF-f_pinky.02.L", + "DEF-f_pinky.02.R", + "DEF-f_pinky.03.L", + "DEF-f_pinky.03.R", + "DEF-f_ring.01.L", + "DEF-f_ring.01.R", + "DEF-f_ring.02.L", + "DEF-f_ring.02.R", + "DEF-f_ring.03.L", + "DEF-f_ring.03.R", + "DEF-foot.L", + "DEF-foot.R", + "DEF-forearm.L", + "DEF-forearm.R", + "DEF-hand.L", + "DEF-hand.R", + "DEF-head", + "DEF-hips", + "DEF-neck", + "DEF-shin.L", + "DEF-shin.R", + "DEF-shoulder.L", + "DEF-shoulder.R", + "DEF-spine.001", + "DEF-spine.002", + "DEF-spine.003", + "DEF-thigh.L", + "DEF-thigh.R", + "DEF-thumb.01.L", + "DEF-thumb.01.R", + "DEF-thumb.02.L", + "DEF-thumb.02.R", + "DEF-thumb.03.L", + "DEF-thumb.03.R", + "DEF-toe.L", + "DEF-toe.R", + "DEF-upper_arm.L", + "DEF-upper_arm.R", + "root" ] - }, - "chains": [], - "twist": [], - "colliders": [ - { - "bone": "DEF-hips", - "child": "DEF-spine.001", - "from": 0.0, - "radius_head": 0.1381, - "radius_tail": 0.1381, - "radius": 0.1381, - "lid": true - }, - { - "bone": "DEF-thigh.L", - "child": "DEF-shin.L", - "from": 0.1, - "radius_head": 0.1037, - "radius_tail": 0.0733, - "radius": 0.0733 - }, - { - "bone": "DEF-thigh.R", - "child": "DEF-shin.R", - "from": 0.1, - "radius_head": 0.1037, - "radius_tail": 0.0733, - "radius": 0.0733 - }, - { - "bone": "DEF-shin.L", - "child": "DEF-foot.L", - "from": 0.1, - "radius_head": 0.0927, - "radius_tail": 0.0516, - "radius": 0.0516 - }, - { - "bone": "DEF-shin.R", - "child": "DEF-foot.R", - "from": 0.1, - "radius_head": 0.0927, - "radius_tail": 0.0516, - "radius": 0.0516 - } - ], - "weights_authored": true, - "driven_bones": [ - "DEF-f_index.01.L", - "DEF-f_index.01.R", - "DEF-f_index.02.L", - "DEF-f_index.02.R", - "DEF-f_index.03.L", - "DEF-f_index.03.R", - "DEF-f_middle.01.L", - "DEF-f_middle.01.R", - "DEF-f_middle.02.L", - "DEF-f_middle.02.R", - "DEF-f_middle.03.L", - "DEF-f_middle.03.R", - "DEF-f_pinky.01.L", - "DEF-f_pinky.01.R", - "DEF-f_pinky.02.L", - "DEF-f_pinky.02.R", - "DEF-f_pinky.03.L", - "DEF-f_pinky.03.R", - "DEF-f_ring.01.L", - "DEF-f_ring.01.R", - "DEF-f_ring.02.L", - "DEF-f_ring.02.R", - "DEF-f_ring.03.L", - "DEF-f_ring.03.R", - "DEF-foot.L", - "DEF-foot.R", - "DEF-forearm.L", - "DEF-forearm.R", - "DEF-hand.L", - "DEF-hand.R", - "DEF-head", - "DEF-hips", - "DEF-neck", - "DEF-shin.L", - "DEF-shin.R", - "DEF-shoulder.L", - "DEF-shoulder.R", - "DEF-spine.001", - "DEF-spine.002", - "DEF-spine.003", - "DEF-thigh.L", - "DEF-thigh.R", - "DEF-thumb.01.L", - "DEF-thumb.01.R", - "DEF-thumb.02.L", - "DEF-thumb.02.R", - "DEF-thumb.03.L", - "DEF-thumb.03.R", - "DEF-toe.L", - "DEF-toe.R", - "DEF-upper_arm.L", - "DEF-upper_arm.R", - "root" - ], - "surfaces": [ - { - "mesh": "Mannequin", - "surface": 0, - "material": "M_Main", - "class": "body", - "detail": "skin", - "why": "no name or weight evidence \u2014 treated as body", - "verts": 3391, - "textured": false, - "chain_share": {} - }, - { - "mesh": "Mannequin", - "surface": 1, - "material": "M_Joints", - "class": "body", - "detail": "skin", - "why": "no name or weight evidence \u2014 treated as body", - "verts": 5157, - "textured": false, - "chain_share": {} - } - ] } \ No newline at end of file diff --git a/assets/characters/skins/miku.glb b/assets/characters/skins/miku.glb index 8523280..8b263d6 100644 Binary files a/assets/characters/skins/miku.glb and b/assets/characters/skins/miku.glb differ diff --git a/assets/characters/skins/miku.rig.json b/assets/characters/skins/miku.rig.json index c8f9d15..b7e952d 100644 --- a/assets/characters/skins/miku.rig.json +++ b/assets/characters/skins/miku.rig.json @@ -1,229 +1,229 @@ { - "roles": { - "hips": "DEF-hips", - "head": "DEF-head", - "neck": "DEF-neck", - "spine": [ - "DEF-spine.001", - "DEF-spine.002", - "DEF-spine.003", - "DEF-neck", - "DEF-head" + "roles": { + "hips": "DEF-hips", + "head": "DEF-head", + "neck": "DEF-neck", + "spine": [ + "DEF-spine.001", + "DEF-spine.002", + "DEF-spine.003", + "DEF-neck", + "DEF-head" + ], + "shoulder.L": "DEF-shoulder.L", + "shoulder.R": "DEF-shoulder.R", + "upper_arm.L": "DEF-upper_arm.L", + "upper_arm.R": "DEF-upper_arm.R", + "forearm.L": "DEF-forearm.L", + "forearm.R": "DEF-forearm.R", + "thigh.L": "DEF-thigh.L", + "thigh.R": "DEF-thigh.R", + "foot.R": "DEF-foot.R", + "foot.L": "DEF-foot.L", + "shin.R": "DEF-shin.R", + "shin.L": "DEF-shin.L", + "hand.L": "DEF-hand.L", + "hand.R": "DEF-hand.R", + "toe.R": "DEF-toe.R", + "toe.L": "DEF-toe.L" + }, + "fingers": { + "thumb.L": [ + "DEF-thumb.01.L", + "DEF-thumb.02.L", + "DEF-thumb.03.L" + ], + "index.L": [ + "DEF-f_index.01.L", + "DEF-f_index.02.L", + "DEF-f_index.03.L" + ], + "middle.L": [ + "DEF-f_middle.01.L", + "DEF-f_middle.02.L", + "DEF-f_middle.03.L" + ], + "ring.L": [ + "DEF-f_ring.01.L", + "DEF-f_ring.02.L", + "DEF-f_ring.03.L" + ], + "pinky.L": [ + "DEF-f_pinky.01.L", + "DEF-f_pinky.02.L", + "DEF-f_pinky.03.L" + ], + "thumb.R": [ + "DEF-thumb.01.R", + "DEF-thumb.02.R", + "DEF-thumb.03.R" + ], + "index.R": [ + "DEF-f_index.01.R", + "DEF-f_index.02.R", + "DEF-f_index.03.R" + ], + "middle.R": [ + "DEF-f_middle.01.R", + "DEF-f_middle.02.R", + "DEF-f_middle.03.R" + ], + "ring.R": [ + "DEF-f_ring.01.R", + "DEF-f_ring.02.R", + "DEF-f_ring.03.R" + ], + "pinky.R": [ + "DEF-f_pinky.01.R", + "DEF-f_pinky.02.R", + "DEF-f_pinky.03.R" + ] + }, + "chains": [], + "twist": [], + "surfaces": [ + { + "mesh": "Object_2", + "surface": 0, + "material": "body", + "class": "body", + "detail": "skin", + "why": "material name says 'skin'", + "verts": 1500, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_2", + "surface": 1, + "material": "body_parts", + "class": "body", + "detail": "skin", + "why": "material name says 'skin'", + "verts": 330, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_2", + "surface": 2, + "material": "hair", + "class": "hair", + "detail": "hair", + "why": "material name says 'hair'", + "verts": 500, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_2", + "surface": 3, + "material": "face", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 461, + "textured": true, + "chain_share": {} + } ], - "shoulder.L": "DEF-shoulder.L", - "shoulder.R": "DEF-shoulder.R", - "upper_arm.L": "DEF-upper_arm.L", - "upper_arm.R": "DEF-upper_arm.R", - "forearm.L": "DEF-forearm.L", - "forearm.R": "DEF-forearm.R", - "thigh.L": "DEF-thigh.L", - "thigh.R": "DEF-thigh.R", - "foot.R": "DEF-foot.R", - "foot.L": "DEF-foot.L", - "shin.R": "DEF-shin.R", - "shin.L": "DEF-shin.L", - "hand.R": "DEF-hand.R", - "hand.L": "DEF-hand.L", - "toe.R": "DEF-toe.R", - "toe.L": "DEF-toe.L" - }, - "fingers": { - "thumb.L": [ - "DEF-thumb.01.L", - "DEF-thumb.02.L", - "DEF-thumb.03.L" + "colliders": [ + { + "bone": "DEF-hips", + "child": "DEF-spine.001", + "from": 0.0, + "radius_head": 0.1915, + "radius_tail": 0.1915, + "radius": 0.1915, + "lid": true + }, + { + "bone": "DEF-thigh.L", + "child": "DEF-shin.L", + "from": 0.1, + "radius_head": 0.1435, + "radius_tail": 0.1304, + "radius": 0.1304 + }, + { + "bone": "DEF-thigh.R", + "child": "DEF-shin.R", + "from": 0.1, + "radius_head": 0.1084, + "radius_tail": 0.1084, + "radius": 0.1084 + }, + { + "bone": "DEF-shin.L", + "child": "DEF-foot.L", + "from": 0.1, + "radius_head": 0.0862, + "radius_tail": 0.0728, + "radius": 0.0728 + }, + { + "bone": "DEF-shin.R", + "child": "DEF-foot.R", + "from": 0.1, + "radius_head": 0.0862, + "radius_tail": 0.0728, + "radius": 0.0728 + } ], - "index.L": [ - "DEF-f_index.01.L", - "DEF-f_index.02.L", - "DEF-f_index.03.L" - ], - "middle.L": [ - "DEF-f_middle.01.L", - "DEF-f_middle.02.L", - "DEF-f_middle.03.L" - ], - "ring.L": [ - "DEF-f_ring.01.L", - "DEF-f_ring.02.L", - "DEF-f_ring.03.L" - ], - "pinky.L": [ - "DEF-f_pinky.01.L", - "DEF-f_pinky.02.L", - "DEF-f_pinky.03.L" - ], - "thumb.R": [ - "DEF-thumb.01.R", - "DEF-thumb.02.R", - "DEF-thumb.03.R" - ], - "index.R": [ - "DEF-f_index.01.R", - "DEF-f_index.02.R", - "DEF-f_index.03.R" - ], - "middle.R": [ - "DEF-f_middle.01.R", - "DEF-f_middle.02.R", - "DEF-f_middle.03.R" - ], - "ring.R": [ - "DEF-f_ring.01.R", - "DEF-f_ring.02.R", - "DEF-f_ring.03.R" - ], - "pinky.R": [ - "DEF-f_pinky.01.R", - "DEF-f_pinky.02.R", - "DEF-f_pinky.03.R" + "weights_authored": false, + "driven_bones": [ + "DEF-f_index.01.L", + "DEF-f_index.01.R", + "DEF-f_index.02.L", + "DEF-f_index.02.R", + "DEF-f_index.03.L", + "DEF-f_index.03.R", + "DEF-f_middle.01.L", + "DEF-f_middle.01.R", + "DEF-f_middle.02.L", + "DEF-f_middle.02.R", + "DEF-f_middle.03.L", + "DEF-f_middle.03.R", + "DEF-f_pinky.01.L", + "DEF-f_pinky.01.R", + "DEF-f_pinky.02.L", + "DEF-f_pinky.02.R", + "DEF-f_pinky.03.L", + "DEF-f_pinky.03.R", + "DEF-f_ring.01.L", + "DEF-f_ring.01.R", + "DEF-f_ring.02.L", + "DEF-f_ring.02.R", + "DEF-f_ring.03.L", + "DEF-f_ring.03.R", + "DEF-foot.L", + "DEF-foot.R", + "DEF-forearm.L", + "DEF-forearm.R", + "DEF-hand.L", + "DEF-hand.R", + "DEF-head", + "DEF-hips", + "DEF-neck", + "DEF-shin.L", + "DEF-shin.R", + "DEF-shoulder.L", + "DEF-shoulder.R", + "DEF-spine.001", + "DEF-spine.002", + "DEF-spine.003", + "DEF-thigh.L", + "DEF-thigh.R", + "DEF-thumb.01.L", + "DEF-thumb.01.R", + "DEF-thumb.02.L", + "DEF-thumb.02.R", + "DEF-thumb.03.L", + "DEF-thumb.03.R", + "DEF-toe.L", + "DEF-toe.R", + "DEF-upper_arm.L", + "DEF-upper_arm.R", + "root" ] - }, - "chains": [], - "twist": [], - "colliders": [ - { - "bone": "DEF-hips", - "child": "DEF-spine.001", - "from": 0.0, - "radius_head": 0.1913, - "radius_tail": 0.1913, - "radius": 0.1913, - "lid": true - }, - { - "bone": "DEF-thigh.L", - "child": "DEF-shin.L", - "from": 0.1, - "radius_head": 0.1435, - "radius_tail": 0.1304, - "radius": 0.1304 - }, - { - "bone": "DEF-thigh.R", - "child": "DEF-shin.R", - "from": 0.1, - "radius_head": 0.1084, - "radius_tail": 0.1084, - "radius": 0.1084 - }, - { - "bone": "DEF-shin.L", - "child": "DEF-foot.L", - "from": 0.1, - "radius_head": 0.0862, - "radius_tail": 0.0728, - "radius": 0.0728 - }, - { - "bone": "DEF-shin.R", - "child": "DEF-foot.R", - "from": 0.1, - "radius_head": 0.0862, - "radius_tail": 0.0728, - "radius": 0.0728 - } - ], - "weights_authored": false, - "driven_bones": [ - "DEF-f_index.01.L", - "DEF-f_index.01.R", - "DEF-f_index.02.L", - "DEF-f_index.02.R", - "DEF-f_index.03.L", - "DEF-f_index.03.R", - "DEF-f_middle.01.L", - "DEF-f_middle.01.R", - "DEF-f_middle.02.L", - "DEF-f_middle.02.R", - "DEF-f_middle.03.L", - "DEF-f_middle.03.R", - "DEF-f_pinky.01.L", - "DEF-f_pinky.01.R", - "DEF-f_pinky.02.L", - "DEF-f_pinky.02.R", - "DEF-f_pinky.03.L", - "DEF-f_pinky.03.R", - "DEF-f_ring.01.L", - "DEF-f_ring.01.R", - "DEF-f_ring.02.L", - "DEF-f_ring.02.R", - "DEF-f_ring.03.L", - "DEF-f_ring.03.R", - "DEF-foot.L", - "DEF-foot.R", - "DEF-forearm.L", - "DEF-forearm.R", - "DEF-hand.L", - "DEF-hand.R", - "DEF-head", - "DEF-hips", - "DEF-neck", - "DEF-shin.L", - "DEF-shin.R", - "DEF-shoulder.L", - "DEF-shoulder.R", - "DEF-spine.001", - "DEF-spine.002", - "DEF-spine.003", - "DEF-thigh.L", - "DEF-thigh.R", - "DEF-thumb.01.L", - "DEF-thumb.01.R", - "DEF-thumb.02.L", - "DEF-thumb.02.R", - "DEF-thumb.03.L", - "DEF-thumb.03.R", - "DEF-toe.L", - "DEF-toe.R", - "DEF-upper_arm.L", - "DEF-upper_arm.R", - "root" - ], - "surfaces": [ - { - "mesh": "Object_2", - "surface": 0, - "material": "body", - "class": "body", - "detail": "skin", - "why": "material name says 'skin'", - "verts": 1496, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_2", - "surface": 1, - "material": "body_parts", - "class": "body", - "detail": "skin", - "why": "material name says 'skin'", - "verts": 330, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_2", - "surface": 2, - "material": "hair", - "class": "hair", - "detail": "hair", - "why": "material name says 'hair'", - "verts": 500, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_2", - "surface": 3, - "material": "face", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 461, - "textured": true, - "chain_share": {} - } - ] } \ No newline at end of file diff --git a/assets/characters/skins/momo.glb b/assets/characters/skins/momo.glb index bf661e3..1ed0ee8 100644 Binary files a/assets/characters/skins/momo.glb and b/assets/characters/skins/momo.glb differ diff --git a/assets/characters/skins/momo.rig.json b/assets/characters/skins/momo.rig.json index 44e2a4f..48f3470 100644 --- a/assets/characters/skins/momo.rig.json +++ b/assets/characters/skins/momo.rig.json @@ -1,1927 +1,2749 @@ { - "roles": { - "hips": "Hips", - "head": "Unused_Noname_010", - "neck": "Neck", - "spine": [ - "Spine", - "Chest", - "Neck", - "Head", - "Unused_Noname_004", - "Unused_Noname_010" + "roles": { + "hips": "Hips", + "head": "Unused_Noname_010", + "neck": "Neck", + "spine": [ + "Spine", + "Chest", + "Neck", + "Head", + "Unused_Noname_004", + "Unused_Noname_010" + ], + "shoulder.L": "Left shoulder", + "shoulder.R": "Right shoulder", + "foot.R": "Right ankle", + "foot.L": "Left ankle", + "hand.L": "Left wrist", + "hand.R": "Right wrist", + "forearm.R": "Right elbow", + "forearm.L": "Left elbow", + "shin.R": "Right knee", + "shin.L": "Left knee", + "toe.L": "Toes4_L", + "toe.R": "Toes0_R", + "upper_arm.R": "Right arm", + "upper_arm.L": "Left arm", + "thigh.L": "Left leg", + "thigh.R": "Right leg" + }, + "fingers": { + "thumb.L": [ + "Thumb0_L", + "Thumb1_L", + "Thumb2_L" + ], + "index.L": [ + "IndexFinger1_L", + "IndexFinger2_L", + "IndexFinger3_L" + ], + "middle.L": [ + "MiddleFinger1_L", + "MiddleFinger2_L", + "MiddleFinger3_L" + ], + "ring.L": [ + "RingFinger1_L", + "RingFinger2_L", + "RingFinger3_L" + ], + "pinky.L": [ + "LittleFinger1_L", + "LittleFinger2_L", + "LittleFinger3_L" + ], + "thumb.R": [ + "Thumb0_R", + "Thumb1_R", + "Thumb2_R" + ], + "index.R": [ + "IndexFinger1_R", + "IndexFinger2_R", + "IndexFinger3_R" + ], + "middle.R": [ + "MiddleFinger1_R", + "MiddleFinger2_R", + "MiddleFinger3_R" + ], + "ring.R": [ + "RingFinger1_R", + "RingFinger2_R", + "RingFinger3_R" + ], + "pinky.R": [ + "LittleFinger1_R", + "LittleFinger2_R", + "LittleFinger3_R" + ] + }, + "chains": [ + { + "class": "bust", + "root_parent": "Chest", + "bones": [ + "Bust03_L", + "Bust03_L_001.seg1", + "Bust03_L_001.seg2", + "Bust03_L_001.seg3", + "Bust03_L_001.seg3.seg1", + "Bust03_L_001.seg3.seg2", + "Bust03_L_001.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.00499, + 0.0 + ], + [ + -0.0, + 0.00499, + 0.0 + ], + [ + -0.0, + 0.00499, + -0.0 + ], + [ + -0.0, + 0.01435, + 0.0 + ], + [ + 0.0, + 0.01435, + 0.0 + ], + [ + -0.0, + 0.01435, + -0.0 + ], + [ + 0.00683, + 0.00521, + -0.03851 + ] + ], + "hulls": [ + [ + [ + 0.04634, + -0.04498, + -0.0188 + ], + [ + 0.0395, + 0.04202, + 0.0229 + ], + [ + -0.03544, + -0.03722, + -0.02393 + ], + [ + 0.00962, + -0.01332, + 0.02258 + ], + [ + 0.05666, + -0.00097, + 0.01403 + ], + [ + 0.00326, + -0.05142, + -0.02317 + ], + [ + -0.02483, + -0.02224, + 0.00711 + ], + [ + 0.02135, + -0.03865, + 0.00152 + ], + [ + 0.02021, + 0.01297, + 0.02357 + ], + [ + 0.05294, + -0.02331, + 0.00172 + ] + ], + [ + [ + -0.03304, + -0.0253, + -0.03656 + ], + [ + 0.02219, + 0.02698, + 0.02558 + ], + [ + -0.01842, + -0.01287, + 0.01309 + ], + [ + -0.03167, + -0.02281, + -0.01434 + ], + [ + 0.01486, + 0.01924, + 0.02155 + ], + [ + -0.0281, + -0.01998, + -0.00439 + ], + [ + -0.03316, + -0.0247, + -0.02601 + ], + [ + -0.02363, + -0.01653, + 0.00511 + ] + ], + [ + [ + -0.04191, + -0.02902, + -0.00775 + ], + [ + 0.02138, + 0.02973, + 0.01524 + ], + [ + -0.01001, + 0.00052, + 0.02539 + ], + [ + 0.00623, + 0.01154, + 0.02665 + ], + [ + -0.00684, + 0.00428, + 0.02012 + ], + [ + 0.00366, + 0.00932, + 0.02186 + ] + ], + [ + [ + 0.00657, + 0.00922, + 0.04892 + ], + [ + -0.03022, + 0.0092, + -0.01448 + ], + [ + -0.00997, + 0.00605, + 0.0197 + ], + [ + 0.00373, + 0.0092, + 0.03236 + ], + [ + 0.01408, + -0.00483, + 0.0469 + ], + [ + -0.00192, + -0.00184, + 0.03967 + ], + [ + -0.02206, + 0.00804, + -0.00454 + ], + [ + 0.01661, + 0.00916, + 0.04293 + ], + [ + -0.00486, + -0.00062, + 0.02782 + ], + [ + -0.0134, + 0.01249, + 0.01071 + ] + ], + [ + [ + 0.01333, + 0.0098, + -0.05279 + ], + [ + -0.00156, + 0.00638, + 0.03494 + ], + [ + -0.02019, + 0.00852, + -0.00828 + ], + [ + -0.01098, + 0.00507, + -0.03248 + ], + [ + -0.00449, + 0.01282, + 0.00724 + ], + [ + -0.01788, + 0.00081, + 0.00851 + ], + [ + 0.00555, + 0.01275, + 0.01997 + ], + [ + -0.02646, + 0.0004, + -0.0319 + ], + [ + -0.00952, + 0.01233, + -0.0181 + ], + [ + -0.00882, + 0.01156, + 0.01983 + ] + ], + [ + [ + 0.01059, + 0.00382, + -0.04272 + ], + [ + -0.00545, + 0.00032, + 0.02199 + ], + [ + -0.00121, + 0.01407, + -0.01083 + ], + [ + -0.00382, + 0.00082, + -0.02753 + ], + [ + 0.01019, + 0.00734, + 0.02234 + ], + [ + -0.0082, + 0.00345, + 0.00619 + ], + [ + 0.01211, + 0.01116, + -0.03039 + ], + [ + 0.00528, + 0.00871, + 0.00075 + ], + [ + -0.00828, + 0.00261, + -0.04063 + ], + [ + 0.00468, + 0.00352, + 0.01108 + ] + ], + [ + [ + 0.00069, + 0.00241, + -0.02729 + ], + [ + 0.00989, + 0.00044, + 0.00541 + ], + [ + -0.00328, + 0.0009, + -0.01071 + ], + [ + 0.01248, + 0.00585, + -0.01005 + ], + [ + 0.00452, + 0.00293, + -0.0258 + ], + [ + 0.00253, + 0.00421, + -0.02599 + ], + [ + 0.00266, + 0.00115, + -0.02714 + ], + [ + -0.00352, + 0.00069, + -0.01266 + ] + ] + ], + "neighbours": [ + { + "Bust03_L_001.seg1": 0.3586729061666176, + "Bust02_L.seg3.seg1": 3.8588887890778887, + "Bust02_L.seg3.seg2": 2.917606487180191, + "Bust01_L_001.seg1": 4.670664258392975, + "Bust02_L.seg3": 3.723309870929166, + "Bust02_L": 6.863980003931569, + "Bust01_L": 1.3345315283103036, + "Bust02_L.seg1": 1.3477985038740319, + "Bust02_L.seg2": 1.3466512639605135, + "Bust02_L.seg3.seg3": 0.16277404522963113, + "Bust01_L_001.seg2": 1.4358638497041856 + }, + { + "Bust03_L_001.seg2": 0.7592651072118581, + "Bust02_L.seg3.seg1": 0.1237061780607136, + "Bust03_L": 0.3586729061666176, + "Bust02_L.seg3.seg2": 0.3167538728905921, + "Bust02_L": 0.644237262532878, + "Bust01_L": 0.35346984890674116, + "Bust02_L.seg1": 0.052446559059559306, + "Bust02_L.seg3.seg3": 0.011888406942535124 + }, + { + "Bust03_L_001.seg1": 0.7592651072118581, + "Bust02_L.seg3.seg1": 0.13488328733690058, + "Bust03_L_001.seg3": 0.7321880660170925, + "Bust02_L": 0.3316717898652811, + "Bust01_L": 0.26973418226795387, + "Bust02_L.seg1": 0.008437275370829056, + "Bust02_L.seg3.seg2": 0.290208261545314, + "Bust03_L_001.seg3.seg1": 0.2520912193710234, + "Bust02_L.seg3.seg3": 0.020978414525259503 + }, + { + "Bust01_L": 1.5052745077741634, + "Bust03_L_001.seg3.seg1": 1.8704421475539075, + "Bust02_L.seg3.seg1": 1.3505508543923577, + "Bust02_L.seg3.seg2": 2.143975667065543, + "Bust03_L_001.seg2": 0.7321880660170925, + "Bust02_L.seg3.seg3": 0.1271319299764597, + "Bust02_L.seg1": 0.00902320485287289, + "Bust02_L": 0.010517158767988644 + }, + { + "Bust02_L.seg2": 0.30773343000595166, + "Bust01_L": 1.5978016141043205, + "Bust03_L_001.seg3.seg2": 2.646226452359712, + "Bust02_L.seg3": 0.21922121334412736, + "Bust02_L.seg3.seg1": 2.0366420037191117, + "Bust02_L.seg1": 0.147190302093068, + "Bust03_L_001.seg3": 1.8704421475539075, + "Bust02_L.seg3.seg2": 0.363866540238196, + "Bust02_L": 0.8597345819068769, + "Bust03_L_001.seg2": 0.2520912193710234 + }, + { + "Bust02_L": 3.879083846473461, + "Bust01_L": 3.3716049932078844, + "Bust02_L.seg2": 1.1855289358719165, + "Bust03_L_001.seg3.seg1": 2.646226452359712, + "Bust02_L.seg1": 0.8669993786760157, + "Bust03_L_001.seg3.seg3": 2.573233646983547, + "Bust02_L.seg3": 0.3606864747225725, + "Bust02_L.seg3.seg1": 0.6416785060231014 + }, + { + "Bust02_L": 3.142309670472786, + "Bust01_L": 1.8397425588836491, + "Bust03_L_001.seg3.seg2": 2.573233646983547, + "Bust02_L.seg1": 0.22190282014397256, + "Bust02_L.seg2": 0.08037160404861954 + } + ] + }, + { + "class": "bust", + "root_parent": "Chest", + "bones": [ + "Bust02_L", + "Bust02_L.seg1", + "Bust02_L.seg2", + "Bust02_L.seg3", + "Bust02_L.seg3.seg1", + "Bust02_L.seg3.seg2", + "Bust02_L.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.00671, + -0.0 + ], + [ + 0.0, + 0.00671, + 0.0 + ], + [ + -0.0, + 0.00671, + -0.0 + ], + [ + -0.0, + 0.02278, + -0.0 + ], + [ + -0.0, + 0.02278, + 0.0 + ], + [ + 0.0, + 0.02278, + -0.0 + ], + [ + 0.0, + 0.02278, + -0.0 + ] + ], + "hulls": [ + [ + [ + -0.05329, + -0.06204, + 0.01442 + ], + [ + 0.03808, + 0.05407, + -0.02505 + ], + [ + 0.04119, + -0.04707, + -0.04114 + ], + [ + -0.00491, + -0.0594, + -0.0193 + ], + [ + 0.05435, + 0.00526, + -0.03349 + ], + [ + -0.02046, + -0.05285, + 0.02337 + ], + [ + 0.03347, + -0.01547, + -0.04407 + ], + [ + 0.04598, + 0.03063, + -0.04143 + ], + [ + 0.01604, + -0.0615, + -0.03614 + ], + [ + -0.04558, + -0.0441, + 0.03282 + ] + ], + [ + [ + -0.03762, + -0.04372, + 0.03863 + ], + [ + 0.04338, + 0.03738, + -0.02103 + ], + [ + 0.04475, + 0.01715, + -0.01642 + ], + [ + -0.02284, + -0.05229, + 0.03215 + ], + [ + 0.03678, + 0.03498, + -0.01244 + ], + [ + 0.0376, + 0.03055, + -0.01704 + ] + ], + [ + [ + -0.04425, + -0.03467, + 0.04791 + ], + [ + 0.03636, + 0.04794, + -0.01339 + ], + [ + -0.02573, + -0.05177, + 0.03878 + ], + [ + 0.03518, + 0.0335, + -0.00628 + ], + [ + -0.04076, + -0.04296, + 0.04344 + ], + [ + 0.04017, + 0.03788, + -0.00918 + ] + ], + [ + [ + 0.04788, + 0.00961, + -0.03177 + ], + [ + 0.04361, + 0.00583, + -0.00214 + ], + [ + 0.04011, + 0.01992, + -0.01312 + ], + [ + 0.04206, + 0.0082, + -0.01767 + ], + [ + 0.04203, + 0.0129, + -0.00737 + ], + [ + 0.04098, + 0.01544, + -0.0223 + ], + [ + 0.04709, + 0.01241, + -0.02131 + ], + [ + 0.04619, + 0.01684, + -0.01071 + ], + [ + 0.04874, + 0.00323, + -0.00389 + ] + ], + [ + [ + -0.0512, + 0.015, + 0.04397 + ], + [ + 0.04456, + 0.00699, + -0.0197 + ], + [ + 0.03135, + 0.01318, + -0.00398 + ], + [ + -0.05153, + 0.01961, + 0.02683 + ], + [ + 0.03287, + 0.01446, + -0.01761 + ], + [ + -0.04048, + 0.0159, + 0.03883 + ], + [ + 0.04059, + 0.01168, + -0.00699 + ], + [ + -0.05073, + 0.0142, + 0.035 + ], + [ + 0.03575, + 0.01019, + -0.02472 + ], + [ + 0.02842, + 0.01866, + -0.01177 + ] + ], + [ + [ + -0.04183, + 0.00379, + 0.01982 + ], + [ + 0.02987, + 0.00405, + -0.00818 + ], + [ + -0.00781, + 0.01524, + -0.00394 + ], + [ + -0.01903, + 0.00648, + 0.02046 + ], + [ + 0.01096, + 0.00972, + -0.01017 + ], + [ + -0.03204, + 0.01084, + 0.01052 + ], + [ + 0.0023, + 0.00805, + 0.00408 + ], + [ + -0.01959, + 0.01125, + 0.0092 + ], + [ + -0.01939, + 0.01634, + -0.00182 + ], + [ + 0.02185, + 0.00483, + -0.016 + ] + ], + [] + ], + "neighbours": [ + { + "Bust02_R": 3.9435785968840262, + "Bust01_R": 0.30519817466328325, + "Bust02_L.seg1": 0.36803861733326043, + "Bust01_L": 6.587348069081763, + "Bust03_L_001.seg3.seg2": 3.879083846473461, + "Bust03_L_001.seg3.seg3": 3.142309670472786, + "Bust01_L_001.seg1": 4.465735350628883, + "Bust03_L": 6.863980003931569, + "Bust03_L_001.seg1": 0.644237262532878, + "Bust03_L_001.seg2": 0.3316717898652811, + "Bust03_L_001.seg3.seg1": 0.8597345819068769, + "Bust01_L_001.seg2": 0.6338280679407119, + "Bust03_L_001.seg3": 0.010517158767988644 + }, + { + "Bust02_R": 0.3891492882426062, + "Bust02_L.seg2": 0.2765257206660864, + "Bust02_L": 0.36803861733326043, + "Bust01_L": 0.3851452433042416, + "Bust03_L_001.seg3.seg2": 0.8669993786760157, + "Bust03_L_001.seg3.seg1": 0.147190302093068, + "Bust03_L_001.seg3.seg3": 0.22190282014397256, + "Bust03_L_001.seg1": 0.052446559059559306, + "Bust03_L_001.seg2": 0.008437275370829056, + "Bust03_L": 1.3477985038740319, + "Bust01_L_001.seg1": 0.576539970347548, + "Bust02_R.seg1": 0.062370390055425606, + "Bust01_R": 0.027615028196460223, + "Bust01_L_001.seg2": 0.39275737184804727, + "Bust03_L_001.seg3": 0.00902320485287289 + }, + { + "Bust02_R": 0.41052354321339884, + "Bust02_L.seg3.seg2": 0.046474851580658144, + "Bust02_L.seg1": 0.2765257206660864, + "Bust01_L": 0.6615328390835954, + "Bust03_L_001.seg3.seg1": 0.30773343000595166, + "Bust03_L_001.seg3.seg2": 1.1855289358719165, + "Bust02_L.seg3": 0.2047552245049421, + "Bust02_L.seg3.seg1": 0.07279805859498367, + "Bust03_L": 1.3466512639605135, + "Bust01_L_001.seg1": 0.4814511027728548, + "Bust02_R.seg2": 0.021024301676234286, + "Bust02_R.seg1": 0.08813501649591318, + "Bust01_L_001.seg2": 0.6096695253273541, + "Bust01_R": 0.012396945518706376, + "Bust03_L_001.seg3.seg3": 0.08037160404861954 + }, + { + "Bust02_L.seg2": 0.2047552245049421, + "Bust03_L_001.seg3.seg2": 0.3606864747225725, + "Bust01_L": 0.07032285140442074, + "Bust03_L_001.seg3.seg1": 0.21922121334412736, + "Bust02_L.seg3.seg1": 0.561643697588948, + "Bust03_L": 3.723309870929166, + "Bust01_L_001.seg1": 0.7402913423204663, + "Bust01_L_001.seg2": 1.4104683381142173 + }, + { + "Bust02_L.seg2": 0.07279805859498367, + "Bust03_L_001.seg3.seg2": 0.6416785060231014, + "Bust01_L": 1.1766967531288508, + "Bust03_L_001.seg3.seg1": 2.0366420037191117, + "Bust02_L.seg3": 0.561643697588948, + "Bust03_L_001.seg3": 1.3505508543923577, + "Bust02_L.seg3.seg2": 1.51927919095755, + "Bust03_L_001.seg2": 0.13488328733690058, + "Bust03_L_001.seg1": 0.1237061780607136, + "Bust03_L": 3.8588887890778887, + "Bust01_L_001.seg1": 0.9839179007960346, + "Bust01_L_001.seg2": 1.4442330552949985, + "Bust02_R": 0.016424363549511933 + }, + { + "Bust02_R": 0.2982727503526822, + "Bust02_L.seg2": 0.046474851580658144, + "Bust02_R.seg1": 0.4291334144282218, + "Bust01_L": 3.659736651414759, + "Bust03_L_001.seg3.seg1": 0.363866540238196, + "Bust03_L_001.seg3": 2.143975667065543, + "Bust02_L.seg3.seg1": 1.51927919095755, + "Bust03_L_001.seg1": 0.3167538728905921, + "Bust03_L": 2.917606487180191, + "Bust01_L_001.seg1": 1.5798432020636302, + "Bust02_R.seg2": 0.1489095585051004, + "Bust02_L.seg3.seg3": 0.9515758677119792, + "Bust03_L_001.seg2": 0.290208261545314, + "Bust01_L_001.seg2": 0.5496193668051023, + "Bust02_R.seg3.seg2": 0.009932505948278908, + "Bust01_R": 0.004763507211692053 + }, + { + "Bust01_L": 1.5861144389656645, + "Bust02_L.seg3.seg2": 0.9515758677119792, + "Bust03_L_001.seg3": 0.1271319299764597, + "Bust03_L_001.seg1": 0.011888406942535124, + "Bust03_L": 0.16277404522963113, + "Bust01_L_001.seg1": 0.4934775100950594, + "Bust01_L_001.seg2": 0.03293627244596503, + "Bust02_R.seg1": 0.013941149562380628, + "Bust03_L_001.seg2": 0.020978414525259503 + } + ] + }, + { + "class": "bust", + "root_parent": "Chest", + "bones": [ + "Bust01_L", + "Bust01_L_001.seg1", + "Bust01_L_001.seg2", + "Bust01_L_001.seg3" + ], + "tips": [ + [ + -0.0, + 0.03306, + 0.0 + ], + [ + 0.0, + 0.03306, + -0.0 + ], + [ + -0.0, + 0.03306, + 0.0 + ], + [ + -0.0, + 0.03306, + 0.0 + ] + ], + "hulls": [ + [], + [ + [ + 0.01246, + 0.03042, + 0.01328 + ], + [ + 0.00663, + 0.02652, + -0.00078 + ], + [ + 0.00454, + 0.02848, + 0.00805 + ], + [ + 0.01454, + 0.02943, + 0.00554 + ], + [ + 0.0057, + 0.02755, + 0.00341 + ] + ], + [], + [] + ], + "neighbours": [ + { + "Bust02_L.seg2": 0.6615328390835954, + "Bust02_R": 0.535888595975675, + "Bust02_L": 6.587348069081763, + "Bust03_L_001.seg3.seg2": 3.3716049932078844, + "Bust03_L_001.seg3.seg1": 1.5978016141043205, + "Bust02_L.seg1": 0.3851452433042416, + "Bust03_L_001.seg3.seg3": 1.8397425588836491, + "Bust02_L.seg3": 0.07032285140442074, + "Bust02_L.seg3.seg1": 1.1766967531288508, + "Bust03_L_001.seg3": 1.5052745077741634, + "Bust02_L.seg3.seg2": 3.659736651414759, + "Bust01_L_001.seg1": 0.21420036573379497, + "Bust03_L": 1.3345315283103036, + "Bust03_L_001.seg1": 0.35346984890674116, + "Bust03_L_001.seg2": 0.26973418226795387, + "Bust02_L.seg3.seg3": 1.5861144389656645, + "Bust02_R.seg1": 0.009816568754717592 + }, + { + "Bust03_L": 4.670664258392975, + "Bust02_L.seg3.seg1": 0.9839179007960346, + "Bust02_L.seg3.seg2": 1.5798432020636302, + "Bust02_L.seg3": 0.7402913423204663, + "Bust01_L_001.seg2": 1.9960872844519022, + "Bust02_L": 4.465735350628883, + "Bust01_L": 0.21420036573379497, + "Bust02_L.seg1": 0.576539970347548, + "Bust02_L.seg2": 0.4814511027728548, + "Bust02_L.seg3.seg3": 0.4934775100950594 + }, + { + "Bust01_L_001.seg1": 1.9960872844519022, + "Bust02_L.seg3.seg2": 0.5496193668051023, + "Bust02_L.seg3.seg3": 0.03293627244596503, + "Bust02_L.seg3.seg1": 1.4442330552949985, + "Bust03_L": 1.4358638497041856, + "Bust02_L.seg3": 1.4104683381142173, + "Bust02_L.seg2": 0.6096695253273541, + "Bust02_L.seg1": 0.39275737184804727, + "Bust02_L": 0.6338280679407119 + }, + {} + ] + }, + { + "class": "bust", + "root_parent": "Chest", + "bones": [ + "Bust03_R", + "Bust03_R_001.seg1", + "Bust03_R_001.seg2", + "Bust03_R_001.seg3", + "Bust03_R_001.seg3.seg1", + "Bust03_R_001.seg3.seg2", + "Bust03_R_001.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.00502, + -0.0 + ], + [ + 0.0, + 0.00502, + -0.0 + ], + [ + -0.0, + 0.00502, + 0.0 + ], + [ + -0.0, + 0.01419, + -0.0 + ], + [ + 0.0, + 0.01419, + -0.0 + ], + [ + 0.0, + 0.01419, + 0.0 + ], + [ + -0.00419, + 0.0059, + -0.04446 + ] + ], + "hulls": [ + [ + [ + -0.06165, + -0.0298, + -0.00191 + ], + [ + 0.03437, + -0.03689, + -0.02435 + ], + [ + -0.02689, + 0.02875, + 0.01865 + ], + [ + -0.00589, + -0.02238, + 0.01514 + ], + [ + -0.02605, + -0.0467, + -0.02952 + ], + [ + -0.05941, + 0.00998, + 0.02131 + ], + [ + 0.02519, + -0.02186, + 0.00864 + ], + [ + -0.03916, + -0.01332, + 0.01505 + ], + [ + 0.00391, + -0.03914, + -0.00911 + ], + [ + -0.00974, + 0.00804, + 0.02386 + ] + ], + [ + [ + -0.03368, + 0.0381, + 0.01234 + ], + [ + 0.02844, + -0.01959, + -0.00363 + ], + [ + -0.00677, + 0.01702, + 0.02547 + ], + [ + 0.0184, + -0.01219, + 0.01353 + ], + [ + -0.02276, + 0.02786, + 0.02271 + ], + [ + -0.0156, + 0.01949, + 0.02069 + ], + [ + 0.02377, + -0.01597, + 0.00572 + ] + ], + [ + [ + 0.04198, + -0.03518, + -0.02675 + ], + [ + -0.02233, + 0.02964, + 0.01387 + ], + [ + 0.00984, + 0.0011, + 0.02547 + ], + [ + 0.04078, + -0.02864, + -0.008 + ], + [ + -0.0043, + 0.00976, + 0.02127 + ], + [ + 0.03219, + -0.02764, + -0.01345 + ], + [ + 0.03386, + -0.02981, + -0.02504 + ], + [ + 0.00631, + 0.00488, + 0.0198 + ] + ], + [ + [ + -0.00703, + 0.00953, + 0.04869 + ], + [ + 0.02575, + 0.01303, + -0.03207 + ], + [ + 0.01317, + 0.01208, + 0.01054 + ], + [ + 0.00428, + -0.00071, + 0.02772 + ], + [ + 0.02044, + 0.01184, + -0.01404 + ], + [ + -0.01482, + -0.00437, + 0.04675 + ], + [ + -0.00411, + 0.00932, + 0.03214 + ], + [ + 0.00149, + -0.00157, + 0.03983 + ], + [ + 0.00957, + 0.00578, + 0.01957 + ], + [ + 0.03054, + 0.00801, + -0.01471 + ] + ], + [ + [ + -0.01303, + 0.00961, + -0.05317 + ], + [ + -0.0213, + 0.00703, + 0.03959 + ], + [ + 0.02096, + 0.00804, + -0.00847 + ], + [ + 0.00366, + 0.01413, + 0.02051 + ], + [ + 0.01108, + 0.00453, + -0.03271 + ], + [ + 0.01863, + 0.00083, + 0.00848 + ], + [ + 0.00459, + 0.00333, + 0.03256 + ], + [ + 0.00972, + 0.01193, + -0.0184 + ], + [ + -0.00837, + 0.00769, + 0.02745 + ], + [ + 0.0046, + 0.01272, + 0.00692 + ] + ], + [ + [ + -0.01016, + 0.00381, + -0.04314 + ], + [ + -0.01472, + 0.0037, + 0.02381 + ], + [ + 0.00967, + 0.0085, + -0.00331 + ], + [ + -0.00513, + 0.00947, + -0.02234 + ], + [ + 0.00552, + 0.00047, + 0.02166 + ], + [ + 0.00855, + 0.00133, + -0.04116 + ], + [ + -0.00448, + 0.0038, + 0.01068 + ], + [ + 0.00412, + 0.00061, + -0.02787 + ], + [ + -0.00854, + 0.01249, + -0.00962 + ], + [ + 0.00594, + 0.01309, + -0.01437 + ] + ], + [ + [ + -5e-05, + 0.00246, + -0.02777 + ], + [ + -0.01142, + 0.00623, + -0.01072 + ], + [ + 0.00382, + 0.00099, + -0.01117 + ], + [ + -0.00388, + 0.00307, + -0.02631 + ], + [ + -0.00185, + 0.0043, + -0.02649 + ], + [ + -0.00205, + 0.00124, + -0.02762 + ], + [ + 0.00406, + 0.00076, + -0.01311 + ] + ] + ], + "neighbours": [ + { + "Bust01_R_001.seg1": 4.872979579964469, + "Bust02_R.seg3": 3.8096784365146026, + "Bust02_R.seg2": 1.4685666200036516, + "Bust02_R.seg3.seg1": 4.091008918518201, + "Bust02_R.seg1": 1.6583980823664166, + "Bust03_R_001.seg1": 0.46425750390917786, + "Bust02_R": 5.526851071461593, + "Bust01_R": 1.3542830545800926, + "Bust02_R.seg3.seg2": 3.7923387795275865, + "Bust01_R_001.seg2": 1.5518790811540795, + "Bust02_R.seg3.seg3": 0.20646308877271352 + }, + { + "Bust03_R": 0.46425750390917786, + "Bust03_R_001.seg2": 0.9843194343001453, + "Bust02_R.seg3.seg1": 0.21529264096571454, + "Bust02_R.seg3.seg2": 0.3199974347075536, + "Bust02_R": 0.43331033938613484, + "Bust02_R.seg1": 0.04796793112592734, + "Bust01_R": 0.3229379864088022, + "Bust02_R.seg3.seg3": 0.02434423377548156 + }, + { + "Bust03_R_001.seg3": 0.6429316084978858, + "Bust03_R_001.seg1": 0.9843194343001453, + "Bust02_R.seg3.seg1": 0.1864379065181292, + "Bust01_R": 0.3312070217482911, + "Bust02_R.seg3.seg2": 0.3067298550694062, + "Bust02_R": 0.473111645251968, + "Bust02_R.seg1": 0.022868736349894725, + "Bust02_R.seg2": 0.015914169786305887, + "Bust03_R_001.seg3.seg1": 0.17511312672375734 + }, + { + "Bust01_R": 1.3217243811786785, + "Bust03_R_001.seg3.seg1": 1.838716663287674, + "Bust02_R.seg3": 0.03507813582950803, + "Bust02_R.seg3.seg1": 1.7996471992782785, + "Bust03_R_001.seg2": 0.6429316084978858, + "Bust02_R": 0.03657530946414411, + "Bust02_R.seg3.seg2": 1.1646884236250614, + "Bust02_R.seg2": 0.13685483424734102, + "Bust02_R.seg3.seg3": 0.030189774167661554 + }, + { + "Bust01_R": 1.668925975376655, + "Bust03_R_001.seg3": 1.838716663287674, + "Bust02_R.seg3": 0.19369096987123835, + "Bust02_R.seg3.seg1": 1.0333359118852008, + "Bust02_R.seg2": 1.1014637724450491, + "Bust02_R.seg1": 0.514428302025346, + "Bust03_R_001.seg3.seg2": 2.660225662361538, + "Bust02_R": 1.115169820484057, + "Bust02_R.seg3.seg2": 0.011899287372656886, + "Bust03_R_001.seg2": 0.17511312672375734 + }, + { + "Bust01_R": 3.2855771769645594, + "Bust02_R": 5.151639805711525, + "Bust03_R_001.seg3.seg3": 2.505141755581172, + "Bust02_R.seg2": 0.5013527261295944, + "Bust02_R.seg1": 1.058675894719488, + "Bust03_R_001.seg3.seg1": 2.660225662361538, + "Bust02_R.seg3.seg1": 0.052888163833218926, + "Bust02_R.seg3": 0.02659486539865341 + }, + { + "Bust01_R": 1.900665125198059, + "Bust02_R": 3.6139076485458776, + "Bust03_R_001.seg3.seg2": 2.505141755581172, + "Bust02_R.seg1": 0.023796804867131982 + } + ] + }, + { + "class": "bust", + "root_parent": "Chest", + "bones": [ + "Bust02_R", + "Bust02_R.seg1", + "Bust02_R.seg2", + "Bust02_R.seg3", + "Bust02_R.seg3.seg1", + "Bust02_R.seg3.seg2", + "Bust02_R.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.00727, + 0.0 + ], + [ + -0.0, + 0.00727, + 0.0 + ], + [ + -0.0, + 0.00727, + 0.0 + ], + [ + -0.0, + 0.02132, + 0.0 + ], + [ + -0.0, + 0.02132, + -0.0 + ], + [ + -0.0, + 0.02132, + -0.0 + ], + [ + -0.0, + 0.02132, + -0.0 + ] + ], + "hulls": [ + [ + [ + 0.04401, + -0.06689, + 0.02491 + ], + [ + -0.03462, + 0.05156, + -0.04028 + ], + [ + -0.03463, + -0.04498, + -0.0439 + ], + [ + 0.00213, + -0.0617, + -0.00806 + ], + [ + -0.0368, + 0.00366, + -0.04301 + ], + [ + 0.03404, + -0.03799, + 0.04267 + ], + [ + 0.00827, + -0.05969, + 0.02333 + ], + [ + -0.00954, + -0.05169, + -0.03263 + ], + [ + -0.04391, + 0.02797, + -0.03912 + ], + [ + -0.0543, + -0.01331, + -0.03926 + ] + ], + [ + [ + -0.03205, + 0.04907, + -0.03145 + ], + [ + 0.02236, + -0.04474, + 0.04231 + ], + [ + -0.04008, + 0.02024, + -0.02208 + ], + [ + -0.02309, + 0.04357, + -0.01934 + ], + [ + -0.03183, + 0.03314, + -0.02048 + ], + [ + -0.03919, + 0.03885, + -0.02992 + ], + [ + -0.04254, + 0.02721, + -0.02709 + ], + [ + -0.03185, + 0.02969, + -0.02512 + ] + ], + [ + [ + 0.03185, + -0.03665, + 0.05212 + ], + [ + -0.02993, + 0.04536, + -0.02259 + ], + [ + -0.03998, + 0.01677, + -0.01623 + ], + [ + -0.02962, + 0.03292, + -0.0108 + ], + [ + -0.03683, + 0.0363, + -0.01945 + ], + [ + -0.02242, + 0.04091, + -0.0147 + ], + [ + -0.03089, + 0.02896, + -0.01571 + ] + ], + [ + [ + -0.04099, + 0.00677, + -0.03177 + ], + [ + -0.04037, + 0.00582, + -0.00365 + ], + [ + -0.03315, + 0.01638, + -0.02208 + ], + [ + -0.04255, + 0.00792, + -0.0203 + ], + [ + -0.03134, + 0.01394, + -0.03171 + ], + [ + -0.03771, + 0.01283, + -0.00739 + ], + [ + -0.0331, + 0.00669, + -0.02881 + ], + [ + -0.03563, + 0.0088, + -0.01844 + ], + [ + -0.04291, + 0.01064, + -0.00809 + ] + ], + [ + [ + 0.04461, + 0.01299, + 0.04197 + ], + [ + -0.03326, + 0.01248, + -0.02654 + ], + [ + -0.02936, + 0.00982, + -0.00622 + ], + [ + -0.03049, + 0.00587, + -0.01593 + ], + [ + -0.02474, + 0.01612, + -0.01181 + ], + [ + -0.02661, + 0.01255, + -0.02038 + ], + [ + -0.02729, + 0.00954, + -0.02918 + ], + [ + -0.03233, + 0.01546, + -0.01916 + ] + ], + [ + [ + 0.05051, + 0.0082, + 0.03198 + ], + [ + -0.02737, + 0.00689, + -0.02443 + ], + [ + 0.00785, + 0.01036, + 0.01096 + ], + [ + -0.01214, + 0.00606, + -0.00428 + ], + [ + 0.03116, + 0.01579, + 0.00964 + ], + [ + 0.02356, + 0.01259, + 0.02781 + ], + [ + 0.00214, + 0.01465, + -0.00418 + ], + [ + 0.04059, + 0.0073, + 0.02312 + ], + [ + -0.0117, + 0.01056, + -0.01634 + ], + [ + 0.04268, + 0.01416, + 0.01336 + ] + ], + [] + ], + "neighbours": [ + { + "Bust02_L.seg2": 0.41052354321339884, + "Bust02_L.seg3.seg2": 0.2982727503526822, + "Bust02_L.seg1": 0.3891492882426062, + "Bust01_R": 7.095980061149035, + "Bust02_R.seg1": 0.34267716667835146, + "Bust01_L": 0.535888595975675, + "Bust02_L": 3.9435785968840262, + "Bust03_R_001.seg3.seg2": 5.151639805711525, + "Bust03_R_001.seg3.seg3": 3.6139076485458776, + "Bust03_R_001.seg3.seg1": 1.115169820484057, + "Bust01_R_001.seg1": 3.2529226583629116, + "Bust03_R": 5.526851071461593, + "Bust03_R_001.seg2": 0.473111645251968, + "Bust03_R_001.seg1": 0.43331033938613484, + "Bust03_R_001.seg3": 0.03657530946414411, + "Bust01_R_001.seg2": 0.15262119013582015, + "Bust02_L.seg3.seg1": 0.016424363549511933 + }, + { + "Bust02_L.seg3.seg2": 0.4291334144282218, + "Bust01_R": 0.6486448018274646, + "Bust02_R.seg2": 0.3699912966836818, + "Bust02_R": 0.34267716667835146, + "Bust03_R_001.seg3.seg1": 0.514428302025346, + "Bust03_R_001.seg3.seg2": 1.058675894719488, + "Bust01_R_001.seg1": 0.9501698919544972, + "Bust03_R": 1.6583980823664166, + "Bust03_R_001.seg1": 0.04796793112592734, + "Bust03_R_001.seg2": 0.022868736349894725, + "Bust02_L.seg2": 0.08813501649591318, + "Bust02_L.seg1": 0.062370390055425606, + "Bust01_R_001.seg2": 0.29790414581837243, + "Bust03_R_001.seg3.seg3": 0.023796804867131982, + "Bust02_L.seg3.seg3": 0.013941149562380628, + "Bust01_L": 0.009816568754717592 + }, + { + "Bust02_R.seg1": 0.3699912966836818, + "Bust01_R": 0.7941354195550765, + "Bust03_R_001.seg3.seg1": 1.1014637724450491, + "Bust02_R.seg3": 0.23690872116343858, + "Bust03_R_001.seg3.seg2": 0.5013527261295944, + "Bust03_R": 1.4685666200036516, + "Bust01_R_001.seg1": 0.7388861380556807, + "Bust02_L.seg2": 0.021024301676234286, + "Bust02_L.seg3.seg2": 0.1489095585051004, + "Bust02_R.seg3.seg2": 0.043855311627231275, + "Bust03_R_001.seg3": 0.13685483424734102, + "Bust02_R.seg3.seg1": 0.10007491529948342, + "Bust01_R_001.seg2": 0.45532842437374466, + "Bust03_R_001.seg2": 0.015914169786305887 + }, + { + "Bust01_R": 0.02840122605162118, + "Bust03_R_001.seg3.seg1": 0.19369096987123835, + "Bust03_R_001.seg3": 0.03507813582950803, + "Bust02_R.seg3.seg1": 0.5319036274291622, + "Bust02_R.seg2": 0.23690872116343858, + "Bust03_R": 3.8096784365146026, + "Bust01_R_001.seg1": 1.043837113289384, + "Bust01_R_001.seg2": 1.2901951073301015, + "Bust03_R_001.seg3.seg2": 0.02659486539865341 + }, + { + "Bust01_R": 0.9829024807132503, + "Bust03_R_001.seg3.seg1": 1.0333359118852008, + "Bust03_R_001.seg3": 1.7996471992782785, + "Bust02_R.seg3": 0.5319036274291622, + "Bust03_R": 4.091008918518201, + "Bust01_R_001.seg1": 0.9027050900715654, + "Bust03_R_001.seg2": 0.1864379065181292, + "Bust03_R_001.seg1": 0.21529264096571454, + "Bust02_R.seg3.seg2": 1.357242740032684, + "Bust01_R_001.seg2": 1.7186133114694615, + "Bust02_R.seg2": 0.10007491529948342, + "Bust03_R_001.seg3.seg2": 0.052888163833218926 + }, + { + "Bust03_R_001.seg1": 0.3199974347075536, + "Bust03_R": 3.7923387795275865, + "Bust02_R.seg3.seg1": 1.357242740032684, + "Bust01_R": 2.440210027591748, + "Bust03_R_001.seg2": 0.3067298550694062, + "Bust01_R_001.seg1": 1.7358954924312886, + "Bust02_R.seg3.seg3": 0.8671538583119991, + "Bust03_R_001.seg3": 1.1646884236250614, + "Bust02_R.seg2": 0.043855311627231275, + "Bust01_R_001.seg2": 1.0634390136076133, + "Bust03_R_001.seg3.seg1": 0.011899287372656886, + "Bust02_L.seg3.seg2": 0.009932505948278908 + }, + { + "Bust01_R": 1.2453558432318645, + "Bust02_R.seg3.seg2": 0.8671538583119991, + "Bust03_R_001.seg1": 0.02434423377548156, + "Bust01_R_001.seg1": 0.8679172668651285, + "Bust01_R_001.seg2": 0.20069937704986562, + "Bust03_R": 0.20646308877271352, + "Bust03_R_001.seg3": 0.030189774167661554 + } + ] + }, + { + "class": "bust", + "root_parent": "Chest", + "bones": [ + "Bust01_R", + "Bust01_R_001.seg1", + "Bust01_R_001.seg2", + "Bust01_R_001.seg3" + ], + "tips": [ + [ + -0.0, + 0.03325, + -0.0 + ], + [ + -0.0, + 0.03325, + 0.0 + ], + [ + 0.0, + 0.03325, + -0.0 + ], + [ + 0.0, + 0.03325, + -0.0 + ] + ], + "hulls": [ + [], + [ + [ + -0.02064, + 0.02212, + -0.01718 + ], + [ + -0.00536, + 0.02826, + 0.00774 + ], + [ + -0.01888, + 0.02651, + -0.00485 + ], + [ + -0.00745, + 0.02622, + -0.00108 + ], + [ + -0.01978, + 0.02523, + -0.00908 + ], + [ + -0.00652, + 0.02729, + 0.0031 + ] + ], + [ + [ + -0.01424, + -0.0006, + 0.05201 + ], + [ + -0.00336, + 0.00663, + 0.00936 + ], + [ + -0.01194, + 0.00376, + 0.02499 + ], + [ + -0.00847, + 0.00398, + 0.03858 + ], + [ + -0.00264, + 0.00631, + 0.0192 + ] + ], + [] + ], + "neighbours": [ + { + "Bust02_R.seg1": 0.6486448018274646, + "Bust02_R.seg2": 0.7941354195550765, + "Bust02_R": 7.095980061149035, + "Bust02_L": 0.30519817466328325, + "Bust03_R_001.seg3.seg2": 3.2855771769645594, + "Bust03_R_001.seg3.seg3": 1.900665125198059, + "Bust03_R_001.seg3.seg1": 1.668925975376655, + "Bust03_R_001.seg3": 1.3217243811786785, + "Bust02_R.seg3": 0.02840122605162118, + "Bust02_R.seg3.seg1": 0.9829024807132503, + "Bust01_R_001.seg1": 0.23046008919102093, + "Bust03_R": 1.3542830545800926, + "Bust02_R.seg3.seg2": 2.440210027591748, + "Bust03_R_001.seg2": 0.3312070217482911, + "Bust03_R_001.seg1": 0.3229379864088022, + "Bust02_R.seg3.seg3": 1.2453558432318645, + "Bust02_L.seg1": 0.027615028196460223, + "Bust02_L.seg2": 0.012396945518706376, + "Bust02_L.seg3.seg2": 0.004763507211692053 + }, + { + "Bust02_R": 3.2529226583629116, + "Bust01_R": 0.23046008919102093, + "Bust01_R_001.seg2": 2.0399540746772074, + "Bust02_R.seg1": 0.9501698919544972, + "Bust03_R": 4.872979579964469, + "Bust02_R.seg3": 1.043837113289384, + "Bust02_R.seg2": 0.7388861380556807, + "Bust02_R.seg3.seg1": 0.9027050900715654, + "Bust02_R.seg3.seg2": 1.7358954924312886, + "Bust02_R.seg3.seg3": 0.8679172668651285 + }, + { + "Bust01_R_001.seg1": 2.0399540746772074, + "Bust03_R": 1.5518790811540795, + "Bust02_R.seg3.seg1": 1.7186133114694615, + "Bust02_R.seg3": 1.2901951073301015, + "Bust02_R.seg3.seg2": 1.0634390136076133, + "Bust02_R.seg3.seg3": 0.20069937704986562, + "Bust02_R.seg2": 0.45532842437374466, + "Bust02_R.seg1": 0.29790414581837243, + "Bust02_R": 0.15262119013582015 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "HairF_Top", + "HairFL", + "HairFL_01" + ], + "tips": [ + [ + 0.0, + 0.12695, + -0.0 + ], + [ + -0.0, + 0.02817, + 0.0 + ], + [ + -0.0, + 0.02817, + 0.0 + ] + ], + "hulls": [ + [ + [ + -0.0709, + 0.19701, + -0.02448 + ], + [ + -0.01219, + 0.03468, + 0.00089 + ], + [ + -0.00316, + 0.12791, + -0.02292 + ], + [ + -0.0051, + 0.07784, + -0.03084 + ], + [ + -0.02866, + 0.16772, + -0.03377 + ], + [ + -0.02224, + 0.14248, + -0.04227 + ], + [ + 0.00083, + 0.05853, + -0.01463 + ], + [ + 0.0047, + 0.10031, + -0.02643 + ], + [ + -0.04913, + 0.18521, + -0.02467 + ], + [ + -0.01094, + 0.11379, + -0.04102 + ] + ], + [], + [] + ], + "neighbours": [ + { + "HairFL": 0.05267075423349632 + }, + { + "HairF_Top": 0.05267075423349632 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "HairF_Top_001", + "HairFR_02_00", + "HairFR_02_01", + "HairFR_02_02" + ], + "tips": [ + [ + -0.0, + 0.20762, + 0.0 + ], + [ + -0.0, + 0.04704, + 0.0 + ], + [ + 0.0, + 0.06538, + -0.0 + ], + [ + 0.0, + 0.06538, + -0.0 + ] + ], + "hulls": [ + [ + [ + -0.00288, + 0.22283, + 0.013 + ], + [ + 0.00889, + 0.03929, + -0.01939 + ], + [ + -0.02264, + 0.13514, + -0.04835 + ], + [ + -0.02427, + 0.09846, + 0.0138 + ], + [ + 0.00018, + 0.204, + -0.05523 + ], + [ + -0.02084, + 0.15999, + 0.02109 + ], + [ + -0.01409, + 0.08541, + -0.03676 + ], + [ + -0.0253, + 0.18142, + -0.02376 + ], + [ + -0.0335, + 0.13129, + -0.0098 + ], + [ + -0.0196, + 0.06556, + -0.00155 + ] + ], + [ + [ + 0.02007, + 0.05938, + -0.04047 + ], + [ + -0.00686, + 0.02392, + 0.01915 + ], + [ + -0.00724, + 0.06091, + -0.00245 + ], + [ + 0.00043, + 0.02599, + -0.02315 + ], + [ + -0.00422, + 0.06112, + 0.02235 + ], + [ + 0.01078, + 0.05041, + -0.0206 + ], + [ + 0.01842, + 0.03651, + -0.04542 + ], + [ + -0.01235, + 0.03465, + 0.00185 + ], + [ + -0.01321, + 0.04676, + 0.01771 + ], + [ + -0.00091, + 0.03762, + -0.00943 + ] + ], + [ + [ + -0.00679, + 0.10408, + 0.0075 + ], + [ + 0.00039, + 0.01538, + -0.0133 + ], + [ + 0.01456, + 0.06402, + -0.02416 + ], + [ + -0.01345, + 0.0494, + 0.02085 + ], + [ + -0.00778, + 0.07451, + 0.00301 + ], + [ + -0.00182, + 0.04668, + -0.00654 + ], + [ + -0.01943, + 0.02489, + 0.01069 + ], + [ + 0.01626, + 0.03975, + -0.03271 + ], + [ + 0.00807, + 0.08816, + -0.01896 + ], + [ + -0.00331, + 0.02975, + -0.00083 + ] + ], + [] + ], + "neighbours": [ + { + "HairFR_02_00": 11.198780385334263, + "HairFR_02_01": 1.6859091911334967, + "HairFR_02_02": 0.019142537174898777 + }, + { + "HairF_Top_001": 11.198780385334263, + "HairFR_02_01": 14.303984964823453, + "HairFR_02_02": 0.6820841668863975 + }, + { + "HairF_Top_001": 1.6859091911334967, + "HairFR_02_00": 14.303984964823453, + "HairFR_02_02": 12.428229860437282 + }, + { + "HairFR_02_01": 12.428229860437282, + "HairFR_02_00": 0.6820841668863975, + "HairF_Top_001": 0.019142537174898777 + } + ] + }, + { + "class": "hair", + "root_parent": "Head", + "bones": [ + "HairF_Top_002", + "HairFR", + "HairFR_01", + "HairFR_02" + ], + "tips": [ + [ + -0.0, + 0.12133, + -0.0 + ], + [ + 0.0, + 0.02307, + -0.0 + ], + [ + 0.0, + 0.02981, + -0.0 + ], + [ + 0.0, + 0.02981, + -0.0 + ] + ], + "hulls": [ + [ + [ + -0.05014, + 0.12632, + -0.01759 + ], + [ + -0.03167, + 0.00333, + -0.03768 + ], + [ + -0.04825, + 0.05919, + -0.00628 + ], + [ + -0.04605, + 0.09632, + -0.00308 + ], + [ + -0.03867, + 0.02863, + -0.01278 + ], + [ + -0.056, + 0.08241, + -0.01369 + ], + [ + -0.05546, + 0.10631, + -0.01526 + ], + [ + -0.03607, + 0.0151, + -0.02392 + ], + [ + -0.04399, + 0.11512, + -0.00607 + ], + [ + -0.04627, + 0.04432, + -0.01442 + ] + ], + [ + [ + -0.03139, + 0.01458, + -0.03773 + ], + [ + -0.02316, + 0.0231, + -0.01229 + ], + [ + -0.03061, + 0.02285, + -0.02646 + ], + [ + -0.02709, + 0.01232, + -0.02493 + ], + [ + -0.02925, + 0.02335, + -0.01823 + ], + [ + -0.02453, + 0.013, + -0.03198 + ], + [ + -0.02469, + 0.01954, + -0.0246 + ], + [ + -0.03143, + 0.01883, + -0.03237 + ], + [ + -0.03219, + 0.01133, + -0.03243 + ], + [ + -0.02541, + 0.01284, + -0.03788 + ] + ], + [ + [ + -0.02626, + 0.00464, + -0.01368 + ], + [ + -0.0101, + 0.00782, + 0.00737 + ], + [ + -0.01803, + 0.00459, + -0.00296 + ], + [ + -0.02014, + 0.00414, + -0.01004 + ], + [ + -0.01431, + 0.00762, + 0.00157 + ], + [ + -0.02189, + 0.00626, + -0.00693 + ], + [ + -0.02398, + 0.00295, + -0.01634 + ], + [ + -0.01261, + 0.00713, + 0.00436 + ], + [ + -0.0208, + 0.00235, + -0.00737 + ], + [ + -0.02368, + 0.00442, + -0.00806 + ] + ], + [] + ], + "neighbours": [ + { + "HairFR": 6.522659145445981, + "HairFR_01": 2.4443696761753726, + "HairFR_02": 0.012604948497175639 + }, + { + "HairF_Top_002": 6.522659145445981, + "HairFR_01": 10.866074319649554, + "HairFR_02": 0.8757130831490118 + }, + { + "HairF_Top_002": 2.4443696761753726, + "HairFR": 10.866074319649554, + "HairFR_02": 0.9474554536632946 + }, + { + "HairFR_01": 0.9474554536632946, + "HairFR": 0.8757130831490118, + "HairF_Top_002": 0.012604948497175639 + } + ] + } ], - "shoulder.L": "Left shoulder", - "shoulder.R": "Right shoulder", - "foot.L": "Left ankle", - "foot.R": "Right ankle", - "hand.L": "Left wrist", - "hand.R": "Right wrist", - "forearm.R": "Right elbow", - "forearm.L": "Left elbow", - "shin.R": "Right knee", - "shin.L": "Left knee", - "toe.R": "Toes4_R", - "toe.L": "Toes4_L", - "upper_arm.R": "Right arm", - "upper_arm.L": "Left arm", - "thigh.L": "Left leg", - "thigh.R": "Right leg" - }, - "fingers": { - "thumb.L": [ - "Thumb0_L", - "Thumb1_L", - "Thumb2_L" + "twist": [], + "surfaces": [ + { + "mesh": "Object_262", + "surface": 0, + "material": "Face", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 9620, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_263", + "surface": 0, + "material": "Clothes", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 12846, + "textured": true, + "chain_share": { + "bust": 0.056 + } + }, + { + "mesh": "Object_264", + "surface": 0, + "material": "Hair_A", + "class": "hair", + "detail": "hair", + "why": "material name says 'hair'", + "verts": 15552, + "textured": true, + "chain_share": { + "hair": 0.018 + } + }, + { + "mesh": "Object_265", + "surface": 0, + "material": "Hair_B", + "class": "hair", + "detail": "hair", + "why": "material name says 'hair'", + "verts": 5281, + "textured": true, + "chain_share": { + "hair": 0.094 + } + }, + { + "mesh": "Object_266", + "surface": 0, + "material": "Iris", + "class": "body", + "detail": "eyes", + "why": "material name says 'eyes'", + "verts": 260, + "textured": true, + "chain_share": {} + } ], - "index.L": [ - "IndexFinger1_L", - "IndexFinger2_L", - "IndexFinger3_L" + "colliders": [ + { + "bone": "Hips", + "child": "Spine", + "from": 0.0, + "radius_head": 0.128, + "radius_tail": 0.128, + "radius": 0.128, + "lid": true + }, + { + "bone": "Left leg", + "child": "Left knee", + "from": 0.1, + "radius_head": 0.1203, + "radius_tail": 0.0758, + "radius": 0.0758 + }, + { + "bone": "Right leg", + "child": "Right knee", + "from": 0.1, + "radius_head": 0.1205, + "radius_tail": 0.0756, + "radius": 0.0756 + }, + { + "bone": "Left knee", + "child": "Left ankle", + "from": 0.1, + "radius_head": 0.1305, + "radius_tail": 0.1204, + "radius": 0.1204 + }, + { + "bone": "Right knee", + "child": "Right ankle", + "from": 0.1, + "radius_head": 0.1304, + "radius_tail": 0.1204, + "radius": 0.1204 + } ], - "middle.L": [ - "MiddleFinger1_L", - "MiddleFinger2_L", - "MiddleFinger3_L" - ], - "ring.L": [ - "RingFinger1_L", - "RingFinger2_L", - "RingFinger3_L" - ], - "pinky.L": [ - "LittleFinger1_L", - "LittleFinger2_L", - "LittleFinger3_L" - ], - "thumb.R": [ - "Thumb0_R", - "Thumb1_R", - "Thumb2_R" - ], - "index.R": [ - "IndexFinger1_R", - "IndexFinger2_R", - "IndexFinger3_R" - ], - "middle.R": [ - "MiddleFinger1_R", - "MiddleFinger2_R", - "MiddleFinger3_R" - ], - "ring.R": [ - "RingFinger1_R", - "RingFinger2_R", - "RingFinger3_R" - ], - "pinky.R": [ - "LittleFinger1_R", - "LittleFinger2_R", - "LittleFinger3_R" + "weights_authored": true, + "driven_bones": [ + "Chest", + "Head", + "Hips", + "Left ankle", + "Left arm", + "Left elbow", + "Left knee", + "Left leg", + "Left shoulder", + "Left wrist", + "Neck", + "Neck_001", + "Right ankle", + "Right arm", + "Right elbow", + "Right knee", + "Right leg", + "Right shoulder", + "Right wrist", + "Root", + "Root_002", + "Root_003", + "Root_004", + "Root_005", + "Root_006", + "Root_007", + "Spine", + "Toes0_R", + "Toes4_L", + "Unused_Noname_004", + "Unused_Noname_010" ] - }, - "chains": [ - { - "class": "bust", - "root_parent": "Chest", - "bones": [ - "Bust03_L_001", - "Bust03_L_001.seg1", - "Bust03_L_001.seg2", - "Bust03_L_001.seg3" - ], - "tips": [ - [ - -0.0, - 0.00499, - 0.0 - ], - [ - -0.0, - 0.00499, - -0.0 - ], - [ - -0.0, - 0.00499, - 0.0 - ], - [ - -0.04786, - 0.03009, - -0.00987 - ] - ], - "hulls": [ - [ - [ - 0.04633, - -0.04498, - -0.01881 - ], - [ - 0.04429, - 0.0312, - 0.0252 - ], - [ - -0.03544, - -0.03722, - -0.02393 - ], - [ - -0.01034, - -0.00311, - 0.01941 - ], - [ - 0.03922, - -0.01299, - 0.01519 - ], - [ - 0.00226, - -0.03668, - -0.00227 - ], - [ - -0.00221, - -0.04468, - -0.03252 - ], - [ - 0.02021, - 0.01297, - 0.02357 - ], - [ - -0.02483, - -0.02224, - 0.00711 - ], - [ - 0.01419, - -0.01764, - 0.01819 - ] - ], - [ - [ - -0.03304, - -0.0253, - -0.03656 - ], - [ - 0.02219, - 0.02698, - 0.02558 - ], - [ - -0.01842, - -0.01287, - 0.01309 - ], - [ - -0.03167, - -0.02281, - -0.01434 - ], - [ - 0.01486, - 0.01924, - 0.02155 - ], - [ - -0.0281, - -0.01998, - -0.00439 - ], - [ - -0.03316, - -0.0247, - -0.02601 - ], - [ - -0.02363, - -0.01653, - 0.00511 - ] - ], - [ - [ - -0.04191, - -0.02902, - -0.00775 - ], - [ - 0.02138, - 0.02973, - 0.01524 - ], - [ - -0.01001, - 0.00052, - 0.02539 - ], - [ - 0.00623, - 0.01154, - 0.02665 - ], - [ - -0.00684, - 0.00428, - 0.02012 - ], - [ - 0.00366, - 0.00932, - 0.02186 - ] - ], - [ - [ - -0.04767, - -0.0252, - -0.03253 - ], - [ - 0.01504, - 0.04595, - 0.01354 - ], - [ - -0.0166, - -0.00573, - 0.01624 - ], - [ - -0.03357, - 0.02518, - -0.02554 - ], - [ - -0.01636, - 0.03061, - 0.00722 - ], - [ - -0.04063, - -0.0104, - -0.00069 - ], - [ - -0.0493, - 0.00465, - -0.0215 - ], - [ - 0.01032, - 0.0206, - 0.01634 - ], - [ - -0.03573, - 0.01456, - 0.00058 - ], - [ - -0.00267, - 0.04461, - -0.00642 - ] - ] - ], - "neighbours": [ - { - "Bust02_L.seg3": 10.684019018217892, - "Bust03_L_001.seg1": 0.39247250024091307, - "Bust01_L_001.seg1": 4.800724986178796, - "Bust02_L": 6.635007128308069, - "Bust01_L_001": 1.3835900670950232, - "Bust02_L.seg2": 1.1628704178966145, - "Bust02_L.seg1": 1.2647811147863781, - "Bust01_L_001.seg2": 1.6922467906349197 - }, - { - "Bust03_L_001.seg2": 0.7403938854892498, - "Bust02_L.seg3": 0.5296733698751332, - "Bust03_L_001": 0.39247250024091307, - "Bust02_L": 0.6099544862060277, - "Bust01_L_001": 0.3548198778082869, - "Bust02_L.seg1": 0.051393940012049555 - }, - { - "Bust03_L_001.seg1": 0.7403938854892498, - "Bust02_L.seg3": 0.4899941747474428, - "Bust03_L_001.seg3": 1.023675833835116, - "Bust02_L": 0.3201858651144034, - "Bust01_L_001": 0.26777733789898783, - "Bust02_L.seg1": 0.008267936081229 - }, - { - "Bust02_L": 7.855898180459636, - "Bust01_L_001": 8.371310126734542, - "Bust02_L.seg2": 1.5247301951670538, - "Bust02_L.seg1": 1.282291840066493, - "Bust02_L.seg3": 7.879450940680849, - "Bust03_L_001.seg2": 1.023675833835116 - } - ] - }, - { - "class": "bust", - "root_parent": "Chest", - "bones": [ - "Bust02_L", - "Bust02_L.seg1", - "Bust02_L.seg2", - "Bust02_L.seg3" - ], - "tips": [ - [ - 0.0, - 0.00671, - 0.0 - ], - [ - 0.0, - 0.00671, - 0.0 - ], - [ - -0.0, - 0.00671, - -0.0 - ], - [ - 0.0039, - 0.01862, - 0.0891 - ] - ], - "hulls": [ - [ - [ - -0.05329, - -0.06204, - 0.01442 - ], - [ - 0.03808, - 0.05407, - -0.02505 - ], - [ - 0.04119, - -0.04707, - -0.04114 - ], - [ - -0.00491, - -0.0594, - -0.0193 - ], - [ - 0.05435, - 0.00526, - -0.03349 - ], - [ - 0.03347, - -0.01547, - -0.04407 - ], - [ - -0.03478, - -0.04433, - 0.03204 - ], - [ - -0.02065, - -0.0593, - 0.00525 - ], - [ - 0.04598, - 0.03063, - -0.04143 - ], - [ - 0.01604, - -0.0615, - -0.03614 - ] - ], - [ - [ - -0.03762, - -0.04372, - 0.03863 - ], - [ - 0.04338, - 0.03738, - -0.02103 - ], - [ - 0.04475, - 0.01715, - -0.01642 - ], - [ - 0.0376, - 0.03055, - -0.01704 - ] - ], - [ - [ - -0.04425, - -0.03467, - 0.04791 - ], - [ - 0.03636, - 0.04794, - -0.01339 - ], - [ - 0.03518, - 0.0335, - -0.00628 - ], - [ - -0.04076, - -0.04296, - 0.04344 - ], - [ - 0.04017, - 0.03788, - -0.00918 - ] - ], - [ - [ - -0.03022, - -0.01453, - 0.07085 - ], - [ - 0.03357, - 0.04759, - -0.00158 - ], - [ - 0.02215, - 0.01615, - 0.05052 - ], - [ - -0.03306, - -0.04202, - 0.04804 - ], - [ - 0.03502, - 0.02209, - 0.01952 - ], - [ - -0.00246, - 0.01072, - 0.0669 - ], - [ - 0.02175, - 0.04171, - 0.0353 - ], - [ - -0.00812, - -0.01468, - 0.05663 - ], - [ - 0.04415, - 0.02105, - -0.00302 - ], - [ - -0.04002, - -0.02482, - 0.05624 - ] - ] - ], - "neighbours": [ - { - "Bust02_R.seg1": 0.005960952082285087, - "Bust02_R": 3.915824087943572, - "Bust01_R_001": 0.31005864655387483, - "Bust02_L.seg1": 0.4461314780366022, - "Bust01_L_001": 6.613423885382499, - "Bust03_L_001.seg3": 7.855898180459636, - "Bust01_L_001.seg1": 4.352280333812645, - "Bust01_L_001.seg2": 0.8579342939684047, - "Bust03_L_001": 6.635007128308069, - "Bust03_L_001.seg2": 0.3201858651144034, - "Bust03_L_001.seg1": 0.6099544862060277 - }, - { - "Bust02_L.seg2": 0.38188457635027917, - "Bust02_R": 0.3747821533537301, - "Bust02_L": 0.4461314780366022, - "Bust01_L_001": 0.5235603986584445, - "Bust03_L_001.seg3": 1.282291840066493, - "Bust01_L_001.seg1": 0.5806351635478039, - "Bust01_L_001.seg2": 0.4524824916068204, - "Bust03_L_001.seg2": 0.008267936081229, - "Bust03_L_001.seg1": 0.051393940012049555, - "Bust03_L_001": 1.2647811147863781, - "Bust02_R.seg1": 0.05764958974084089, - "Bust01_R_001": 0.025810371362056328 - }, - { - "Bust02_L.seg3": 0.49236435356060004, - "Bust02_R": 0.41566034136591967, - "Bust02_R.seg1": 0.09971818586097242, - "Bust02_L.seg1": 0.38188457635027917, - "Bust01_L_001": 0.6073705560446581, - "Bust03_L_001.seg3": 1.5247301951670538, - "Bust01_L_001.seg1": 0.45601591565223715, - "Bust01_L_001.seg2": 0.5283447766357731, - "Bust03_L_001": 1.1628704178966145, - "Bust02_R.seg2": 0.022343147561200094, - "Bust01_R_001": 0.011205419963657398 - }, - { - "Bust02_L.seg2": 0.49236435356060004, - "Bust02_R": 0.45644401532108525, - "Bust02_R.seg1": 0.48457604242424607, - "Bust01_L_001": 6.729600693488217, - "Bust03_L_001.seg3": 7.879450940680849, - "Bust03_L_001.seg2": 0.4899941747474428, - "Bust03_L_001.seg1": 0.5296733698751332, - "Bust03_L_001": 10.684019018217892, - "Bust01_L_001.seg1": 3.965650537698678, - "Bust01_L_001.seg2": 3.4417638093538288, - "Bust02_R.seg2": 0.16906718151336297, - "Bust02_R.seg3": 0.05393812309813728, - "Bust01_R_001": 0.01809775013429321 - } - ] - }, - { - "class": "bust", - "root_parent": "Chest", - "bones": [ - "Bust01_L_001", - "Bust01_L_001.seg1", - "Bust01_L_001.seg2", - "Bust01_L_001.seg3" - ], - "tips": [ - [ - 0.0, - 0.03306, - -0.0 - ], - [ - 0.0, - 0.03306, - -0.0 - ], - [ - 0.0, - 0.03306, - -0.0 - ], - [ - 0.0, - 0.03306, - -0.0 - ] - ], - "hulls": [ - [], - [], - [], - [] - ], - "neighbours": [ - { - "Bust02_L.seg3": 6.729600693488217, - "Bust02_L.seg2": 0.6073705560446581, - "Bust02_R": 0.6135937533841633, - "Bust02_L.seg1": 0.5235603986584445, - "Bust02_L": 6.613423885382499, - "Bust03_L_001.seg3": 8.371310126734542, - "Bust01_L_001.seg1": 0.38629788478024046, - "Bust03_L_001": 1.3835900670950232, - "Bust03_L_001.seg2": 0.26777733789898783, - "Bust03_L_001.seg1": 0.3548198778082869, - "Bust02_R.seg1": 0.009288447555037216 - }, - { - "Bust02_L.seg3": 3.965650537698678, - "Bust03_L_001": 4.800724986178796, - "Bust01_L_001.seg2": 2.2278659449511196, - "Bust02_L.seg2": 0.45601591565223715, - "Bust02_L": 4.352280333812645, - "Bust02_L.seg1": 0.5806351635478039, - "Bust01_L_001": 0.38629788478024046 - }, - { - "Bust01_L_001.seg1": 2.2278659449511196, - "Bust02_L.seg2": 0.5283447766357731, - "Bust02_L": 0.8579342939684047, - "Bust02_L.seg1": 0.4524824916068204, - "Bust02_L.seg3": 3.4417638093538288, - "Bust03_L_001": 1.6922467906349197 - }, - {} - ] - }, - { - "class": "bust", - "root_parent": "Chest", - "bones": [ - "Bust03_R_001", - "Bust03_R_001.seg1", - "Bust03_R_001.seg2", - "Bust03_R_001.seg3" - ], - "tips": [ - [ - -0.0, - 0.00502, - -0.0 - ], - [ - 0.0, - 0.00502, - -0.0 - ], - [ - 0.0, - 0.00502, - 0.0 - ], - [ - 0.04626, - 0.03105, - -0.01089 - ] - ], - "hulls": [ - [ - [ - -0.0442, - -0.04569, - -0.01868 - ], - [ - -0.04566, - 0.03105, - 0.02103 - ], - [ - 0.03437, - -0.03689, - -0.02435 - ], - [ - 0.01014, - -0.00238, - 0.01961 - ], - [ - -0.03916, - -0.01332, - 0.01505 - ], - [ - -0.00154, - -0.03677, - -0.00128 - ], - [ - 0.00348, - -0.0455, - -0.03125 - ], - [ - -0.02076, - 0.01322, - 0.02295 - ], - [ - 0.02519, - -0.02186, - 0.00864 - ], - [ - -0.01409, - -0.01741, - 0.01849 - ] - ], - [ - [ - -0.03367, - 0.0381, - 0.01234 - ], - [ - 0.02844, - -0.01959, - -0.00363 - ], - [ - -0.00677, - 0.01702, - 0.02547 - ], - [ - 0.0184, - -0.01219, - 0.01353 - ], - [ - -0.02276, - 0.02786, - 0.02271 - ], - [ - -0.0156, - 0.01949, - 0.02069 - ], - [ - 0.02377, - -0.01597, - 0.00572 - ] - ], - [ - [ - 0.04198, - -0.03518, - -0.02675 - ], - [ - -0.02232, - 0.02964, - 0.01387 - ], - [ - 0.00984, - 0.0011, - 0.02547 - ], - [ - 0.04078, - -0.02864, - -0.008 - ], - [ - -0.0043, - 0.00976, - 0.02127 - ], - [ - 0.03219, - -0.02764, - -0.01345 - ], - [ - 0.03386, - -0.02981, - -0.02504 - ], - [ - 0.00631, - 0.00488, - 0.0198 - ] - ], - [ - [ - 0.04812, - -0.02642, - -0.03085 - ], - [ - -0.01636, - 0.0459, - 0.01167 - ], - [ - 0.01621, - -0.00508, - 0.01619 - ], - [ - 0.03313, - 0.02501, - -0.02622 - ], - [ - 0.0154, - 0.031, - 0.00617 - ], - [ - 0.04022, - -0.01015, - -0.00026 - ], - [ - 0.04159, - -0.00016, - -0.0305 - ], - [ - -0.01121, - 0.02073, - 0.01522 - ], - [ - 0.03515, - 0.01515, - 0.00022 - ], - [ - 0.00163, - 0.04435, - -0.00802 - ] - ] - ], - "neighbours": [ - { - "Bust02_R": 5.307509138452929, - "Bust01_R_001": 1.4136093184462317, - "Bust01_R_001.seg1": 4.873491615786004, - "Bust02_R.seg3": 11.991912180052568, - "Bust02_R.seg2": 1.2955009556263646, - "Bust02_R.seg1": 1.502616046949964, - "Bust03_R_001.seg1": 0.4782062616681064, - "Bust01_R_001.seg2": 1.7041195119539072 - }, - { - "Bust03_R_001": 0.4782062616681064, - "Bust03_R_001.seg2": 1.003560123198177, - "Bust02_R.seg3": 0.5816850788738794, - "Bust02_R": 0.3973616822628325, - "Bust02_R.seg1": 0.047119399907472825, - "Bust01_R_001": 0.309535936388295 - }, - { - "Bust03_R_001.seg3": 0.8463580589684323, - "Bust03_R_001.seg1": 1.003560123198177, - "Bust02_R.seg3": 0.5084140222547837, - "Bust01_R_001": 0.3455534938015109, - "Bust02_R": 0.5067806721295243, - "Bust02_R.seg1": 0.02246419977765446, - "Bust02_R.seg2": 0.015858820704345522 - }, - { - "Bust02_R": 10.011569689474808, - "Bust01_R_001": 8.207608152240955, - "Bust02_R.seg3": 4.943467353681944, - "Bust02_R.seg2": 1.7124571917050189, - "Bust02_R.seg1": 1.6439859359300746, - "Bust03_R_001.seg2": 0.8463580589684323 - } - ] - }, - { - "class": "bust", - "root_parent": "Chest", - "bones": [ - "Bust02_R", - "Bust02_R.seg1", - "Bust02_R.seg2", - "Bust02_R.seg3" - ], - "tips": [ - [ - -0.0, - 0.00727, - 0.0 - ], - [ - -0.0, - 0.00727, - 0.0 - ], - [ - -0.0, - 0.00727, - 0.0 - ], - [ - -0.01121, - 0.02484, - 0.08191 - ] - ], - "hulls": [ - [ - [ - 0.04401, - -0.06689, - 0.02491 - ], - [ - -0.03462, - 0.05156, - -0.04028 - ], - [ - -0.03463, - -0.04498, - -0.0439 - ], - [ - 0.00361, - -0.0687, - -0.00615 - ], - [ - -0.0368, - 0.00366, - -0.04301 - ], - [ - 0.03404, - -0.03799, - 0.04267 - ], - [ - -0.00954, - -0.05169, - -0.03263 - ], - [ - 0.02183, - -0.06161, - 0.01115 - ], - [ - -0.04391, - 0.02797, - -0.03912 - ], - [ - -0.0543, - -0.01331, - -0.03926 - ] - ], - [ - [ - -0.03205, - 0.04907, - -0.03145 - ], - [ - -0.04008, - 0.02024, - -0.02208 - ], - [ - -0.02309, - 0.04357, - -0.01934 - ], - [ - -0.03183, - 0.03314, - -0.02048 - ], - [ - -0.03919, - 0.03885, - -0.02992 - ], - [ - -0.04254, - 0.02721, - -0.02709 - ], - [ - -0.03185, - 0.02969, - -0.02512 - ] - ], - [ - [ - 0.03185, - -0.03665, - 0.05212 - ], - [ - -0.02993, - 0.04536, - -0.02259 - ], - [ - -0.03998, - 0.01677, - -0.01623 - ], - [ - -0.03089, - 0.02896, - -0.01571 - ], - [ - -0.02242, - 0.04091, - -0.0147 - ], - [ - -0.03683, - 0.0363, - -0.01945 - ] - ], - [ - [ - 0.02768, - -0.02522, - 0.07202 - ], - [ - -0.02744, - 0.04326, - -0.01054 - ], - [ - -0.01191, - 0.03169, - 0.05578 - ], - [ - -0.02916, - 0.02393, - 0.02116 - ], - [ - 0.00317, - -0.00364, - 0.05832 - ], - [ - -0.02141, - 0.05154, - 0.02865 - ], - [ - -0.03679, - 0.01737, - -0.00467 - ], - [ - 0.02152, - -0.04126, - 0.05256 - ], - [ - -0.02212, - 0.03952, - 0.00942 - ], - [ - -0.00917, - 0.01028, - 0.04994 - ] - ] - ], - "neighbours": [ - { - "Bust02_L.seg3": 0.45644401532108525, - "Bust02_L.seg2": 0.41566034136591967, - "Bust02_R.seg1": 0.4601587836668959, - "Bust02_L.seg1": 0.3747821533537301, - "Bust02_L": 3.915824087943572, - "Bust01_R_001": 7.0890085884225496, - "Bust01_L_001": 0.6135937533841633, - "Bust03_R_001.seg3": 10.011569689474808, - "Bust01_R_001.seg1": 3.203896606274064, - "Bust03_R_001": 5.307509138452929, - "Bust01_R_001.seg2": 0.2931302984772375, - "Bust03_R_001.seg2": 0.5067806721295243, - "Bust03_R_001.seg1": 0.3973616822628325 - }, - { - "Bust02_L.seg3": 0.48457604242424607, - "Bust02_L.seg2": 0.09971818586097242, - "Bust02_R": 0.4601587836668959, - "Bust02_R.seg2": 0.4511130851945411, - "Bust01_R_001": 0.6168202296418669, - "Bust02_L": 0.005960952082285087, - "Bust03_R_001.seg3": 1.6439859359300746, - "Bust01_R_001.seg1": 0.9091350649963715, - "Bust01_R_001.seg2": 0.38717467091078317, - "Bust03_R_001": 1.502616046949964, - "Bust03_R_001.seg2": 0.02246419977765446, - "Bust03_R_001.seg1": 0.047119399907472825, - "Bust02_L.seg1": 0.05764958974084089, - "Bust01_L_001": 0.009288447555037216 - }, - { - "Bust02_R.seg1": 0.4511130851945411, - "Bust01_R_001": 0.7206833667025079, - "Bust02_R.seg3": 0.4693402085530754, - "Bust03_R_001.seg3": 1.7124571917050189, - "Bust03_R_001": 1.2955009556263646, - "Bust01_R_001.seg1": 0.7197488227225909, - "Bust01_R_001.seg2": 0.48970259372865915, - "Bust02_L.seg2": 0.022343147561200094, - "Bust02_L.seg3": 0.16906718151336297, - "Bust03_R_001.seg2": 0.015858820704345522 - }, - { - "Bust03_R_001.seg3": 4.943467353681944, - "Bust01_R_001": 4.914154174777157, - "Bust02_R.seg2": 0.4693402085530754, - "Bust03_R_001": 11.991912180052568, - "Bust01_R_001.seg1": 4.62673858015464, - "Bust03_R_001.seg2": 0.5084140222547837, - "Bust03_R_001.seg1": 0.5816850788738794, - "Bust01_R_001.seg2": 4.197806614286925, - "Bust02_L.seg3": 0.05393812309813728 - } - ] - }, - { - "class": "bust", - "root_parent": "Chest", - "bones": [ - "Bust01_R_001", - "Bust01_R_001.seg1", - "Bust01_R_001.seg2", - "Bust01_R_001.seg3" - ], - "tips": [ - [ - -0.0, - 0.03325, - -0.0 - ], - [ - 0.0, - 0.03325, - -0.0 - ], - [ - 0.0, - 0.03325, - -0.0 - ], - [ - 0.0, - 0.03325, - -0.0 - ] - ], - "hulls": [ - [], - [ - [ - -0.02064, - 0.02212, - -0.01718 - ], - [ - -0.00536, - 0.02826, - 0.00773 - ], - [ - -0.01888, - 0.02651, - -0.00485 - ], - [ - -0.00745, - 0.02622, - -0.00108 - ], - [ - -0.01978, - 0.02523, - -0.00909 - ], - [ - -0.00652, - 0.02729, - 0.0031 - ] - ], - [], - [] - ], - "neighbours": [ - { - "Bust02_R.seg2": 0.7206833667025079, - "Bust02_R.seg1": 0.6168202296418669, - "Bust02_L": 0.31005864655387483, - "Bust02_R": 7.0890085884225496, - "Bust03_R_001.seg3": 8.207608152240955, - "Bust02_R.seg3": 4.914154174777157, - "Bust01_R_001.seg1": 0.38291045211652436, - "Bust03_R_001": 1.4136093184462317, - "Bust03_R_001.seg2": 0.3455534938015109, - "Bust03_R_001.seg1": 0.309535936388295, - "Bust02_L.seg1": 0.025810371362056328, - "Bust02_L.seg2": 0.011205419963657398, - "Bust02_L.seg3": 0.01809775013429321 - }, - { - "Bust02_R": 3.203896606274064, - "Bust01_R_001": 0.38291045211652436, - "Bust03_R_001": 4.873491615786004, - "Bust01_R_001.seg2": 2.315045669707698, - "Bust02_R.seg1": 0.9091350649963715, - "Bust02_R.seg3": 4.62673858015464, - "Bust02_R.seg2": 0.7197488227225909 - }, - { - "Bust01_R_001.seg1": 2.315045669707698, - "Bust02_R.seg1": 0.38717467091078317, - "Bust02_R.seg3": 4.197806614286925, - "Bust03_R_001": 1.7041195119539072, - "Bust02_R.seg2": 0.48970259372865915, - "Bust02_R": 0.2931302984772375 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "HairF_Top", - "HairFL_00", - "HairFL_01" - ], - "tips": [ - [ - 0.0, - 0.12695, - -0.0 - ], - [ - 0.0, - 0.02817, - 0.0 - ], - [ - 0.0, - 0.02817, - 0.0 - ] - ], - "hulls": [ - [ - [ - -0.0709, - 0.19701, - -0.02448 - ], - [ - -0.01219, - 0.03468, - 0.00089 - ], - [ - -0.00316, - 0.12791, - -0.02292 - ], - [ - -0.0051, - 0.07784, - -0.03084 - ], - [ - -0.02866, - 0.16772, - -0.03377 - ], - [ - -0.02224, - 0.14248, - -0.04227 - ], - [ - 0.00084, - 0.05853, - -0.01463 - ], - [ - 0.0047, - 0.10031, - -0.02643 - ], - [ - -0.04913, - 0.18521, - -0.02467 - ], - [ - -0.01094, - 0.11379, - -0.04102 - ] - ], - [], - [] - ], - "neighbours": [ - { - "HairFL_00": 0.05267075423349632 - }, - { - "HairF_Top": 0.05267075423349632 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "HairF_Top_001", - "HairFR_02_00", - "HairFR_02_01", - "HairFR_02_02" - ], - "tips": [ - [ - -0.0, - 0.20762, - -0.0 - ], - [ - -0.0, - 0.04704, - 0.0 - ], - [ - 0.0, - 0.06538, - -0.0 - ], - [ - 0.0, - 0.06538, - -0.0 - ] - ], - "hulls": [ - [ - [ - -0.00288, - 0.22283, - 0.013 - ], - [ - 0.00889, - 0.03929, - -0.01939 - ], - [ - -0.02264, - 0.13514, - -0.04835 - ], - [ - -0.02427, - 0.09846, - 0.0138 - ], - [ - 0.00019, - 0.204, - -0.05523 - ], - [ - -0.02084, - 0.15999, - 0.02109 - ], - [ - -0.01409, - 0.08541, - -0.03676 - ], - [ - -0.0253, - 0.18142, - -0.02376 - ], - [ - -0.0335, - 0.13128, - -0.0098 - ], - [ - -0.0196, - 0.06556, - -0.00155 - ] - ], - [ - [ - 0.02007, - 0.05938, - -0.04047 - ], - [ - -0.00686, - 0.02392, - 0.01915 - ], - [ - -0.00724, - 0.06091, - -0.00245 - ], - [ - 0.00043, - 0.02599, - -0.02315 - ], - [ - -0.00422, - 0.06112, - 0.02235 - ], - [ - 0.01078, - 0.05041, - -0.0206 - ], - [ - 0.01842, - 0.03651, - -0.04542 - ], - [ - -0.01235, - 0.03465, - 0.00185 - ], - [ - -0.01321, - 0.04676, - 0.01771 - ], - [ - -0.00091, - 0.03762, - -0.00943 - ] - ], - [ - [ - -0.00679, - 0.10408, - 0.0075 - ], - [ - 0.00039, - 0.01538, - -0.0133 - ], - [ - 0.01456, - 0.06402, - -0.02416 - ], - [ - -0.01345, - 0.0494, - 0.02085 - ], - [ - -0.00778, - 0.07451, - 0.00301 - ], - [ - -0.00182, - 0.04668, - -0.00654 - ], - [ - -0.01943, - 0.02489, - 0.01069 - ], - [ - 0.01626, - 0.03975, - -0.03271 - ], - [ - 0.00807, - 0.08816, - -0.01896 - ], - [ - -0.00331, - 0.02975, - -0.00083 - ] - ], - [] - ], - "neighbours": [ - { - "HairFR_02_00": 11.198780429524012, - "HairFR_02_01": 1.68590918343183, - "HairFR_02_02": 0.01914253633767224 - }, - { - "HairF_Top_001": 11.198780429524012, - "HairFR_02_01": 14.30398476346928, - "HairFR_02_02": 0.6820841702186262 - }, - { - "HairF_Top_001": 1.68590918343183, - "HairFR_02_00": 14.30398476346928, - "HairFR_02_02": 12.428229786449679 - }, - { - "HairFR_02_01": 12.428229786449679, - "HairFR_02_00": 0.6820841702186262, - "HairF_Top_001": 0.01914253633767224 - } - ] - }, - { - "class": "hair", - "root_parent": "Head", - "bones": [ - "HairF_Top_002", - "HairFR_00", - "HairFR_01", - "HairFR_02" - ], - "tips": [ - [ - -0.0, - 0.12133, - 0.0 - ], - [ - 0.0, - 0.02307, - -0.0 - ], - [ - 0.0, - 0.02981, - -0.0 - ], - [ - 0.0, - 0.02981, - -0.0 - ] - ], - "hulls": [ - [ - [ - -0.05014, - 0.12632, - -0.01759 - ], - [ - -0.03167, - 0.00333, - -0.03768 - ], - [ - -0.04825, - 0.05919, - -0.00628 - ], - [ - -0.04605, - 0.09631, - -0.00308 - ], - [ - -0.03867, - 0.02863, - -0.01278 - ], - [ - -0.056, - 0.08241, - -0.01369 - ], - [ - -0.05546, - 0.10631, - -0.01526 - ], - [ - -0.03607, - 0.0151, - -0.02392 - ], - [ - -0.04399, - 0.11512, - -0.00607 - ], - [ - -0.04627, - 0.04432, - -0.01442 - ] - ], - [ - [ - -0.03139, - 0.01458, - -0.03774 - ], - [ - -0.02316, - 0.0231, - -0.01229 - ], - [ - -0.03061, - 0.02285, - -0.02646 - ], - [ - -0.02709, - 0.01232, - -0.02493 - ], - [ - -0.02925, - 0.02335, - -0.01823 - ], - [ - -0.02453, - 0.013, - -0.03198 - ], - [ - -0.02469, - 0.01954, - -0.0246 - ], - [ - -0.03143, - 0.01883, - -0.03237 - ], - [ - -0.03219, - 0.01133, - -0.03244 - ], - [ - -0.02541, - 0.01284, - -0.03788 - ] - ], - [ - [ - -0.02626, - 0.00464, - -0.01368 - ], - [ - -0.0101, - 0.00782, - 0.00737 - ], - [ - -0.01803, - 0.00459, - -0.00296 - ], - [ - -0.02014, - 0.00414, - -0.01004 - ], - [ - -0.01431, - 0.00762, - 0.00157 - ], - [ - -0.02189, - 0.00626, - -0.00694 - ], - [ - -0.02398, - 0.00295, - -0.01635 - ], - [ - -0.01261, - 0.00713, - 0.00436 - ], - [ - -0.0208, - 0.00235, - -0.00737 - ], - [ - -0.02368, - 0.00442, - -0.00806 - ] - ], - [] - ], - "neighbours": [ - { - "HairFR_00": 6.522659186510956, - "HairFR_01": 2.4443696705936615, - "HairFR_02": 0.012604948497175639 - }, - { - "HairF_Top_002": 6.522659186510956, - "HairFR_01": 10.866074148113151, - "HairFR_02": 0.8757130731296292 - }, - { - "HairF_Top_002": 2.4443696705936615, - "HairFR_00": 10.866074148113151, - "HairFR_02": 0.9474554435243647 - }, - { - "HairFR_01": 0.9474554435243647, - "HairFR_00": 0.8757130731296292, - "HairF_Top_002": 0.012604948497175639 - } - ] - } - ], - "twist": [], - "colliders": [ - { - "bone": "Hips", - "child": "Spine", - "from": 0.0, - "radius_head": 0.128, - "radius_tail": 0.128, - "radius": 0.128, - "lid": true - }, - { - "bone": "Left leg", - "child": "Left knee", - "from": 0.1, - "radius_head": 0.1203, - "radius_tail": 0.0758, - "radius": 0.0758 - }, - { - "bone": "Right leg", - "child": "Right knee", - "from": 0.1, - "radius_head": 0.1205, - "radius_tail": 0.0756, - "radius": 0.0756 - }, - { - "bone": "Left knee", - "child": "Left ankle", - "from": 0.1, - "radius_head": 0.1305, - "radius_tail": 0.1204, - "radius": 0.1204 - }, - { - "bone": "Right knee", - "child": "Right ankle", - "from": 0.1, - "radius_head": 0.1304, - "radius_tail": 0.1204, - "radius": 0.1204 - } - ], - "weights_authored": true, - "driven_bones": [ - "Chest", - "Head", - "Hips", - "Left ankle", - "Left arm", - "Left elbow", - "Left knee", - "Left leg", - "Left shoulder", - "Left wrist", - "Neck", - "Neck_001", - "Right ankle", - "Right arm", - "Right elbow", - "Right knee", - "Right leg", - "Right shoulder", - "Right wrist", - "Root_001", - "Root_002", - "Root_003", - "Root_004", - "Root_005", - "Root_006", - "Root_007", - "Spine", - "Toes4_L", - "Toes4_R", - "Unused_Noname_004", - "Unused_Noname_010" - ], - "surfaces": [ - { - "mesh": "Object_262", - "surface": 0, - "material": "Face", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 9620, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_263", - "surface": 0, - "material": "Clothes", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 12846, - "textured": true, - "chain_share": { - "bust": 0.059 - } - }, - { - "mesh": "Object_264", - "surface": 0, - "material": "Hair_A", - "class": "hair", - "detail": "hair", - "why": "material name says 'hair'", - "verts": 15552, - "textured": true, - "chain_share": { - "hair": 0.018 - } - }, - { - "mesh": "Object_265", - "surface": 0, - "material": "Hair_B", - "class": "hair", - "detail": "hair", - "why": "material name says 'hair'", - "verts": 5281, - "textured": true, - "chain_share": { - "hair": 0.094 - } - }, - { - "mesh": "Object_266", - "surface": 0, - "material": "Iris", - "class": "body", - "detail": "eyes", - "why": "material name says 'eyes'", - "verts": 260, - "textured": true, - "chain_share": {} - } - ] } \ No newline at end of file diff --git a/assets/characters/skins/skins.json b/assets/characters/skins/skins.json index 4029775..e3b4eb6 100644 --- a/assets/characters/skins/skins.json +++ b/assets/characters/skins/skins.json @@ -5,6 +5,15 @@ "name": "Miku", "description": "Hatsune Miku \u2014 Virtual Idol", "model": "res://assets/characters/skins/miku.glb", + "viewmodel": { + "sleeve": "#168F98", + "accent": "#252632", + "skin": "#F5CDBB", + "glove": "#20212C", + "nail": "#20A9B1", + "hand_style": "fingerless", + "sleeve_ratio": 0.78 + }, "unlocked": true }, { @@ -12,6 +21,15 @@ "name": "Taila", "description": "", "model": "res://assets/characters/skins/taila.glb", + "viewmodel": { + "sleeve": "#171D32", + "accent": "#E8772D", + "skin": "#EFC6B7", + "glove": "#151826", + "nail": "#E8772D", + "hand_style": "glove", + "sleeve_ratio": 0.78 + }, "unlocked": true }, { @@ -19,6 +37,15 @@ "name": "Mannequin", "description": "Quaternius reference mannequin (CC0)", "model": "res://assets/characters/skins/mannequin.glb", + "viewmodel": { + "sleeve": "#D9692D", + "accent": "#6D4C9B", + "skin": "#D9692D", + "glove": "#6D4C9B", + "nail": "#F0A14B", + "hand_style": "android", + "sleeve_ratio": 0.84 + }, "unlocked": true }, { @@ -26,6 +53,15 @@ "name": "Kiyoko", "description": "Kiyoko School Girl \u2014 VRoid (CC-BY, Kasujin)", "model": "res://assets/characters/skins/kiyoko.glb", + "viewmodel": { + "sleeve": "#ECE7E3", + "accent": "#253151", + "skin": "#F0C6B2", + "glove": "#253151", + "nail": "#E9A9B4", + "hand_style": "bare", + "sleeve_ratio": 0.32 + }, "unlocked": true }, { @@ -33,6 +69,15 @@ "name": "Aria", "description": "Anime Girl Rigged \u2014 VRoid (CC-BY, dequeijospizza)", "model": "res://assets/characters/skins/aria.glb", + "viewmodel": { + "sleeve": "#E8E8E5", + "accent": "#27252D", + "skin": "#EFC7B7", + "glove": "#27252D", + "nail": "#DFA0AE", + "hand_style": "bare", + "sleeve_ratio": 0.30 + }, "unlocked": true }, { @@ -40,7 +85,16 @@ "name": "Momo", "description": "Momo Ayase, DANDADAN (CC-BY, HiGuys920)", "model": "res://assets/characters/skins/momo.glb", + "viewmodel": { + "sleeve": "#D66F98", + "accent": "#34303A", + "skin": "#F0C5B2", + "glove": "#34303A", + "nail": "#D66F98", + "hand_style": "fingerless", + "sleeve_ratio": 0.80 + }, "unlocked": true } ] -} \ No newline at end of file +} diff --git a/assets/characters/skins/taila.glb b/assets/characters/skins/taila.glb index 49d45d1..4909804 100644 Binary files a/assets/characters/skins/taila.glb and b/assets/characters/skins/taila.glb differ diff --git a/assets/characters/skins/taila.rig.json b/assets/characters/skins/taila.rig.json index 077b535..c9c8886 100644 --- a/assets/characters/skins/taila.rig.json +++ b/assets/characters/skins/taila.rig.json @@ -1,8232 +1,9767 @@ { - "roles": { - "hips": "DEF-spine", - "head": "DEF-spine.006", - "neck": "DEF-spine.005", - "spine": [ - "DEF-spine.001", - "DEF-spine.002", - "DEF-spine.003", - "DEF-spine.004", - "DEF-spine.005", - "DEF-spine.006" + "roles": { + "hips": "DEF-spine", + "head": "DEF-spine.006", + "neck": "DEF-spine.005", + "spine": [ + "DEF-spine.001", + "DEF-spine.002", + "DEF-spine.003", + "DEF-spine.004", + "DEF-spine.005", + "DEF-spine.006" + ], + "shoulder.L": "DEF-shoulder.L", + "shoulder.R": "DEF-shoulder.R", + "upper_arm.L": "DEF-upper_arm.L", + "upper_arm.R": "DEF-upper_arm.R", + "forearm.R": "DEF-forearm.R", + "forearm.L": "DEF-forearm.L", + "thigh.R": "DEF-thigh.R", + "thigh.L": "DEF-thigh.L", + "foot.L": "DEF-foot.L", + "foot.R": "DEF-foot.R", + "shin.R": "DEF-shin.R", + "shin.L": "DEF-shin.L", + "hand.R": "DEF-hand.R", + "hand.L": "DEF-hand.L", + "toe.R": "DEF-toe.R", + "toe.L": "DEF-toe.L" + }, + "fingers": { + "thumb.L": [ + "DEF-thumb.01.L", + "DEF-thumb.02.L", + "DEF-thumb.03.L" + ], + "index.L": [ + "DEF-f_index.01.L", + "DEF-f_index.02.L", + "DEF-f_index.03.L" + ], + "middle.L": [ + "DEF-f_middle.01.L", + "DEF-f_middle.02.L", + "DEF-f_middle.03.L" + ], + "ring.L": [ + "DEF-f_ring.01.L", + "DEF-f_ring.02.L", + "DEF-f_ring.03.L" + ], + "pinky.L": [ + "DEF-f_pinky.01.L", + "DEF-f_pinky.02.L", + "DEF-f_pinky.03.L" + ], + "thumb.R": [ + "DEF-thumb.01.R", + "DEF-thumb.02.R", + "DEF-thumb.03.R" + ], + "index.R": [ + "DEF-f_index.01.R", + "DEF-f_index.02.R", + "DEF-f_index.03.R" + ], + "middle.R": [ + "DEF-f_middle.01.R", + "DEF-f_middle.02.R", + "DEF-f_middle.03.R" + ], + "ring.R": [ + "DEF-f_ring.01.R", + "DEF-f_ring.02.R", + "DEF-f_ring.03.R" + ], + "pinky.R": [ + "DEF-f_pinky.01.R", + "DEF-f_pinky.02.R", + "DEF-f_pinky.03.R" + ] + }, + "chains": [ + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair", + "DEF-hair.001", + "DEF-hair.002" + ], + "tips": [ + [ + -0.0, + 0.06838, + -0.0 + ], + [ + 0.0, + 0.04506, + -0.0 + ], + [ + -0.01032, + 0.00853, + 0.01183 + ] + ], + "hulls": [ + [ + [ + 0.01674, + 0.1029, + 0.02973 + ], + [ + 0.03451, + 0.03231, + 0.00016 + ], + [ + -0.01716, + 0.05337, + -0.00299 + ], + [ + 0.03264, + 0.08202, + 0.01591 + ], + [ + 0.00803, + 0.0577, + 0.0108 + ], + [ + 0.03992, + 0.05757, + 0.00882 + ], + [ + 0.02314, + 0.05921, + 0.0009 + ], + [ + -0.00304, + 0.05285, + 0.00139 + ], + [ + 0.01035, + 0.06042, + -0.00529 + ], + [ + 0.01928, + 0.07988, + 0.01229 + ] + ], + [ + [ + -0.00899, + 0.04442, + -0.00085 + ], + [ + -0.02404, + 0.01035, + 0.01139 + ], + [ + 0.01712, + 0.01993, + 0.005 + ], + [ + -0.00514, + 0.01451, + -0.00241 + ], + [ + 0.00984, + 0.03774, + -0.00153 + ], + [ + 0.00415, + 0.01792, + 0.0089 + ], + [ + -0.01645, + 0.0083, + -0.00014 + ], + [ + 0.00046, + 0.03505, + 0.00649 + ], + [ + -0.01208, + 0.00937, + 0.01203 + ], + [ + -0.00743, + 0.03312, + -0.00319 + ] + ], + [ + [ + -0.01394, + 0.02572, + 0.00011 + ], + [ + -0.0018, + -0.01608, + 0.01364 + ], + [ + 0.00391, + 0.0038, + -0.00163 + ], + [ + -0.01374, + 0.00288, + 0.0104 + ], + [ + 0.00701, + 0.02313, + 0.00607 + ], + [ + -0.02015, + -0.0111, + 0.01284 + ], + [ + -0.00695, + 0.01233, + 0.00213 + ], + [ + -0.00142, + -0.00478, + 0.00609 + ], + [ + -0.00975, + -0.01068, + 0.01879 + ], + [ + 5e-05, + 0.01018, + 0.00929 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.001": 6.444893577923472, + "DEF-hair.007": 2.1405754241556942, + "DEF-hair.004": 4.305379067287163, + "DEF-hair.003": 3.7377944662223026, + "DEF-hair.002": 2.8528797830108568 + }, + { + "DEF-hair.002": 10.932883844376821, + "DEF-hair": 6.444893577923472, + "DEF-hair.004": 0.563517680316123, + "DEF-hair.003": 0.15824819222541398, + "DEF-hair.007": 0.3355432027270542 + }, + { + "DEF-hair.001": 10.932883844376821, + "DEF-hair": 2.8528797830108568, + "DEF-hair.007": 0.03886162555068284 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.003", + "DEF-hair.004" + ], + "tips": [ + [ + 0.0, + 0.06123, + 0.0 + ], + [ + -0.00017, + 0.04438, + -0.00029 + ] + ], + "hulls": [ + [ + [ + 0.00113, + 0.06235, + -0.00227 + ], + [ + 0.00701, + 0.02584, + -0.00787 + ], + [ + -0.00614, + 0.04412, + -0.00738 + ], + [ + 0.00784, + 0.04471, + 0.00317 + ], + [ + 0.00988, + 0.02683, + 0.003 + ], + [ + 0.00477, + 0.04586, + -0.00738 + ], + [ + 0.00445, + 0.05938, + 0.00744 + ], + [ + -0.00921, + 0.05971, + -0.00256 + ], + [ + 0.00043, + 0.02595, + 0.0005 + ], + [ + -0.00134, + 0.04361, + 0.00076 + ] + ], + [ + [ + 0.00022, + 0.03937, + -0.00024 + ], + [ + 0.00511, + 0.01786, + 0.00141 + ], + [ + -0.00567, + 0.01832, + -0.00298 + ], + [ + -0.00236, + 0.01841, + 0.00283 + ], + [ + 0.00218, + 0.01769, + -0.00326 + ], + [ + -0.00314, + 0.01799, + -0.00474 + ], + [ + 0.00415, + 0.01817, + 0.00434 + ], + [ + -0.00465, + 0.01808, + -0.00268 + ], + [ + -0.00301, + 0.01789, + -0.00366 + ], + [ + 0.00404, + 0.01778, + 0.0016 + ] + ] + ], + "neighbours": [ + { + "DEF-hair": 3.7377944662223026, + "DEF-hair.004": 0.48107029989034844, + "DEF-hair.001": 0.15824819222541398 + }, + { + "DEF-hair": 4.305379067287163, + "DEF-hair.001": 0.563517680316123, + "DEF-hair.003": 0.48107029989034844 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.007", + "DEF-hair.005" + ], + "tips": [ + [ + 0.0, + 0.04186, + -0.0 + ], + [ + 0.00621, + 0.02143, + 0.00318 + ] + ], + "hulls": [ + [ + [ + -0.02112, + 0.04918, + 0.02422 + ], + [ + 0.00107, + -0.01033, + 0.00095 + ], + [ + -0.00347, + 0.03222, + -0.00397 + ], + [ + -0.00948, + 0.01364, + 0.01086 + ], + [ + 0.01133, + 0.01422, + -0.00041 + ], + [ + -0.00915, + 0.03367, + 0.01549 + ], + [ + -0.0059, + -0.00789, + 0.01565 + ], + [ + -0.00287, + 0.01257, + -0.00376 + ], + [ + 0.00363, + 0.03034, + 0.00838 + ], + [ + 0.00801, + -0.00529, + 0.01351 + ] + ], + [ + [ + -0.00018, + 0.03655, + -0.00043 + ], + [ + 0.01031, + -0.00915, + -4e-05 + ], + [ + -0.00729, + 0.01104, + 0.00396 + ], + [ + 0.0085, + 0.01128, + -0.00185 + ], + [ + 0.00311, + 0.01248, + 0.00607 + ], + [ + -0.00257, + 0.00994, + -0.00246 + ], + [ + 0.01434, + -0.00662, + 0.00254 + ], + [ + -0.00639, + 0.0121, + 0.00782 + ], + [ + 0.00527, + 0.01035, + -0.00419 + ], + [ + -0.00572, + 0.01172, + 0.00704 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.005": 3.2375641098089627, + "DEF-hair": 2.1405754241556942, + "DEF-hair.001": 0.3355432027270542, + "DEF-hair.002": 0.03886162555068284 + }, + { + "DEF-hair.007": 3.2375641098089627 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.008", + "DEF-hair.009", + "DEF-hair.010", + "DEF-hair.011" + ], + "tips": [ + [ + -0.0, + 0.08774, + 0.0 + ], + [ + -0.0, + 0.05443, + -0.0 + ], + [ + -0.0, + 0.03074, + 0.0 + ], + [ + -0.00258, + 0.02087, + 0.00038 + ] + ], + "hulls": [ + [ + [ + 0.02765, + 0.09488, + -0.00831 + ], + [ + 0.01219, + -0.00529, + -0.01352 + ], + [ + -0.00966, + 0.04947, + 0.0296 + ], + [ + 0.03232, + 0.03961, + -0.02464 + ], + [ + -0.00344, + 0.06915, + -0.00733 + ], + [ + 0.01392, + 0.01818, + 0.01576 + ], + [ + -0.01562, + 0.08459, + 0.02502 + ], + [ + 0.01659, + 0.07392, + 0.02003 + ], + [ + 0.02263, + 0.06867, + -0.02538 + ], + [ + -0.00898, + 0.01997, + -0.0017 + ] + ], + [ + [ + 0.03852, + 0.0474, + -0.02383 + ], + [ + -0.01561, + 0.01392, + 0.02178 + ], + [ + -0.01735, + 0.05484, + -0.01293 + ], + [ + 0.01606, + 0.01503, + -0.0077 + ], + [ + 0.00483, + 0.03686, + 0.01325 + ], + [ + 0.01213, + 0.05777, + -0.01159 + ], + [ + -0.00464, + 0.03242, + -0.01137 + ], + [ + -0.01895, + 0.0337, + 0.00761 + ], + [ + -0.01409, + 0.06011, + 0.00746 + ], + [ + 0.01101, + 0.01666, + 0.01596 + ] + ], + [ + [ + -0.01869, + 0.01611, + 0.00916 + ], + [ + -0.00763, + 0.01686, + -0.01105 + ], + [ + -0.0002, + 0.01687, + 0.01034 + ], + [ + -0.01846, + 0.0161, + 0.00208 + ], + [ + -0.00718, + 0.01676, + -0.01006 + ], + [ + -0.01743, + 0.01605, + 0.00244 + ], + [ + -0.01785, + 0.01605, + 0.00846 + ], + [ + -0.0005, + 0.0168, + 0.00929 + ] + ], + [ + [ + 0.00088, + 0.03163, + -0.00099 + ], + [ + -0.01318, + 0.00392, + 0.00628 + ], + [ + 0.01083, + 0.00678, + -0.00118 + ], + [ + -0.00504, + 0.00595, + -0.00867 + ], + [ + 0.00037, + 0.00517, + 0.00747 + ], + [ + 0.00756, + 0.0066, + -0.00563 + ], + [ + -0.01301, + 0.00416, + 0.00091 + ], + [ + -0.01235, + 0.00379, + 0.00558 + ], + [ + 0.00012, + 0.00504, + 0.00641 + ], + [ + 0.00708, + 0.00616, + -0.00476 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.009": 5.061211017329557, + "DEF-hair.L.008": 0.10078104744782834, + "DEF-hair.R.012": 9.298894315948552 + }, + { + "DEF-hair.010": 2.072245624944944, + "DEF-hair.008": 5.061211017329557 + }, + { + "DEF-hair.009": 2.072245624944944 + }, + {} + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.L.008", + "DEF-hair.L.009", + "DEF-hair.L.010", + "DEF-hair.L.011" + ], + "tips": [ + [ + -0.0, + 0.15518, + 0.0 + ], + [ + 0.0, + 0.1139, + -0.0 + ], + [ + 0.0, + 0.0683, + -0.0 + ], + [ + -0.00183, + 0.09275, + -0.00073 + ] + ], + "hulls": [ + [ + [ + -0.0201, + 0.12899, + -0.03061 + ], + [ + -0.02432, + 0.079, + 0.02303 + ], + [ + -0.02055, + 0.07496, + -0.03044 + ], + [ + -0.01427, + 0.12502, + 0.01645 + ], + [ + -0.00353, + 0.07731, + -0.00349 + ], + [ + -0.03306, + 0.07625, + -0.00346 + ], + [ + -0.03184, + 0.12798, + -0.00534 + ], + [ + -0.00435, + 0.12595, + -0.00541 + ], + [ + -0.01423, + 0.07892, + 0.01983 + ], + [ + -0.01092, + 0.07553, + -0.02619 + ] + ], + [ + [ + -0.01584, + 0.11898, + 0.02156 + ], + [ + -0.01936, + 0.01511, + -0.02701 + ], + [ + -0.01421, + 0.07061, + -0.01786 + ], + [ + -0.02173, + 0.01636, + 0.01885 + ], + [ + -0.01498, + 0.11514, + -0.01949 + ], + [ + -0.02381, + 0.06925, + 0.0219 + ], + [ + -0.00428, + 0.01801, + -0.00334 + ], + [ + -0.02958, + 0.01445, + -0.00452 + ], + [ + -0.03155, + 0.07078, + -0.00053 + ], + [ + -0.02325, + 0.1195, + 0.00065 + ] + ], + [ + [ + -0.00944, + 0.07223, + 0.01619 + ], + [ + -0.00096, + -0.00272, + 0.00095 + ], + [ + -0.02097, + 0.02867, + 0.02652 + ], + [ + -0.02262, + 0.03074, + -0.00957 + ], + [ + -0.0118, + 0.06831, + -0.00751 + ], + [ + -0.00862, + 0.02933, + 0.0079 + ], + [ + -0.029, + 0.02986, + 0.00881 + ], + [ + -0.01507, + 0.07231, + 0.00445 + ], + [ + -0.01532, + 0.03041, + -0.00731 + ], + [ + -0.01399, + 0.02864, + 0.02361 + ] + ], + [ + [ + -0.00073, + 0.05526, + -0.00107 + ], + [ + -0.00219, + -0.00378, + 0.00364 + ], + [ + -0.00086, + 0.05418, + -0.00098 + ], + [ + -0.00111, + -0.00363, + 0.00361 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.008": 0.10078104744782834, + "DEF-hair.L": 0.0793055946615831, + "DEF-hair.L.009": 1.529110640133334, + "DEF-hair.L.012": 3.1007271781263652, + "DEF-hair.L.013": 0.021517533900501484 + }, + { + "DEF-hair.L.010": 2.697011560446442, + "DEF-hair.L.008": 1.529110640133334 + }, + { + "DEF-hair.L.011": 3.3977685462242633, + "DEF-hair.L.009": 2.697011560446442 + }, + { + "DEF-hair.L.010": 3.3977685462242633 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.L.012", + "DEF-hair.L.013", + "DEF-hair.L.014" + ], + "tips": [ + [ + -0.0, + 0.105, + -0.0 + ], + [ + 0.0, + 0.05073, + -0.0 + ], + [ + -0.0067, + 0.04095, + 0.00485 + ] + ], + "hulls": [ + [ + [ + -0.02527, + 0.09, + 0.01221 + ], + [ + -0.01623, + 0.01641, + -0.012 + ], + [ + -0.00056, + 0.05121, + 0.0168 + ], + [ + -0.02063, + 0.05216, + -0.01658 + ], + [ + -0.01205, + 0.08679, + -0.0162 + ], + [ + -0.00573, + 0.08579, + 0.01506 + ], + [ + -0.00293, + 0.05454, + -0.00049 + ], + [ + -0.00286, + 0.0856, + -0.00186 + ], + [ + -0.01003, + 0.06304, + 0.01981 + ], + [ + -0.00815, + 0.01761, + -0.01073 + ] + ], + [ + [ + -0.01737, + 0.05399, + 0.00728 + ], + [ + -0.01851, + 0.00751, + -0.01353 + ], + [ + -0.01059, + 0.00565, + 0.02048 + ], + [ + -0.02089, + 0.03301, + -0.00948 + ], + [ + -0.01369, + 0.03095, + 0.02149 + ], + [ + -0.00248, + 0.00933, + 0.00082 + ], + [ + -0.00592, + 0.03143, + 0.00333 + ], + [ + -0.02148, + 0.00584, + 0.00598 + ], + [ + -0.02351, + 0.03225, + 0.00747 + ], + [ + -0.01105, + 0.04818, + -0.00687 + ] + ], + [ + [ + 0.00167, + 0.03852, + -0.0 + ], + [ + -0.00624, + 0.00804, + 0.01374 + ], + [ + -0.01075, + 0.00706, + -0.00464 + ], + [ + -0.00133, + 0.00698, + 0.00283 + ], + [ + -0.01224, + 0.00774, + 0.00546 + ], + [ + -0.00636, + 0.00686, + -0.00431 + ], + [ + -0.0025, + 0.0077, + 0.01144 + ], + [ + -0.00688, + 0.0067, + -0.00336 + ], + [ + -0.00238, + 0.00686, + 0.00309 + ], + [ + -0.01001, + 0.00687, + -0.00385 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.L.008": 3.1007271781263652, + "DEF-hair.L.013": 2.0607935679774254, + "DEF-hair.L.018": 5.976813851190336, + "DEF-hair.L.025": 0.040129953350502845 + }, + { + "DEF-hair.L.014": 3.4327523639141315, + "DEF-hair.L.012": 2.0607935679774254, + "DEF-hair.L.008": 0.021517533900501484 + }, + { + "DEF-hair.L.013": 3.4327523639141315 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.L.018", + "DEF-hair.L.019", + "DEF-hair.L.020", + "DEF-hair.L.021" + ], + "tips": [ + [ + -0.0, + 0.06312, + 0.0 + ], + [ + -0.0, + 0.05056, + -0.0 + ], + [ + 0.0, + 0.02033, + -0.0 + ], + [ + 0.00479, + 0.02461, + -0.00168 + ] + ], + "hulls": [ + [ + [ + -0.02612, + 0.14192, + -0.003 + ], + [ + -0.02929, + -0.03494, + 7e-05 + ], + [ + 0.00844, + 0.05411, + 0.0319 + ], + [ + -0.01794, + 0.08141, + -0.01905 + ], + [ + -0.0142, + 0.01826, + -0.00923 + ], + [ + -0.00185, + 0.10667, + 0.02716 + ], + [ + 0.01647, + 0.12273, + -0.02448 + ], + [ + -0.00038, + -0.01379, + 0.03151 + ], + [ + 0.01097, + 0.01946, + 0.02163 + ], + [ + 0.0037, + 0.04787, + -0.003 + ] + ], + [ + [ + -0.02288, + 0.05078, + -0.0256 + ], + [ + -0.00915, + -0.01232, + 0.01282 + ], + [ + 0.01774, + 0.04282, + 0.01727 + ], + [ + -0.00945, + 0.0198, + -0.00824 + ], + [ + 0.01434, + 0.00964, + 0.01206 + ], + [ + -0.00794, + 0.0451, + -0.00021 + ], + [ + -0.00348, + 0.01658, + 0.01378 + ], + [ + 0.00623, + 0.05727, + 0.00083 + ], + [ + -0.01301, + 0.04025, + -0.01727 + ], + [ + 0.01291, + 0.04211, + -0.00045 + ] + ], + [ + [ + 0.01475, + 0.03544, + 0.01055 + ], + [ + 0.00952, + -0.00352, + -0.00197 + ], + [ + 0.02579, + 0.01218, + 0.01849 + ], + [ + 0.00063, + 0.01187, + 0.00239 + ], + [ + 0.02179, + 0.00272, + 0.0051 + ], + [ + 0.0095, + 0.02439, + 0.00123 + ], + [ + 0.02215, + 0.02327, + 0.01003 + ], + [ + 0.02259, + -0.0023, + 0.01636 + ], + [ + 0.0127, + 0.01015, + 0.00255 + ], + [ + 0.01403, + 0.00342, + 0.01365 + ] + ], + [ + [ + -0.0004, + 0.02115, + -0.00031 + ], + [ + 0.00701, + 0.01093, + 0.00114 + ], + [ + -0.00092, + 0.00992, + -0.00416 + ], + [ + 0.00074, + 0.00975, + 0.00111 + ], + [ + 0.00551, + 0.01082, + -0.00311 + ], + [ + 0.00155, + 0.01032, + -0.00522 + ], + [ + 0.00567, + 0.01064, + 0.0034 + ], + [ + 0.00474, + 0.01042, + -0.00246 + ], + [ + 0.00147, + 0.00995, + -0.0042 + ], + [ + 0.00154, + 0.00977, + 0.00037 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.L.019": 30.077657467988345, + "DEF-hair.L.012": 5.976813851190336 + }, + { + "DEF-hair.L.018": 30.077657467988345, + "DEF-hair.L.020": 8.790750536997786 + }, + { + "DEF-hair.L.019": 8.790750536997786, + "DEF-hair.L.021": 1.3119401760714409 + }, + { + "DEF-hair.L.020": 1.3119401760714409 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.L.022", + "DEF-hair.L.023", + "DEF-hair.L.024", + "DEF-hair.L.025" + ], + "tips": [ + [ + 0.0, + 0.06757, + 0.0 + ], + [ + 0.0, + 0.05187, + -0.0 + ], + [ + -0.0, + 0.02554, + 0.0 + ], + [ + -0.00718, + 0.05819, + -0.00047 + ] + ], + "hulls": [ + [ + [ + -0.0171, + 0.05533, + -0.00825 + ], + [ + 0.00085, + 0.05042, + 0.01568 + ], + [ + -0.01647, + 0.05217, + 0.00964 + ], + [ + 0.00018, + 0.05293, + -0.00219 + ], + [ + 0.0041, + 0.05091, + 0.01062 + ], + [ + -0.01131, + 0.05508, + -0.00972 + ], + [ + -0.00071, + 0.0528, + -0.00157 + ], + [ + -0.01626, + 0.05521, + -0.00756 + ], + [ + 0.00057, + 0.05054, + 0.01463 + ], + [ + -0.01553, + 0.05226, + 0.0091 + ] + ], + [ + [ + -0.00961, + 0.04836, + -0.01404 + ], + [ + -0.01303, + 0.00389, + -0.01295 + ], + [ + -0.00493, + 0.01951, + 0.00843 + ], + [ + -0.00136, + 0.02665, + -0.01764 + ], + [ + -0.0161, + 0.03114, + -0.01071 + ], + [ + -0.01843, + 0.01303, + 0.00013 + ], + [ + 0.00438, + 0.04454, + -0.00581 + ], + [ + 0.00052, + 0.0112, + -0.00442 + ], + [ + -0.00752, + 0.03146, + 0.00147 + ], + [ + 0.00152, + 0.04372, + -0.02001 + ] + ], + [ + [ + -0.00381, + 0.02522, + -0.00718 + ], + [ + -0.00559, + 0.0012, + -0.00894 + ], + [ + 0.00224, + 0.00661, + -0.00227 + ], + [ + -0.00973, + 0.02269, + 0.00034 + ], + [ + 0.00214, + 0.00447, + -0.01281 + ], + [ + -0.00643, + 0.00293, + 3e-05 + ], + [ + -0.00344, + 0.0247, + 3e-05 + ], + [ + -0.00842, + 0.0234, + -0.00509 + ], + [ + 0.00399, + 0.00592, + -0.00951 + ], + [ + -0.00327, + 0.00453, + 0.00141 + ] + ], + [ + [ + -0.00355, + 0.02964, + -0.00022 + ], + [ + -0.00363, + 0.02855, + -0.00025 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.L.023": 1.3606538529242842 + }, + { + "DEF-hair.L.022": 1.3606538529242842, + "DEF-hair.L.024": 1.7455139142072973 + }, + { + "DEF-hair.L.025": 1.9640462828626353, + "DEF-hair.L.023": 1.7455139142072973 + }, + { + "DEF-hair.L.012": 0.040129953350502845, + "DEF-hair.L.024": 1.9640462828626353 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.R.008", + "DEF-hair.R.009", + "DEF-hair.R.010", + "DEF-hair.R.011" + ], + "tips": [ + [ + 0.0, + 0.15518, + -0.0 + ], + [ + -0.0, + 0.1139, + -0.0 + ], + [ + 0.0, + 0.0683, + 0.0 + ], + [ + 0.00251, + -0.00932, + 0.00663 + ] + ], + "hulls": [ + [ + [ + 0.02108, + 0.12854, + -0.03051 + ], + [ + 0.05777, + 0.00779, + -0.02551 + ], + [ + 0.0253, + 0.07855, + 0.02313 + ], + [ + 0.10638, + 0.04973, + -0.03343 + ], + [ + 0.02152, + 0.07451, + -0.03034 + ], + [ + 0.01525, + 0.12457, + 0.01655 + ], + [ + 0.07177, + 0.03723, + -0.00026 + ], + [ + 0.02707, + 0.01361, + -0.01575 + ], + [ + 0.00451, + 0.07686, + -0.00339 + ], + [ + 0.08192, + 0.03253, + -0.03457 + ] + ], + [ + [ + 0.01656, + 0.11824, + 0.02184 + ], + [ + 0.02009, + 0.01436, + -0.02673 + ], + [ + 0.01493, + 0.06986, + -0.01758 + ], + [ + 0.02245, + 0.01562, + 0.01914 + ], + [ + 0.01571, + 0.11439, + -0.0192 + ], + [ + 0.02454, + 0.0685, + 0.02218 + ], + [ + 0.00501, + 0.01726, + -0.00305 + ], + [ + 0.0303, + 0.01371, + -0.00423 + ], + [ + 0.03227, + 0.07003, + -0.00025 + ], + [ + 0.02397, + 0.11875, + 0.00093 + ] + ], + [ + [ + -0.02348, + 0.11573, + -0.01225 + ], + [ + 0.00105, + -0.00379, + 0.00086 + ], + [ + 0.00952, + 0.07116, + 0.0161 + ], + [ + 0.02106, + 0.0276, + 0.02643 + ], + [ + 0.02271, + 0.02967, + -0.00967 + ], + [ + 0.01189, + 0.06724, + -0.00761 + ], + [ + 0.00871, + 0.02826, + 0.0078 + ], + [ + 0.02909, + 0.02879, + 0.00872 + ], + [ + 0.01515, + 0.07124, + 0.00436 + ], + [ + 0.0154, + 0.02934, + -0.0074 + ] + ], + [] + ], + "neighbours": [ + { + "DEF-hair.R.005": 1.3949823102104308, + "DEF-hair.R": 2.6819266073154084, + "DEF-hair.R.006": 0.08180381314025653, + "DEF-hair.R.009": 5.783332180253737, + "DEF-hair.R.012": 9.98357430229115, + "DEF-hair.R.013": 0.03451450992651994 + }, + { + "DEF-hair.R.010": 2.9875086680951517, + "DEF-hair.R.008": 5.783332180253737 + }, + { + "DEF-hair.R.011": 3.974891197996893, + "DEF-hair.R.009": 2.9875086680951517 + }, + { + "DEF-hair.R.010": 3.974891197996893 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.006", + "bones": [ + "DEF-hair.R.012", + "DEF-hair.R.013", + "DEF-hair.R.014" + ], + "tips": [ + [ + 0.0, + 0.105, + 0.0 + ], + [ + -0.0, + 0.05073, + 0.0 + ], + [ + 0.0059, + 0.03897, + 0.00457 + ] + ], + "hulls": [ + [ + [ + 0.02629, + 0.08967, + 0.01231 + ], + [ + 0.02854, + 0.01432, + -0.02129 + ], + [ + 0.00159, + 0.05089, + 0.0169 + ], + [ + 0.02154, + 0.04587, + -0.04861 + ], + [ + 0.01367, + 0.05223, + -0.01523 + ], + [ + 0.01307, + 0.08646, + -0.01609 + ], + [ + 0.03538, + 0.029, + -0.00254 + ], + [ + 0.00917, + 0.01728, + -0.01063 + ], + [ + 0.03284, + 0.03419, + -0.02689 + ], + [ + 0.00675, + 0.08546, + 0.01516 + ] + ], + [ + [ + 0.0181, + 0.0532, + 0.00736 + ], + [ + 0.01924, + 0.00672, + -0.01345 + ], + [ + 0.01131, + 0.00486, + 0.02056 + ], + [ + 0.02161, + 0.03222, + -0.0094 + ], + [ + 0.01441, + 0.03016, + 0.02157 + ], + [ + 0.00321, + 0.00854, + 0.0009 + ], + [ + 0.00665, + 0.03064, + 0.00341 + ], + [ + 0.02221, + 0.00505, + 0.00606 + ], + [ + 0.02424, + 0.03146, + 0.00755 + ], + [ + 0.01178, + 0.04739, + -0.00679 + ] + ], + [ + [ + -0.00207, + 0.03753, + -0.00014 + ], + [ + 0.00584, + 0.00705, + 0.0136 + ], + [ + 0.01035, + 0.00607, + -0.00478 + ], + [ + 0.00093, + 0.00599, + 0.00268 + ], + [ + 0.01184, + 0.00675, + 0.00532 + ], + [ + 0.00596, + 0.00587, + -0.00445 + ], + [ + 0.0021, + 0.00671, + 0.0113 + ], + [ + 0.00198, + 0.00587, + 0.00295 + ], + [ + 0.00962, + 0.00588, + -0.00399 + ], + [ + 0.00555, + 0.00676, + 0.01258 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.R.013": 2.196162649285034, + "DEF-hair.R.008": 9.98357430229115, + "DEF-hair.008": 9.298894315948552 + }, + { + "DEF-hair.R.014": 4.241960469519606, + "DEF-hair.R.012": 2.196162649285034, + "DEF-hair.R.008": 0.03451450992651994 + }, + { + "DEF-hair.R.013": 4.241960469519606 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.004", + "bones": [ + "DEF-hair.L", + "DEF-hair.L.001", + "DEF-hair.L.002", + "DEF-hair.L.003" + ], + "tips": [ + [ + 0.0, + 0.11761, + 0.0 + ], + [ + 0.0, + 0.18049, + 0.0 + ], + [ + 0.0, + 0.10823, + 0.0 + ], + [ + -0.04057, + -0.18138, + -0.1173 + ] + ], + "hulls": [ + [ + [ + -0.15137, + 0.17533, + -0.07598 + ], + [ + -0.00862, + -0.01403, + -0.03231 + ], + [ + -0.21243, + 0.04236, + -0.06494 + ], + [ + -0.05177, + 0.08996, + 0.01251 + ], + [ + -0.13274, + 0.06548, + -0.05554 + ], + [ + -0.00623, + 0.0907, + -0.0537 + ], + [ + -0.07673, + -0.03425, + -0.01586 + ], + [ + -0.04578, + 0.02054, + 0.01325 + ], + [ + -0.06815, + 0.05124, + -0.0536 + ], + [ + -0.11547, + 0.09188, + -0.01059 + ] + ], + [ + [ + -0.04022, + 0.19274, + 0.02268 + ], + [ + -0.18808, + -0.01116, + -0.04065 + ], + [ + -0.00948, + 0.03806, + -0.05252 + ], + [ + -0.00011, + 0.12049, + -0.03261 + ], + [ + -0.05523, + 0.03318, + 0.01582 + ], + [ + -0.0526, + 0.12081, + 0.02013 + ], + [ + -0.1658, + 0.05417, + -0.05721 + ], + [ + -0.00178, + 0.18739, + -0.03687 + ], + [ + -0.00859, + 0.0377, + -0.00229 + ], + [ + -0.04574, + 0.03444, + -0.02739 + ] + ], + [ + [ + -0.13171, + -0.13625, + -0.07775 + ], + [ + -0.01593, + 0.10242, + 0.01499 + ], + [ + -0.03483, + 0.00179, + -0.01444 + ], + [ + -0.12119, + -0.04466, + -0.10461 + ], + [ + -0.03919, + 0.04719, + 0.02226 + ], + [ + -0.00011, + 0.04727, + -0.01891 + ], + [ + -0.14943, + -0.07688, + -0.1013 + ], + [ + 0.00414, + 0.00687, + -0.02526 + ], + [ + -0.11494, + -0.10959, + -0.09972 + ], + [ + -0.00516, + 0.09746, + -0.01752 + ] + ], + [ + [ + -0.00177, + -0.27194, + -0.12982 + ], + [ + 0.00148, + 0.04408, + -0.00096 + ], + [ + -0.09496, + -0.20564, + -0.13214 + ], + [ + -0.04621, + -0.22711, + -0.13847 + ], + [ + -0.01361, + 0.01332, + 0.0075 + ], + [ + -0.03276, + -0.24628, + -0.12281 + ], + [ + -0.00278, + 0.01174, + -0.01145 + ], + [ + -0.07862, + -0.20394, + -0.14078 + ], + [ + -0.00561, + -0.27192, + -0.11553 + ], + [ + -0.02874, + -0.24582, + -0.13598 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.L.005": 11.524415771325243, + "DEF-hair.L.006": 0.9517508130810395, + "DEF-hair.L.007": 0.02390360394735, + "DEF-hair.L.008": 0.0793055946615831, + "DEF-hair.L.004": 3.697105989174614, + "DEF-hair.L.001": 1.7201846105818497 + }, + { + "DEF-hair.L.002": 4.333528852882772, + "DEF-hair.L": 1.7201846105818497, + "DEF-hair.L.005": 1.3899033932425278, + "DEF-hair.L.004": 0.1234906030940448, + "DEF-hair.L.003": 2.1324737371081905 + }, + { + "DEF-hair.L.003": 9.736098069029858, + "DEF-hair.L.001": 4.333528852882772, + "DEF-hair.L.005": 3.6150718734555625 + }, + { + "DEF-hair.L.002": 9.736098069029858, + "DEF-hair.L.005": 4.40068354347777, + "DEF-hair.L.001": 2.1324737371081905 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.004", + "bones": [ + "DEF-hair.R", + "DEF-hair.R.001", + "DEF-hair.R.002", + "DEF-hair.R.003" + ], + "tips": [ + [ + 0.0, + 0.11762, + -0.0 + ], + [ + -0.0, + 0.18049, + 0.0 + ], + [ + 0.0, + 0.10823, + -0.0 + ], + [ + 0.00673, + 0.04895, + -0.00199 + ] + ], + "hulls": [ + [ + [ + 0.10392, + 0.04499, + -0.03143 + ], + [ + -0.00179, + 0.08913, + -0.04032 + ], + [ + 0.01041, + -0.01469, + -0.03154 + ], + [ + 0.05269, + 0.08941, + 0.01263 + ], + [ + 0.04669, + 0.01999, + 0.01337 + ], + [ + 0.00289, + 0.0244, + -0.00341 + ], + [ + 0.03259, + 0.06068, + -0.03311 + ], + [ + 0.00187, + 0.02225, + -0.05257 + ], + [ + 0.06928, + 0.05051, + -0.05454 + ], + [ + 0.03548, + 0.01714, + -0.02935 + ] + ], + [ + [ + 0.04087, + 0.19192, + 0.02292 + ], + [ + 0.01014, + 0.03724, + -0.05228 + ], + [ + 0.00077, + 0.11967, + -0.03237 + ], + [ + 0.05589, + 0.03236, + 0.01606 + ], + [ + 0.05326, + 0.11999, + 0.02037 + ], + [ + 0.00244, + 0.18657, + -0.03663 + ], + [ + 0.00925, + 0.03688, + -0.00205 + ], + [ + 0.0464, + 0.03362, + -0.02715 + ], + [ + 0.04423, + 0.12228, + -0.02087 + ], + [ + 0.033, + 0.19244, + -0.01446 + ] + ], + [ + [ + 0.02337, + 0.10374, + 0.0137 + ], + [ + 0.02848, + -0.00246, + 0.02582 + ], + [ + 0.00759, + 0.04735, + -0.02831 + ], + [ + 0.03953, + 0.0462, + 0.02246 + ], + [ + 0.00572, + 0.04488, + 0.00827 + ], + [ + 0.00551, + 0.09646, + -0.01731 + ], + [ + 0.00241, + 0.00207, + 0.00675 + ], + [ + 0.0336, + 0.04774, + -0.00921 + ], + [ + 0.0059, + 0.07567, + 0.00688 + ], + [ + 0.0273, + 0.0798, + -0.0066 + ] + ], + [ + [ + -0.00144, + 0.04301, + -0.00088 + ], + [ + 0.01365, + 0.01225, + 0.00757 + ], + [ + 0.00282, + 0.01067, + -0.01137 + ], + [ + 0.00127, + 0.00974, + 0.00225 + ], + [ + 0.01206, + 0.0122, + -0.00415 + ], + [ + -0.0003, + 0.00984, + -0.00764 + ], + [ + 0.00895, + 0.0112, + 0.00848 + ], + [ + 0.00925, + 0.01102, + 0.00745 + ], + [ + 0.01117, + 0.01186, + -0.00362 + ], + [ + 0.00076, + 0.00976, + -0.00739 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.R.005": 8.895136309384288, + "DEF-hair.R.006": 0.5874295983023108, + "DEF-hair.R.008": 2.6819266073154084, + "DEF-hair.R.004": 3.0752010577070132, + "DEF-hair.R.001": 1.803741777314857 + }, + { + "DEF-hair.R.002": 3.682230264940157, + "DEF-hair.R": 1.803741777314857 + }, + { + "DEF-hair.R.003": 3.8194621786413894, + "DEF-hair.R.001": 3.682230264940157 + }, + { + "DEF-hair.R.002": 3.8194621786413894 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.003", + "bones": [ + "DEF-hair.L.004", + "DEF-hair.L.005", + "DEF-hair.L.006", + "DEF-hair.L.007" + ], + "tips": [ + [ + -0.0, + 0.13941, + -0.0 + ], + [ + -0.0, + 0.20008, + -0.0 + ], + [ + 0.0, + 0.10005, + -0.0 + ], + [ + -0.05539, + -0.26952, + -0.08576 + ] + ], + "hulls": [ + [ + [ + -0.12642, + 0.30292, + 0.00405 + ], + [ + 0.05763, + -0.01675, + -0.04559 + ], + [ + -0.06843, + 0.11737, + -0.0105 + ], + [ + -0.01425, + 0.23819, + 0.01163 + ], + [ + -0.04616, + -0.00216, + -0.04609 + ], + [ + 0.02452, + 0.11463, + -0.05849 + ], + [ + -0.08785, + 0.2271, + -0.05288 + ], + [ + -0.04701, + 0.30846, + -0.0095 + ], + [ + 0.06689, + 0.05756, + -0.04087 + ], + [ + 0.01631, + 0.00123, + -0.00275 + ] + ], + [ + [ + 0.22683, + 0.29103, + 0.02328 + ], + [ + -0.08923, + 0.10069, + -0.15483 + ], + [ + 0.10543, + -0.04097, + -0.04242 + ], + [ + -0.05913, + 0.1578, + 0.02553 + ], + [ + 0.13118, + 0.13005, + 0.05198 + ], + [ + 0.01882, + 0.07286, + -0.05548 + ], + [ + 0.12421, + 0.23997, + 0.00629 + ], + [ + 0.04625, + 0.10805, + -0.14725 + ], + [ + 0.10061, + 0.13139, + -0.04347 + ], + [ + 0.01364, + 0.17206, + -0.05526 + ] + ], + [ + [ + -0.03371, + 0.08882, + 0.00319 + ], + [ + 0.01108, + 0.04109, + -0.04371 + ], + [ + -0.02901, + 0.03766, + 0.01054 + ], + [ + 0.01259, + 0.08166, + -0.02576 + ], + [ + -0.02479, + 0.03974, + -0.03012 + ], + [ + 0.00154, + 0.03876, + -0.00123 + ], + [ + -0.02018, + 0.0886, + -0.02503 + ], + [ + 0.00084, + 0.08176, + -0.00177 + ], + [ + 0.01614, + 0.04051, + -0.03051 + ], + [ + -0.04167, + 0.03777, + 0.00429 + ] + ], + [ + [ + 0.09474, + -0.32132, + -0.06828 + ], + [ + -0.00093, + 0.06392, + 0.00035 + ], + [ + -0.10686, + -0.18513, + -0.02452 + ], + [ + 0.03872, + -0.24274, + -0.13557 + ], + [ + -0.11326, + -0.09615, + 0.0002 + ], + [ + -0.03432, + -0.169, + -0.05155 + ], + [ + 0.09388, + -0.25139, + -0.1342 + ], + [ + -0.02109, + 0.01802, + 0.00233 + ], + [ + -0.07019, + -0.21769, + -0.02149 + ], + [ + -0.09006, + -0.1274, + -0.02563 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.R.004": 4.68625580591287, + "DEF-hair.L.005": 40.27876944021308, + "DEF-hair.R.005": 0.1563188807361422, + "DEF-hair.L": 3.697105989174614, + "DEF-hair.L.007": 2.0160448574603813, + "DEF-hair.L.006": 1.8851661673028555, + "DEF-hair.L.001": 0.1234906030940448 + }, + { + "DEF-hair.R.004": 0.451326671620353, + "DEF-hair.L.004": 40.27876944021308, + "DEF-hair.L.006": 18.005348435594794, + "DEF-hair.L.007": 11.200201298516722, + "DEF-hair.L": 11.524415771325243, + "DEF-hair.L.001": 1.3899033932425278, + "DEF-hair.L.002": 3.6150718734555625, + "DEF-hair.L.003": 4.40068354347777 + }, + { + "DEF-hair.L.005": 18.005348435594794, + "DEF-hair.L.007": 11.514555437529838, + "DEF-hair.L": 0.9517508130810395, + "DEF-hair.L.004": 1.8851661673028555 + }, + { + "DEF-hair.L.005": 11.200201298516722, + "DEF-hair.L.006": 11.514555437529838, + "DEF-hair.L": 0.02390360394735, + "DEF-hair.L.004": 2.0160448574603813 + } + ] + }, + { + "class": "hair", + "root_parent": "DEF-spine.003", + "bones": [ + "DEF-hair.R.004", + "DEF-hair.R.005", + "DEF-hair.R.006", + "DEF-hair.R.007" + ], + "tips": [ + [ + 0.0, + 0.13941, + -0.0 + ], + [ + -0.0, + 0.20008, + -0.0 + ], + [ + 0.0, + 0.10005, + -0.0 + ], + [ + 0.108, + -0.20336, + -0.03793 + ] + ], + "hulls": [ + [ + [ + 0.12748, + 0.30271, + 0.00416 + ], + [ + -0.05658, + -0.01695, + -0.04548 + ], + [ + 0.10849, + 0.10935, + -0.0433 + ], + [ + 0.01637, + 0.2129, + -0.00652 + ], + [ + -0.02347, + 0.11443, + -0.05838 + ], + [ + 0.04721, + -0.00236, + -0.04598 + ], + [ + 0.12972, + 0.2226, + -0.03887 + ], + [ + 0.04806, + 0.30826, + -0.0094 + ], + [ + 0.03544, + 0.11471, + -0.00615 + ], + [ + -0.06583, + 0.05736, + -0.04077 + ] + ], + [ + [ + -0.22588, + 0.29058, + 0.02349 + ], + [ + 0.03822, + 0.002, + -0.07772 + ], + [ + -0.0914, + 0.12009, + 0.04015 + ], + [ + 0.07244, + 0.14233, + -0.1518 + ], + [ + 0.06008, + 0.15735, + 0.02574 + ], + [ + -0.10791, + -0.04429, + -0.05776 + ], + [ + -0.02672, + 0.08048, + -0.13759 + ], + [ + -0.12325, + 0.23952, + 0.0065 + ], + [ + -0.01314, + 0.17179, + -0.05603 + ], + [ + -0.03432, + 0.04032, + 0.0005 + ] + ], + [ + [ + 0.03454, + 0.08821, + 0.00351 + ], + [ + -0.01025, + 0.04049, + -0.04339 + ], + [ + 0.02984, + 0.03706, + 0.01086 + ], + [ + -0.01176, + 0.08105, + -0.02544 + ], + [ + 0.02562, + 0.03913, + -0.02979 + ], + [ + -0.00071, + 0.03816, + -0.00091 + ], + [ + 0.02101, + 0.08799, + -0.0247 + ], + [ + -1e-05, + 0.08116, + -0.00145 + ], + [ + -0.01531, + 0.03991, + -0.03019 + ], + [ + 0.0425, + 0.03716, + 0.00461 + ] + ], + [ + [ + -0.09428, + -0.32225, + -0.06801 + ], + [ + 0.0014, + 0.06298, + 0.00062 + ], + [ + 0.10733, + -0.18606, + -0.02425 + ], + [ + 0.11373, + -0.09708, + 0.00047 + ], + [ + 0.03479, + -0.16994, + -0.05128 + ], + [ + 0.02156, + 0.01709, + 0.0026 + ], + [ + 0.07066, + -0.21863, + -0.02122 + ], + [ + 0.09053, + -0.12833, + -0.02536 + ], + [ + -0.11638, + -0.28965, + -0.06878 + ], + [ + 0.06006, + -0.19855, + -0.05526 + ] + ] + ], + "neighbours": [ + { + "DEF-hair.R.005": 39.80276112999891, + "DEF-hair.R": 3.0752010577070132, + "DEF-hair.R.007": 2.4225536418512603, + "DEF-hair.R.006": 1.7560717968837305, + "DEF-hair.L.004": 4.68625580591287, + "DEF-hair.L.005": 0.451326671620353 + }, + { + "DEF-hair.R.006": 16.949171209581294, + "DEF-hair.R.007": 4.844984208602863, + "DEF-hair.R": 8.895136309384288, + "DEF-hair.R.008": 1.3949823102104308, + "DEF-hair.R.004": 39.80276112999891, + "DEF-hair.L.004": 0.1563188807361422, + "DEF-skirt.L.003.seg3.seg3": 2.5491632031573146, + "DEF-skirt.L.003.seg3.seg2": 0.07432172635976281 + }, + { + "DEF-hair.R.005": 16.949171209581294, + "DEF-hair.R.007": 7.382812851721533, + "DEF-hair.R": 0.5874295983023108, + "DEF-hair.R.008": 0.08180381314025653, + "DEF-hair.R.004": 1.7560717968837305 + }, + { + "DEF-hair.R.005": 4.844984208602863, + "DEF-hair.R.006": 7.382812851721533, + "DEF-hair.R.004": 2.4225536418512603 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine.001", + "bones": [ + "DEF-skirt", + "DEF-skirt.seg1", + "DEF-skirt.seg2", + "DEF-skirt.seg3", + "DEF-skirt.seg3.seg1", + "DEF-skirt.seg3.seg2", + "DEF-skirt.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.04862, + -0.0 + ], + [ + 0.0, + 0.04862, + -0.0 + ], + [ + -0.0, + 0.04862, + 0.0 + ], + [ + -0.0, + 0.03555, + -0.0 + ], + [ + -0.0, + 0.03555, + 0.0 + ], + [ + 0.0, + 0.03555, + -0.0 + ], + [ + -0.03501, + 0.06574, + -0.09908 + ] + ], + "hulls": [ + [ + [ + -0.01048, + 0.00471, + -0.09838 + ], + [ + 0.00854, + -0.00859, + 0.01809 + ], + [ + -0.0271, + 0.03158, + -0.01766 + ], + [ + -0.0049, + 0.0074, + -0.04266 + ], + [ + -0.00582, + -0.0011, + -0.0058 + ], + [ + -0.00356, + 0.02438, + -0.02161 + ], + [ + -0.02431, + 0.0063, + -0.04071 + ], + [ + 0.00058, + -0.00428, + 0.00657 + ], + [ + -0.01004, + 0.00121, + -0.01887 + ], + [ + -0.0145, + 0.00332, + -0.03183 + ] + ], + [ + [ + 0.03347, + 0.04167, + 0.04521 + ], + [ + 0.00529, + 0.02842, + -0.04709 + ], + [ + -0.00166, + 0.02549, + 0.00886 + ], + [ + 0.03211, + 0.0011, + 0.01598 + ], + [ + 0.02719, + 0.03735, + -0.013 + ], + [ + -0.01385, + 0.01375, + -0.02703 + ], + [ + 0.0171, + 0.0103, + -0.01507 + ], + [ + 0.04061, + 0.03722, + 0.02051 + ], + [ + 0.00867, + 0.01004, + 0.02387 + ], + [ + 0.02479, + 0.02369, + 0.00318 + ] + ], + [ + [ + -0.0604, + 0.02572, + -0.06034 + ], + [ + 0.04098, + 0.02712, + 0.02868 + ], + [ + -0.03136, + 0.01869, + 0.01049 + ], + [ + 0.00783, + 0.03339, + -0.01775 + ], + [ + -0.02218, + 0.03954, + -0.04195 + ], + [ + 0.02203, + 0.01142, + 0.00456 + ], + [ + -0.0438, + 0.01432, + -0.02093 + ], + [ + -0.01168, + 0.02406, + 0.03633 + ], + [ + -0.0128, + 0.04482, + 0.00027 + ], + [ + 0.01566, + 0.0355, + 0.04064 + ] + ], + [ + [ + 0.03769, + 0.02106, + 0.09036 + ], + [ + 0.00921, + -0.00437, + 0.01238 + ], + [ + 0.02135, + 0.03533, + 0.03328 + ], + [ + 0.02536, + 0.02605, + 0.07367 + ], + [ + 0.02704, + -0.0094, + 0.02063 + ], + [ + 0.02482, + 0.03053, + 0.05113 + ], + [ + 0.00882, + -0.00421, + 0.01339 + ], + [ + 0.02629, + 0.02568, + 0.07323 + ], + [ + 0.02384, + 0.03096, + 0.05134 + ], + [ + 0.0385, + 0.02073, + 0.08971 + ] + ], + [ + [ + 0.05388, + 0.00868, + 0.04135 + ], + [ + -0.02511, + 0.03365, + -0.04098 + ], + [ + -0.01613, + 0.00768, + 0.01862 + ], + [ + 0.02943, + 0.01859, + 0.00975 + ], + [ + -0.04254, + 0.01647, + -0.01371 + ], + [ + 0.00294, + 0.02411, + 0.00308 + ], + [ + -0.01194, + 0.02966, + -0.0201 + ], + [ + 0.05132, + 0.01348, + 0.01736 + ], + [ + 0.00533, + 0.0033, + 0.02518 + ], + [ + -0.02929, + 0.01229, + 0.00014 + ] + ], + [], + [ + [ + -0.00667, + 0.05201, + -0.05897 + ], + [ + -0.04122, + 0.02088, + -0.04844 + ], + [ + 0.00498, + 0.03051, + -0.03021 + ], + [ + -0.01311, + 0.01967, + -0.05267 + ], + [ + -0.03903, + 0.03758, + -0.05219 + ], + [ + -0.0102, + 0.03662, + -0.05495 + ], + [ + -0.03885, + 0.03772, + -0.05113 + ], + [ + -0.0071, + 0.05217, + -0.05798 + ], + [ + -0.01353, + 0.01994, + -0.0517 + ], + [ + 0.00585, + 0.03023, + -0.03081 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.L.003": 1.326156383856757, + "DEF-skirt.L": 10.695220888352067, + "DEF-skirt.003": 10.393737776381599, + "DEF-skirt.007": 0.09740479559841991, + "DEF-skirt.003.seg2": 2.414865854921683, + "DEF-skirt.seg1": 1.828215843620832, + "DEF-skirt.003.seg1": 2.268834518300463, + "DEF-skirt.L.004": 0.19723534186546737, + "DEF-skirt.003.seg3": 1.3252616406526603, + "DEF-skirt.003.seg3.seg1": 0.1264698867553462, + "DEF-skirt.L.seg1": 0.1748055563495634 + }, + { + "DEF-skirt.seg2": 2.981566244310728, + "DEF-skirt.007.seg2": 0.013958547848881953, + "DEF-skirt.003.seg3.seg1": 1.1568532950699486, + "DEF-skirt.003.seg3.seg2": 0.11113539730119074, + "DEF-skirt.003.seg2": 3.0401021461514377, + "DEF-skirt": 1.828215843620832, + "DEF-skirt.003.seg1": 1.5344447400919996, + "DEF-skirt.007.seg3": 0.013931251322537197, + "DEF-skirt.007.seg3.seg1": 0.10326649938885435, + "DEF-skirt.009": 0.07365434578813679, + "DEF-skirt.005": 0.12456313680711745, + "DEF-skirt.003.seg3": 4.144818838300255, + "DEF-skirt.L.seg1": 2.274402476592972, + "DEF-skirt.007": 0.05097562226131125, + "DEF-skirt.L.seg2": 2.1440963053991426, + "DEF-skirt.L.004": 0.2166604484120559, + "DEF-skirt.L.003": 0.10608606037822921, + "DEF-skirt.003": 0.09789723353220037 + }, + { + "DEF-skirt.seg3": 2.092407403245921, + "DEF-skirt.003.seg3.seg1": 2.358715973218084, + "DEF-skirt.seg1": 2.981566244310728, + "DEF-skirt.007.seg2": 0.062276340879366066, + "DEF-skirt.003.seg3.seg2": 0.5614086305271673, + "DEF-skirt.L.seg3": 2.8581711493785273, + "DEF-skirt.007.seg1": 0.10602302515859996, + "DEF-skirt.003.seg3": 2.23476627840628, + "DEF-skirt.L.004": 0.10706301785350203, + "DEF-skirt.005": 0.3701904039963759, + "DEF-skirt.003.seg3.seg3": 0.1987954432480712, + "DEF-skirt.009": 0.15624594621636767, + "DEF-skirt.011": 0.10947330483127027, + "DEF-skirt.007.seg3.seg1": 0.007769999773823699, + "DEF-skirt.007.seg3.seg2": 0.14838076659398158, + "DEF-skirt.007.seg3.seg3": 0.04492484052275092, + "DEF-skirt.009.seg1": 0.05715209920697695, + "DEF-skirt.L.seg2": 3.9105905766472993, + "DEF-skirt.L.003": 0.40112976503480047, + "DEF-skirt.L.seg1": 0.3998687860050416, + "DEF-skirt.003.seg2": 0.1200889205967215 + }, + { + "DEF-skirt.seg2": 2.092407403245921, + "DEF-skirt.003.seg3.seg1": 0.403789280719129, + "DEF-skirt.L.seg3": 0.6911803977028846, + "DEF-skirt.003.seg3": 0.12452012666138562, + "DEF-skirt.L.001": 0.15234850550117335, + "DEF-skirt.L.004": 0.25244402955330414, + "DEF-skirt.011": 0.9369690571555245, + "DEF-skirt.seg3.seg1": 1.356551225861607, + "DEF-skirt.L.seg3.seg1": 0.25118956748680205, + "DEF-skirt.L.seg3.seg2": 0.49240239279626097, + "DEF-skirt.003.seg3.seg2": 0.023631456323105038, + "DEF-skirt.L.003": 0.017549561464065, + "DEF-skirt.L.seg2": 0.058694964116303594 + }, + { + "DEF-skirt.seg3.seg2": 0.7974728689199146, + "DEF-skirt.L.004": 0.48471971803661384, + "DEF-skirt.L.seg3.seg3": 0.40271404225102825, + "DEF-skirt.005": 0.6138460617352536, + "DEF-skirt.003.seg3.seg2": 1.858625426861799, + "DEF-skirt.003.seg3.seg3": 0.46307022535704634, + "DEF-skirt.011": 1.2212581862234682, + "DEF-skirt.seg3": 1.356551225861607, + "DEF-skirt.L.seg3.seg1": 0.7114231291403301, + "DEF-skirt.007.seg3.seg2": 0.03270002877903588, + "DEF-skirt.007.seg2": 0.022665236298227698, + "DEF-skirt.L.seg3.seg2": 0.23643016075465373, + "DEF-skirt.011.seg2": 0.1828297678733849, + "DEF-skirt.011.seg1": 0.07114842230691942, + "DEF-skirt.007.seg3.seg1": 0.010801484237957126, + "DEF-skirt.003.seg3.seg1": 0.10161321903086407, + "DEF-skirt.L.001": 0.02105309606015293 + }, + { + "DEF-skirt.011.seg1": 1.9490406902957946, + "DEF-skirt.L.001": 0.2335541231476439, + "DEF-skirt.002": 0.1676262080621833, + "DEF-skirt.011": 0.6159119122636163, + "DEF-skirt.seg3.seg3": 0.42177095906282575, + "DEF-skirt.L.seg3.seg3": 0.8202263098452109, + "DEF-skirt.011.seg2": 0.4824420103059537, + "DEF-skirt.L.seg3.seg2": 0.0839953655566581, + "DEF-skirt.005.seg1": 0.2788327945314417, + "DEF-skirt.003.seg3.seg3": 0.6034309528782597, + "DEF-skirt.L.004": 0.04960925122223636, + "DEF-skirt.seg3.seg1": 0.7974728689199146, + "DEF-skirt.005": 0.41073438050864913, + "DEF-skirt.003.seg3.seg2": 0.39457844925987096, + "DEF-skirt.007.seg3.seg2": 0.10017902235084042, + "DEF-skirt.007.seg2": 0.027715837136825017, + "DEF-skirt.011.seg3.seg3": 0.05145194199315073, + "DEF-skirt.009": 0.07027978047871075, + "DEF-skirt.007.seg3.seg1": 0.018364528679975622, + "DEF-skirt.009.seg1": 0.03079948105880803 + }, + { + "DEF-skirt.011.seg1": 3.841728980444033, + "DEF-skirt.011": 1.8142268693202859, + "DEF-skirt.002": 0.08269355505130749, + "DEF-skirt.seg3.seg2": 0.42177095906282575, + "DEF-skirt.005.seg2": 5.03848026532854, + "DEF-skirt.009": 0.5669906833388019, + "DEF-skirt.003.seg3.seg3": 4.403183035664169, + "DEF-skirt.011.seg2": 2.3263942659603747, + "DEF-skirt.L.seg3.seg3": 0.2748268291321209, + "DEF-skirt.005.seg1": 3.8134393417781873, + "DEF-skirt.L.seg3.seg2": 0.020429295538830217, + "DEF-skirt.009.seg1": 0.6259572671341773, + "DEF-skirt.007.seg3.seg3": 0.16922538313054947, + "DEF-skirt.006": 0.06978071994562751, + "DEF-skirt.011.seg3.seg3": 0.048795000199878036, + "DEF-skirt.005": 0.2100940425553619, + "DEF-skirt.011.seg3": 0.030706543998581193, + "DEF-skirt.009.seg2": 0.04456463799778154 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine.001", + "bones": [ + "DEF-skirt.003", + "DEF-skirt.003.seg1", + "DEF-skirt.003.seg2", + "DEF-skirt.003.seg3", + "DEF-skirt.003.seg3.seg1", + "DEF-skirt.003.seg3.seg2", + "DEF-skirt.003.seg3.seg3" + ], + "tips": [ + [ + -0.0, + 0.01789, + 0.0 + ], + [ + -0.0, + 0.01789, + 0.0 + ], + [ + 0.0, + 0.01789, + -0.0 + ], + [ + -0.0, + 0.04466, + 0.0 + ], + [ + -0.0, + 0.04466, + 0.0 + ], + [ + -0.0, + 0.04466, + 0.0 + ], + [ + 0.02446, + 0.14296, + -0.02362 + ] + ], + "hulls": [ + [ + [ + -0.05008, + -0.05696, + -0.04666 + ], + [ + 0.03924, + 0.00423, + 0.05153 + ], + [ + -0.02261, + 0.01376, + -0.00513 + ], + [ + -0.01061, + -0.04128, + -0.00124 + ], + [ + -0.04675, + -0.01389, + -0.0306 + ], + [ + 0.01629, + 0.00121, + 0.01377 + ], + [ + 0.00116, + -0.03009, + 0.03884 + ], + [ + 0.04402, + -0.0359, + 0.03842 + ], + [ + -0.03107, + -0.02404, + -0.06145 + ], + [ + -0.03916, + -0.04651, + -0.02055 + ] + ], + [ + [ + 0.05649, + 0.01409, + -0.02176 + ], + [ + -0.02953, + 0.00775, + -0.02538 + ], + [ + 0.01593, + 0.01651, + -0.02885 + ], + [ + 0.03006, + 0.00965, + 0.00505 + ], + [ + -0.00527, + 0.00065, + -0.02313 + ], + [ + 0.03474, + 0.00541, + -0.02441 + ], + [ + 0.03365, + 0.00732, + -0.01286 + ], + [ + 0.05361, + 0.01517, + -0.01043 + ], + [ + 0.0404, + 0.01194, + -0.01841 + ], + [ + -0.02809, + 0.00969, + -0.01762 + ] + ], + [ + [ + 0.05188, + 0.00935, + -0.01886 + ], + [ + -0.01383, + 0.01671, + 0.03524 + ], + [ + -0.00301, + 0.00995, + -0.03276 + ], + [ + 0.0326, + 0.00706, + 0.03816 + ], + [ + -0.01412, + 0.01537, + -0.00122 + ], + [ + 0.00871, + 0.01341, + 0.03461 + ], + [ + 0.02056, + 0.01066, + 0.03685 + ], + [ + 0.04274, + 0.01001, + -0.02639 + ], + [ + -0.0027, + 0.01545, + 0.03451 + ], + [ + -0.00379, + 0.0099, + -0.02409 + ] + ], + [ + [ + -0.06659, + 0.0228, + -0.00272 + ], + [ + 0.03863, + 0.00577, + 0.00082 + ], + [ + -0.01541, + 0.00198, + -0.02453 + ], + [ + -0.02853, + 0.02281, + 0.00706 + ], + [ + 0.00588, + 0.03237, + -0.03485 + ], + [ + 0.02492, + -0.00017, + -0.03347 + ], + [ + 0.0026, + 0.01641, + -0.00103 + ], + [ + -0.05754, + 0.01719, + 0.01954 + ], + [ + -0.00467, + 0.01714, + -0.0289 + ], + [ + 0.02049, + 0.0115, + 0.0034 + ] + ], + [ + [ + -0.04301, + 0.02757, + 0.04322 + ], + [ + 0.0476, + 0.00905, + 0.02164 + ], + [ + 0.0085, + 0.03044, + -0.02808 + ], + [ + -0.05335, + 0.01211, + -0.01144 + ], + [ + 0.00139, + 0.02315, + 0.02417 + ], + [ + 0.01728, + 0.04114, + -0.00606 + ], + [ + 0.03165, + 0.01398, + 0.02661 + ], + [ + -0.02617, + 0.02569, + 0.03967 + ], + [ + -0.01158, + 0.02482, + 0.03173 + ], + [ + 0.01592, + 0.01925, + 0.02552 + ] + ], + [ + [ + 0.07104, + 0.02475, + 0.01099 + ], + [ + -0.01831, + 0.00358, + 0.00096 + ], + [ + 0.01192, + 0.04328, + 0.0127 + ], + [ + 0.05026, + 0.03042, + 0.01867 + ], + [ + -0.00196, + 0.00123, + -0.01106 + ], + [ + 0.02994, + 0.0375, + 0.01424 + ], + [ + 0.02984, + 0.0377, + 0.01531 + ], + [ + 0.01165, + 0.04313, + 0.01165 + ], + [ + 0.0707, + 0.02468, + 0.00995 + ], + [ + -0.00171, + 0.00131, + -0.01 + ] + ], + [ + [ + 0.02393, + 0.1101, + -0.0341 + ], + [ + 0.03022, + 0.01278, + -0.01313 + ], + [ + 0.00323, + 0.0618, + 0.01409 + ], + [ + 0.00814, + 0.10877, + 0.00468 + ], + [ + -0.00942, + 0.02067, + -0.00517 + ], + [ + 0.01864, + 0.07739, + -0.02819 + ], + [ + 0.0492, + 0.10101, + -0.02577 + ], + [ + -0.00167, + 0.07745, + -0.01188 + ], + [ + 0.02142, + 0.0606, + -0.00084 + ], + [ + 0.00869, + 0.0192, + -0.02004 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.007": 17.307897292439947, + "DEF-skirt": 10.393737776381599, + "DEF-skirt.003.seg1": 2.1973733491742267, + "DEF-skirt.L": 7.825558809065379, + "DEF-skirt.seg1": 0.09789723353220037, + "DEF-skirt.L.003": 0.05178392383528613, + "DEF-skirt.R": 1.8412317503301705, + "DEF-skirt.007.seg1": 0.03732637437143671 + }, + { + "DEF-skirt.seg1": 1.5344447400919996, + "DEF-skirt": 2.268834518300463, + "DEF-skirt.003.seg2": 1.3268159482744226, + "DEF-skirt.003": 2.1973733491742267, + "DEF-skirt.L.seg1": 0.09700218840825181 + }, + { + "DEF-skirt.003.seg3": 2.796186512600533, + "DEF-skirt": 2.414865854921683, + "DEF-skirt.seg1": 3.0401021461514377, + "DEF-skirt.003.seg1": 1.3268159482744226, + "DEF-skirt.L.seg1": 0.2979854933847978, + "DEF-skirt.007": 0.17083154774496911, + "DEF-skirt.L.seg2": 0.21471564921936337, + "DEF-skirt.seg2": 0.1200889205967215, + "DEF-skirt.L.seg3": 0.03223739901535605 + }, + { + "DEF-skirt.seg2": 2.23476627840628, + "DEF-skirt.seg3": 0.12452012666138562, + "DEF-skirt.003.seg3.seg1": 1.3719918467625325, + "DEF-skirt.003.seg2": 2.796186512600533, + "DEF-skirt": 1.3252616406526603, + "DEF-skirt.seg1": 4.144818838300255, + "DEF-skirt.007": 1.5368390145847437, + "DEF-skirt.007.seg2": 1.053151290505093, + "DEF-skirt.007.seg1": 2.2966332003201027, + "DEF-skirt.007.seg3": 0.24521446761690108, + "DEF-skirt.L.seg2": 0.6420255266222546, + "DEF-skirt.L.seg3": 0.45624343750090457, + "DEF-skirt.R": 0.04330813415148985, + "DEF-skirt.R.seg1": 0.03283304659289388, + "DEF-skirt.L.seg1": 0.04252019284946501 + }, + { + "DEF-skirt.seg2": 2.358715973218084, + "DEF-skirt.seg3": 0.403789280719129, + "DEF-skirt.seg1": 1.1568532950699486, + "DEF-skirt.007.seg2": 0.666448692941354, + "DEF-skirt.003.seg3.seg2": 1.8795831357278767, + "DEF-skirt.007.seg1": 0.6158521164263719, + "DEF-skirt.003.seg3": 1.3719918467625325, + "DEF-skirt.007.seg3.seg1": 0.4365502716799038, + "DEF-skirt.007.seg3": 0.11857959460915246, + "DEF-skirt.R.seg2": 0.010865782296369086, + "DEF-skirt.007.seg3.seg2": 0.020351700754680624, + "DEF-skirt.005": 0.2785878408378042, + "DEF-skirt": 0.1264698867553462, + "DEF-skirt.seg3.seg1": 0.10161321903086407, + "DEF-skirt.L.seg3.seg1": 0.02949312439741647 + }, + { + "DEF-skirt.seg1": 0.11113539730119074, + "DEF-skirt.seg2": 0.5614086305271673, + "DEF-skirt.007.seg2": 0.24037750336559105, + "DEF-skirt.003.seg3.seg1": 1.8795831357278767, + "DEF-skirt.007.seg1": 0.06492517685354893, + "DEF-skirt.005": 0.9896900858102016, + "DEF-skirt.seg3.seg1": 1.858625426861799, + "DEF-skirt.003.seg3.seg3": 0.813177556003042, + "DEF-skirt.011": 0.23261883158359986, + "DEF-skirt.007.seg3.seg1": 0.7782740178336178, + "DEF-skirt.007.seg3": 0.05651045472693261, + "DEF-skirt.R.seg2": 0.016042945161717337, + "DEF-skirt.007.seg3.seg2": 0.04805373450254019, + "DEF-skirt.009": 0.05579521783738445, + "DEF-skirt.seg3.seg2": 0.39457844925987096, + "DEF-skirt.seg3": 0.023631456323105038, + "DEF-skirt.R.seg3": 0.01747063797741255, + "DEF-skirt.L.seg3.seg1": 0.045733041497018956 + }, + { + "DEF-skirt.006": 0.3897890162556754, + "DEF-skirt.005.seg1": 4.719704061300272, + "DEF-skirt.005": 1.1916771231975871, + "DEF-skirt.005.seg2": 5.299175017577895, + "DEF-skirt.seg3.seg3": 4.403183035664169, + "DEF-skirt.009": 0.9255952835673218, + "DEF-skirt.011.seg1": 1.22743385271503, + "DEF-skirt.011": 1.1737566849790808, + "DEF-skirt.seg3.seg2": 0.6034309528782597, + "DEF-skirt.seg3.seg1": 0.46307022535704634, + "DEF-skirt.003.seg3.seg2": 0.813177556003042, + "DEF-skirt.009.seg1": 0.803862237313018, + "DEF-skirt.009.seg2": 0.2763643729262093, + "DEF-skirt.seg2": 0.1987954432480712, + "DEF-skirt.011.seg2": 0.1914863875935997, + "DEF-skirt.007.seg3.seg2": 0.5731106537743211, + "DEF-skirt.007.seg2": 0.027393346065210267, + "DEF-skirt.007.seg3.seg3": 0.160640208594645, + "DEF-skirt.007.seg3.seg1": 0.03659960675887097 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine.001", + "bones": [ + "DEF-skirt.L", + "DEF-skirt.L.seg1", + "DEF-skirt.L.seg2", + "DEF-skirt.L.seg3", + "DEF-skirt.L.seg3.seg1", + "DEF-skirt.L.seg3.seg2", + "DEF-skirt.L.seg3.seg3" + ], + "tips": [ + [ + -0.0, + 0.03781, + 0.0 + ], + [ + 0.0, + 0.03781, + 0.0 + ], + [ + -0.0, + 0.03781, + 0.0 + ], + [ + 0.0, + 0.0378, + -0.0 + ], + [ + 0.0, + 0.0378, + -0.0 + ], + [ + 0.0, + 0.0378, + 0.0 + ], + [ + 0.0052, + 0.09456, + -0.02579 + ] + ], + "hulls": [ + [ + [ + -0.01718, + -0.03886, + 0.07057 + ], + [ + 0.03955, + 0.02731, + -0.04543 + ], + [ + 0.01011, + -0.04105, + -0.01324 + ], + [ + -0.01167, + 0.00334, + 0.01467 + ], + [ + -0.01033, + 0.03066, + -0.02984 + ], + [ + 0.00036, + -0.0048, + 0.0536 + ], + [ + 0.034, + -0.00826, + -0.03108 + ], + [ + 0.01864, + -0.03554, + -0.04963 + ], + [ + -0.01519, + -0.03227, + 0.00973 + ], + [ + -0.00034, + 0.00351, + -0.01547 + ] + ], + [ + [ + -0.01733, + 0.03016, + 0.03129 + ], + [ + 0.00029, + 0.03363, + -0.01135 + ], + [ + 0.01304, + 0.01139, + 0.02869 + ], + [ + -0.0117, + 0.00288, + 0.01837 + ], + [ + -0.00597, + 0.03248, + 0.01887 + ], + [ + 0.002, + 0.03388, + 0.00543 + ], + [ + 0.00094, + 0.03396, + 0.00519 + ], + [ + -0.0113, + 0.00187, + 0.01831 + ], + [ + -0.01822, + 0.03022, + 0.03066 + ], + [ + -0.00685, + 0.03257, + 0.01823 + ] + ], + [ + [ + 0.02797, + 0.02077, + -0.06929 + ], + [ + -0.00319, + 0.02407, + 0.05732 + ], + [ + 0.03008, + 0.0173, + -0.01102 + ], + [ + 0.00404, + 0.00325, + 0.00995 + ], + [ + 0.02352, + 0.03451, + 0.01921 + ], + [ + 0.01962, + 0.01154, + -0.03737 + ], + [ + -0.01275, + 0.00262, + 0.04527 + ], + [ + 0.03356, + 0.02949, + -0.04329 + ], + [ + 0.0172, + 0.00476, + -0.0589 + ], + [ + 0.01944, + 0.01544, + 0.01925 + ] + ], + [ + [ + 0.02122, + -0.01184, + -0.06997 + ], + [ + -0.03612, + 0.01766, + 0.05385 + ], + [ + 0.02851, + 0.01758, + 0.01527 + ], + [ + -0.0039, + 0.01776, + -0.0394 + ], + [ + 0.02895, + 0.00482, + -0.02182 + ], + [ + -0.00114, + 0.01947, + 0.0565 + ], + [ + -0.0017, + 0.03056, + 0.01664 + ], + [ + 0.03571, + -0.00629, + -0.05011 + ], + [ + 0.00266, + 0.01109, + -0.06193 + ], + [ + 0.00471, + 0.02418, + -0.0176 + ] + ], + [], + [ + [ + -0.06202, + 0.01459, + 0.07088 + ], + [ + 0.01252, + 0.0078, + -0.03198 + ], + [ + -0.01604, + 0.01323, + 0.02519 + ], + [ + -0.01671, + 0.01196, + -0.02271 + ], + [ + 0.00548, + 0.01411, + 0.00671 + ], + [ + -0.03628, + 0.01858, + 0.06736 + ], + [ + 0.02085, + 0.01397, + -0.01067 + ], + [ + -0.02563, + 0.01641, + 0.04885 + ], + [ + -0.06213, + 0.01456, + 0.0698 + ], + [ + 0.01979, + 0.01408, + -0.01091 + ] + ], + [ + [ + -0.06467, + 0.06156, + 0.06772 + ], + [ + 0.00816, + 0.01352, + -0.07234 + ], + [ + -0.0154, + 0.02778, + 0.01759 + ], + [ + 0.03404, + 0.06358, + -0.03037 + ], + [ + 0.003, + 0.05405, + -0.05824 + ], + [ + -0.03368, + 0.03728, + 0.0675 + ], + [ + 0.01055, + 0.04627, + -0.00839 + ], + [ + 0.01904, + 0.0245, + -0.04507 + ], + [ + 0.02178, + 0.07648, + -0.06028 + ], + [ + -0.02407, + 0.0495, + 0.04453 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt": 10.695220888352067, + "DEF-skirt.L.003": 16.573658069216204, + "DEF-skirt.L.003.seg1": 1.6198222225340344, + "DEF-skirt.L.003.seg3": 1.6131563569904204, + "DEF-skirt.R.003": 3.2402381303709697, + "DEF-skirt.003": 7.825558809065379, + "DEF-skirt.L.seg1": 0.9327113656675616, + "DEF-skirt.L.003.seg2": 0.900557430807742, + "DEF-skirt.R": 0.4379191130505151 + }, + { + "DEF-skirt.L.003": 4.725564107140233, + "DEF-skirt.seg1": 2.274402476592972, + "DEF-skirt.L.seg2": 4.427839132077279, + "DEF-skirt.003.seg2": 0.2979854933847978, + "DEF-skirt.L.003.seg1": 0.6622802211922085, + "DEF-skirt.seg2": 0.3998687860050416, + "DEF-skirt.L": 0.9327113656675616, + "DEF-skirt.003.seg1": 0.09700218840825181, + "DEF-skirt.L.003.seg2": 0.32230547800630927, + "DEF-skirt": 0.1748055563495634, + "DEF-skirt.003.seg3": 0.04252019284946501 + }, + { + "DEF-skirt.L.003": 4.007277618379357, + "DEF-skirt.L.004": 0.030678498443618918, + "DEF-skirt.L.seg3": 3.9133133843521852, + "DEF-skirt.L.seg3.seg1": 0.023121958580321622, + "DEF-skirt.L.seg1": 4.427839132077279, + "DEF-skirt.seg1": 2.1440963053991426, + "DEF-skirt.seg2": 3.9105905766472993, + "DEF-skirt.003.seg3": 0.6420255266222546, + "DEF-skirt.003.seg2": 0.21471564921936337, + "DEF-skirt.L.003.seg1": 0.1424029962977591, + "DEF-skirt.L.003.seg2": 0.16001926434900815, + "DEF-skirt.seg3": 0.058694964116303594, + "DEF-skirt.L.001": 0.10537824781740257 + }, + { + "DEF-skirt.L.seg2": 3.9133133843521852, + "DEF-skirt.L.003": 2.6757189730101847, + "DEF-skirt.L.004": 0.16873546962733288, + "DEF-skirt.L.seg3.seg1": 1.1082050165037503, + "DEF-skirt.L.001": 1.1794753370285003, + "DEF-skirt.seg3": 0.6911803977028846, + "DEF-skirt.seg2": 2.8581711493785273, + "DEF-skirt.L.003.seg2": 0.10414642683356412, + "DEF-skirt.L.003.seg3": 0.005636372948892854, + "DEF-skirt.L.003.seg1": 0.22655295168951683, + "DEF-skirt.003.seg3": 0.45624343750090457, + "DEF-skirt.003.seg2": 0.03223739901535605 + }, + { + "DEF-skirt.L.seg2": 0.023121958580321622, + "DEF-skirt.L.003": 0.4743791069056792, + "DEF-skirt.L.004": 0.09720520424174028, + "DEF-skirt.L.seg3": 1.1082050165037503, + "DEF-skirt.L.001": 0.4072310229783051, + "DEF-skirt.011": 0.08485950409668952, + "DEF-skirt.seg3.seg1": 0.7114231291403301, + "DEF-skirt.seg3": 0.25118956748680205, + "DEF-skirt.L.003.seg1": 0.16428564310004323, + "DEF-skirt.L.seg3.seg2": 1.1915904103513841, + "DEF-skirt.L.003.seg3.seg1": 0.01731429169825205, + "DEF-skirt.L.003.seg3.seg2": 0.006523155712845785, + "DEF-skirt.003.seg3.seg2": 0.045733041497018956, + "DEF-skirt.003.seg3.seg1": 0.02949312439741647 + }, + { + "DEF-skirt.011.seg1": 0.15118301815151153, + "DEF-skirt.011.seg2": 0.08240344936733568, + "DEF-skirt.seg3.seg3": 0.020429295538830217, + "DEF-skirt.seg3.seg2": 0.0839953655566581, + "DEF-skirt.L.seg3.seg3": 0.10850965629700338, + "DEF-skirt.L.003.seg1": 0.3403666007961017, + "DEF-skirt.L.004": 0.7253384654734396, + "DEF-skirt.L.001": 0.2261690626343804, + "DEF-skirt.seg3": 0.49240239279626097, + "DEF-skirt.L.seg3.seg1": 1.1915904103513841, + "DEF-skirt.seg3.seg1": 0.23643016075465373, + "DEF-skirt.L.003": 0.42029319196388837, + "DEF-skirt.L.003.seg3.seg1": 0.1895826561493289, + "DEF-skirt.L.003.seg3.seg2": 0.10142363563597101, + "DEF-skirt.L.003.seg2": 0.15569674127310984 + }, + { + "DEF-skirt.L.001.seg1": 0.16373520808554431, + "DEF-skirt.L.004": 2.4919195177858766, + "DEF-skirt.L.001.seg2": 0.03277273825234783, + "DEF-skirt.011.seg2": 3.6437474555280827, + "DEF-skirt.011.seg3": 0.5129986719242634, + "DEF-skirt.011.seg3.seg3": 2.4290242543951024, + "DEF-skirt.L.004.seg1": 0.8945923490834223, + "DEF-skirt.011.seg3.seg1": 0.6725704569200012, + "DEF-skirt.011.seg3.seg2": 0.9627032716458411, + "DEF-skirt.seg3.seg3": 0.2748268291321209, + "DEF-skirt.seg3.seg2": 0.8202263098452109, + "DEF-skirt.011.seg1": 0.4505585897398284, + "DEF-skirt.005.seg2": 0.03948786979380725, + "DEF-skirt.L.003.seg2": 0.47106880291962483, + "DEF-skirt.L.003.seg3.seg1": 0.2645954510120382, + "DEF-skirt.L.seg3.seg2": 0.10850965629700338, + "DEF-skirt.seg3.seg1": 0.40271404225102825, + "DEF-skirt.L.003.seg3.seg2": 0.6523418590640699, + "DEF-skirt.L.003.seg3.seg3": 0.1476984071768418, + "DEF-skirt.L.003.seg1": 0.05685497032801323 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine.001", + "bones": [ + "DEF-skirt.L.003", + "DEF-skirt.L.003.seg1", + "DEF-skirt.L.003.seg2", + "DEF-skirt.L.003.seg3", + "DEF-skirt.L.003.seg3.seg1", + "DEF-skirt.L.003.seg3.seg2", + "DEF-skirt.L.003.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.01473, + 0.0 + ], + [ + -0.0, + 0.01473, + 0.0 + ], + [ + 0.0, + 0.01473, + 0.0 + ], + [ + 0.0, + 0.03703, + 0.0 + ], + [ + 0.0, + 0.03703, + 0.0 + ], + [ + -0.0, + 0.03703, + 0.0 + ], + [ + -0.31405, + 0.09813, + 0.25335 + ] + ], + "hulls": [ + [ + [ + -0.11961, + -0.00244, + 0.09377 + ], + [ + 0.03737, + -0.00762, + -0.02167 + ], + [ + -0.04228, + -0.06138, + 0.03614 + ], + [ + -0.06996, + 0.01341, + 0.01895 + ], + [ + -0.01719, + 0.00218, + -0.0034 + ], + [ + -0.0946, + -0.01653, + 0.05222 + ], + [ + -0.02027, + -0.04183, + -0.005 + ], + [ + -0.00606, + -0.04564, + 0.02964 + ], + [ + 0.02042, + 0.01328, + 0.00447 + ], + [ + -0.0583, + -0.00133, + 0.04915 + ] + ], + [ + [ + 0.09911, + 0.00038, + -0.10791 + ], + [ + -0.02717, + 0.01394, + 0.0105 + ], + [ + 0.04307, + 0.00641, + -0.04657 + ], + [ + 0.03046, + 0.00076, + -0.00282 + ], + [ + 0.07423, + 0.01319, + -0.03945 + ], + [ + -0.00096, + 0.00409, + -0.00347 + ], + [ + -0.01848, + 0.0026, + 0.00091 + ], + [ + 0.00014, + 0.00336, + 0.01308 + ], + [ + 0.02267, + 0.00104, + -0.0143 + ], + [ + -0.02332, + 0.00279, + 0.01466 + ] + ], + [ + [ + 0.08569, + 0.00881, + -0.09381 + ], + [ + -0.04002, + 0.00863, + 0.02482 + ], + [ + 0.02842, + 0.00921, + -0.02168 + ], + [ + 0.06602, + 0.00044, + -0.04476 + ], + [ + -0.00207, + 0.01085, + 0.01334 + ], + [ + 0.03399, + 0.0046, + -0.00424 + ], + [ + 0.03981, + 0.00639, + -0.03647 + ], + [ + -0.04035, + 0.00881, + 0.0238 + ], + [ + -0.00247, + 0.01096, + 0.01233 + ], + [ + 0.0278, + 0.00912, + -0.02258 + ] + ], + [ + [ + -0.06253, + -0.00193, + 0.02708 + ], + [ + 0.03196, + 0.01591, + -0.02594 + ], + [ + -0.01515, + 0.01306, + 0.00884 + ], + [ + 0.00238, + 0.03513, + -0.01734 + ], + [ + 0.03511, + 0.01631, + 0.00085 + ], + [ + -0.04877, + 0.00396, + 0.0161 + ], + [ + 0.01653, + 0.02587, + -0.02317 + ], + [ + 0.02935, + 0.01556, + -0.00688 + ], + [ + -0.01347, + 0.01208, + 0.01766 + ], + [ + -0.015, + 0.01199, + 0.00873 + ] + ], + [ + [ + 0.12822, + 0.02845, + -0.10051 + ], + [ + -0.01185, + 0.00658, + -0.01133 + ], + [ + 0.0642, + 0.02924, + -0.04509 + ], + [ + 0.08902, + 0.00343, + -0.06279 + ], + [ + 0.08653, + 0.00498, + -0.03805 + ], + [ + 0.08028, + 0.01695, + -0.05001 + ], + [ + 0.07135, + 0.02003, + -0.03868 + ], + [ + 0.07611, + 0.00737, + -0.05441 + ], + [ + 0.08822, + 0.00372, + -0.06347 + ], + [ + 0.08088, + 0.01688, + -0.0491 + ] + ], + [ + [ + 0.12063, + 0.00931, + -0.0811 + ], + [ + 0.02821, + 0.01485, + -0.03564 + ], + [ + 0.11316, + 0.02435, + -0.06505 + ], + [ + 0.04441, + 0.00361, + -0.04394 + ], + [ + 0.11257, + 0.02444, + -0.06597 + ], + [ + 0.0448, + 0.00377, + -0.04294 + ], + [ + 0.02873, + 0.01491, + -0.03468 + ], + [ + 0.11982, + 0.00964, + -0.08175 + ] + ], + [ + [ + -0.2279, + 0.06273, + 0.18037 + ], + [ + 0.06948, + 0.01236, + -0.06056 + ], + [ + -0.17014, + 0.02801, + 0.16882 + ], + [ + 0.05223, + 0.02606, + -0.04947 + ], + [ + 0.06986, + 0.0125, + -0.05955 + ], + [ + -0.16994, + 0.02824, + 0.16987 + ], + [ + 0.05168, + 0.02605, + -0.05041 + ], + [ + -0.22704, + 0.06226, + 0.17987 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt": 1.326156383856757, + "DEF-skirt.L": 16.573658069216204, + "DEF-skirt.L.seg2": 4.007277618379357, + "DEF-skirt.L.004": 0.22795107245664514, + "DEF-skirt.L.seg3": 2.6757189730101847, + "DEF-skirt.L.seg3.seg1": 0.4743791069056792, + "DEF-skirt.L.001": 0.24252811304330446, + "DEF-skirt.L.003.seg1": 6.265238986924123, + "DEF-skirt.R.003": 4.080404754242859, + "DEF-skirt.L.seg1": 4.725564107140233, + "DEF-skirt.L.seg3.seg2": 0.42029319196388837, + "DEF-skirt.seg2": 0.40112976503480047, + "DEF-skirt.seg1": 0.10608606037822921, + "DEF-skirt.seg3": 0.017549561464065, + "DEF-skirt.R": 0.4003389939895228, + "DEF-skirt.003": 0.05178392383528613 + }, + { + "DEF-skirt.L.003": 6.265238986924123, + "DEF-skirt.R.003": 3.9250394935935145, + "DEF-skirt.L.003.seg2": 1.497939721902744, + "DEF-skirt.R.003.seg1": 0.007890762955204217, + "DEF-skirt.L.004": 0.31925299788853856, + "DEF-skirt.L.seg3.seg2": 0.3403666007961017, + "DEF-skirt.L.seg3": 0.22655295168951683, + "DEF-skirt.L.seg3.seg1": 0.16428564310004323, + "DEF-skirt.L.seg3.seg3": 0.05685497032801323, + "DEF-skirt.L.seg1": 0.6622802211922085, + "DEF-skirt.L": 1.6198222225340344, + "DEF-skirt.L.seg2": 0.1424029962977591, + "DEF-skirt.R": 0.26708816912305067 + }, + { + "DEF-skirt.R.003": 5.067051489313482, + "DEF-skirt.L.003.seg3": 1.4966581022228378, + "DEF-skirt.L.004": 0.7080545383425245, + "DEF-skirt.L.seg3": 0.10414642683356412, + "DEF-skirt.L.seg3.seg3": 0.47106880291962483, + "DEF-skirt.L.003.seg3.seg1": 0.09484742290937814, + "DEF-skirt.L.003.seg1": 1.497939721902744, + "DEF-skirt.R.003.seg1": 0.12163491119969849, + "DEF-skirt.L.seg3.seg2": 0.15569674127310984, + "DEF-skirt.L": 0.900557430807742, + "DEF-skirt.L.seg1": 0.32230547800630927, + "DEF-skirt.L.seg2": 0.16001926434900815, + "DEF-skirt.R": 0.566768981574711 + }, + { + "DEF-skirt.R.003": 4.979046773653648, + "DEF-skirt.L.003.seg2": 1.4966581022228378, + "DEF-skirt.L.004": 0.12941547114153995, + "DEF-skirt.L.seg3": 0.005636372948892854, + "DEF-skirt.R.003.seg1": 0.713636469437537, + "DEF-skirt.R.003.seg2": 0.36710195659358036, + "DEF-skirt.L.003.seg3.seg1": 1.7966355262485036, + "DEF-skirt.L": 1.6131563569904204, + "DEF-skirt.R": 0.9469972036340248 + }, + { + "DEF-skirt.L.004": 1.4763529281077483, + "DEF-skirt.L.003.seg3.seg2": 0.5748173600684865, + "DEF-skirt.R.003.seg3": 0.06104741856652929, + "DEF-skirt.L.seg3.seg3": 0.2645954510120382, + "DEF-skirt.L.003.seg2": 0.09484742290937814, + "DEF-skirt.R.003.seg1": 0.9853169053148589, + "DEF-skirt.R.003.seg2": 0.8474038647326166, + "DEF-skirt.L.003.seg3": 1.7966355262485036, + "DEF-skirt.L.seg3.seg2": 0.1895826561493289, + "DEF-skirt.L.seg3.seg1": 0.01731429169825205 + }, + { + "DEF-skirt.L.003.seg3.seg3": 0.6089500000103993, + "DEF-skirt.L.004": 2.272135412934264, + "DEF-skirt.R.003.seg3": 1.7964455407544628, + "DEF-skirt.L.003.seg3.seg1": 0.5748173600684865, + "DEF-skirt.L.seg3.seg3": 0.6523418590640699, + "DEF-skirt.L.004.seg1": 0.4210161758607365, + "DEF-skirt.R.004": 0.06144282445376237, + "DEF-skirt.L.seg3.seg2": 0.10142363563597101, + "DEF-skirt.L.seg3.seg1": 0.006523155712845785, + "DEF-skirt.R.003.seg3.seg1": 0.04110595294283381, + "DEF-skirt.L.005": 0.06826316269005073, + "DEF-skirt.R.005": 0.04556505075408657, + "DEF-hair.R.005": 0.07432172635976281 + }, + { + "DEF-skirt.L.005": 1.0710938956248561, + "DEF-skirt.L.004.seg2": 6.411217302681206, + "DEF-skirt.R.005.seg3.seg1": 0.1732640283463538, + "DEF-skirt.R.005.seg3.seg2": 0.1646855340041311, + "DEF-skirt.R.003.seg3.seg2": 1.5724887392051308, + "DEF-skirt.L.004.seg1": 7.3379864950231966, + "DEF-skirt.R.004": 1.4751940750756236, + "DEF-skirt.R.003.seg3.seg3": 1.1741019645273445, + "DEF-skirt.L.004": 1.1637832999210915, + "DEF-skirt.L.003.seg3.seg2": 0.6089500000103993, + "DEF-skirt.R.003.seg3": 0.27500241000605974, + "DEF-skirt.L.seg3.seg3": 0.1476984071768418, + "DEF-skirt.R.003.seg3.seg1": 1.0131085352053377, + "DEF-skirt.R.004.seg1": 0.15432767264401315, + "DEF-hair.R.005": 2.5491632031573146 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine.001", + "bones": [ + "DEF-skirt.R", + "DEF-skirt.R.seg1", + "DEF-skirt.R.seg2", + "DEF-skirt.R.seg3", + "DEF-skirt.R.seg3.seg1", + "DEF-skirt.R.seg3.seg2", + "DEF-skirt.R.seg3.seg3" + ], + "tips": [ + [ + -0.0, + 0.05443, + 0.0 + ], + [ + 0.0, + 0.05443, + -0.0 + ], + [ + -0.0, + 0.05443, + 0.0 + ], + [ + 0.0, + 0.04532, + -0.0 + ], + [ + 0.0, + 0.04532, + -0.0 + ], + [ + 0.0, + 0.04532, + -0.0 + ], + [ + 0.0, + 0.04532, + -0.0 + ] + ], + "hulls": [ + [ + [ + -0.02114, + 0.03224, + -0.06298 + ], + [ + 0.01753, + -0.04064, + 0.03519 + ], + [ + -0.03099, + -0.03708, + -0.04283 + ], + [ + -0.01695, + 0.00322, + 0.00621 + ], + [ + -0.04181, + 0.00588, + -0.02927 + ], + [ + -0.01038, + 0.04966, + -0.02862 + ], + [ + 0.0017, + -0.02936, + -0.02451 + ], + [ + 0.0158, + -0.00613, + 0.04503 + ], + [ + -0.00272, + -0.04123, + 0.00572 + ], + [ + 0.01175, + 0.0006, + 0.01497 + ] + ], + [ + [ + -0.03644, + 0.04124, + -0.07645 + ], + [ + 0.03166, + 0.01198, + 0.06806 + ], + [ + -0.03022, + 0.02067, + 0.00739 + ], + [ + 0.01239, + 0.02598, + 0.0297 + ], + [ + -0.01406, + 0.02014, + -0.0315 + ], + [ + -0.04153, + 0.04495, + -0.03669 + ], + [ + -0.00875, + 0.00158, + 0.04019 + ], + [ + -0.01189, + 0.04561, + -0.00244 + ], + [ + -0.01054, + 0.01781, + -0.06306 + ], + [ + -0.00548, + 0.0172, + -0.00441 + ] + ], + [ + [ + 0.03154, + 0.02968, + 0.0552 + ], + [ + -0.04492, + 0.00877, + -0.04155 + ], + [ + -0.00477, + 0.03216, + 0.00435 + ], + [ + -0.0023, + 0.02461, + -0.05262 + ], + [ + -0.03493, + 0.00104, + 0.00234 + ], + [ + -0.01098, + 0.02919, + -0.03038 + ], + [ + -0.00754, + 0.01281, + -0.00936 + ], + [ + 0.01201, + 0.03077, + 0.01999 + ], + [ + 0.02955, + 0.015, + 0.03963 + ], + [ + -0.01696, + 0.03229, + -0.01065 + ] + ], + [ + [ + 0.05712, + 0.01317, + 0.06953 + ], + [ + -0.00362, + 0.04128, + -0.05759 + ], + [ + -0.00775, + 0.03762, + 0.00733 + ], + [ + 0.02188, + 0.02474, + 0.04863 + ], + [ + 0.01522, + 0.04182, + -0.02127 + ], + [ + -0.01289, + 0.04336, + -0.03121 + ], + [ + 0.01382, + 0.02945, + 0.02435 + ], + [ + -0.02242, + 0.04395, + -0.0095 + ], + [ + 0.03199, + 0.02049, + 0.06707 + ], + [ + -0.02135, + 0.04401, + -0.00968 + ] + ], + [ + [ + 0.0345, + 0.02924, + 0.08346 + ], + [ + 0.0182, + 0.03624, + 0.03216 + ], + [ + 0.02543, + 0.03246, + 0.06159 + ], + [ + 0.01729, + 0.03623, + 0.03276 + ], + [ + 0.03515, + 0.02943, + 0.0826 + ], + [ + 0.02646, + 0.03247, + 0.06122 + ] + ], + [ + [ + -0.01443, + 0.02629, + -0.09103 + ], + [ + 0.02826, + 0.01797, + 0.06799 + ], + [ + -0.02581, + 0.01102, + -0.00523 + ], + [ + -0.00307, + 0.0053, + -0.05764 + ], + [ + 0.02009, + 0.00695, + 0.03396 + ], + [ + -0.006, + 0.03382, + 0.0149 + ], + [ + -0.00334, + 0.03774, + -0.05832 + ], + [ + -0.01511, + 0.02582, + -0.028 + ], + [ + -0.0073, + 0.00199, + 0.01261 + ], + [ + -0.01491, + 0.00937, + -0.08934 + ] + ], + [] + ], + "neighbours": [ + { + "DEF-skirt.R.003": 20.664438102943322, + "DEF-skirt.007": 18.976689667726873, + "DEF-skirt.007.seg1": 0.7743691673186472, + "DEF-skirt.R.seg1": 3.313681496957921, + "DEF-skirt.007.seg3": 0.5966556679136138, + "DEF-skirt.R.003.seg1": 0.9523467435374232, + "DEF-skirt.007.seg2": 0.1646787332505868, + "DEF-skirt.003.seg3": 0.04330813415148985, + "DEF-skirt.L.003.seg1": 0.26708816912305067, + "DEF-skirt.L": 0.4379191130505151, + "DEF-skirt.L.003": 0.4003389939895228, + "DEF-skirt.L.003.seg3": 0.9469972036340248, + "DEF-skirt.L.003.seg2": 0.566768981574711, + "DEF-skirt.003": 1.8412317503301705, + "DEF-skirt.R.003.seg2": 0.01543002782848818 + }, + { + "DEF-skirt.R.seg2": 3.963754055900017, + "DEF-skirt.007.seg3.seg1": 2.26178809882746, + "DEF-skirt.007.seg3.seg2": 0.03095310372400928, + "DEF-skirt.R.003.seg1": 2.6397020337397588, + "DEF-skirt.R": 3.313681496957921, + "DEF-skirt.007.seg3": 3.518399851047917, + "DEF-skirt.R.003.seg2": 0.2931021575939686, + "DEF-skirt.R.003": 1.2574131766197811, + "DEF-skirt.003.seg3": 0.03283304659289388, + "DEF-skirt.R.001": 0.05772151199563896, + "DEF-skirt.R.003.seg3": 0.025832215232391542 + }, + { + "DEF-skirt.R.003.seg2": 0.8756941281473589, + "DEF-skirt.R.003.seg3": 1.7333337308253811, + "DEF-skirt.R.001": 1.5069530565044718, + "DEF-skirt.R.seg1": 3.963754055900017, + "DEF-skirt.007.seg3.seg1": 2.0493244337048684, + "DEF-skirt.007.seg3.seg2": 0.1707793119895472, + "DEF-skirt.003.seg3.seg2": 0.016042945161717337, + "DEF-skirt.003.seg3.seg1": 0.010865782296369086, + "DEF-skirt.R.004": 0.16451946219773894, + "DEF-skirt.R.seg3": 0.09424409085089636, + "DEF-skirt.007.seg3": 0.35272205865821704, + "DEF-skirt.R.003.seg1": 0.04449534258909127 + }, + { + "DEF-skirt.R.seg3.seg1": 2.135797160238627, + "DEF-skirt.R.004.seg2": 0.008828194437185716, + "DEF-skirt.R.004.seg1": 0.10570511559391954, + "DEF-skirt.R.003.seg3.seg1": 1.2722399671513487, + "DEF-skirt.R.003.seg3.seg2": 0.07119304961039236, + "DEF-skirt.009.seg1": 0.019597212899127414, + "DEF-skirt.R.003.seg3": 0.3932421265374052, + "DEF-skirt.007.seg3.seg2": 0.1445767692500946, + "DEF-skirt.007.seg3.seg3": 0.4011411525419204, + "DEF-skirt.R.seg2": 0.09424409085089636, + "DEF-skirt.R.001": 0.37144023323246467, + "DEF-skirt.009": 0.061572100826446596, + "DEF-skirt.R.004": 0.216802611048998, + "DEF-skirt.003.seg3.seg2": 0.01747063797741255 + }, + { + "DEF-skirt.R.001.seg1": 0.17581185285518597, + "DEF-skirt.R.005": 0.022536525217506043, + "DEF-skirt.R.004.seg1": 0.09888366732501774, + "DEF-skirt.R.seg3.seg2": 1.3434585056200286, + "DEF-skirt.R.003.seg3.seg1": 0.5253397092460157, + "DEF-skirt.R.003.seg3.seg2": 1.0999419593470767, + "DEF-skirt.009.seg2": 0.23290165419269715, + "DEF-skirt.009.seg3.seg2": 0.05356704252333344, + "DEF-skirt.009.seg3.seg3": 0.019978953841021385, + "DEF-skirt.009.seg3": 0.16415101097724621, + "DEF-skirt.R.004.seg2": 0.018094137279125344, + "DEF-skirt.R.seg3": 2.135797160238627, + "DEF-skirt.007.seg3.seg2": 0.09501346050256937, + "DEF-skirt.007.seg3.seg3": 0.2975982906731034, + "DEF-skirt.R.003.seg3.seg3": 0.021905167671693704, + "DEF-skirt.R.001": 0.2535603123931991, + "DEF-skirt.R.003.seg3": 0.01640311467330441, + "DEF-skirt.009.seg3.seg1": 0.03219642002010603, + "DEF-skirt.009": 0.025827454314786835 + }, + { + "DEF-skirt.R.seg3.seg3": 1.173287191241684, + "DEF-skirt.R.003.seg3.seg2": 1.94829004622412, + "DEF-skirt.R.003.seg3.seg3": 0.6034120638045861, + "DEF-skirt.009.seg3.seg3": 0.6722838322683956, + "DEF-skirt.009.seg3.seg2": 0.6035550265175813, + "DEF-skirt.R.seg3.seg1": 1.3434585056200286, + "DEF-skirt.R.003.seg3.seg1": 0.18638949252082215, + "DEF-skirt.009.seg2": 0.4464423540921278, + "DEF-skirt.009.seg3": 0.20040029934984271, + "DEF-skirt.009.seg3.seg1": 0.5977067894809132 + }, + { + "DEF-skirt.009.seg3.seg3": 1.2648193653775424, + "DEF-skirt.R.seg3.seg2": 1.173287191241684, + "DEF-skirt.R.003.seg3.seg2": 0.3416790233670112, + "DEF-skirt.R.003.seg3.seg3": 0.16450454396592246, + "DEF-skirt.009.seg3.seg2": 0.17109059074600252, + "DEF-skirt.009.seg3.seg1": 0.07258592586402557 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine.001", + "bones": [ + "DEF-skirt.R.003", + "DEF-skirt.R.003.seg1", + "DEF-skirt.R.003.seg2", + "DEF-skirt.R.003.seg3", + "DEF-skirt.R.003.seg3.seg1", + "DEF-skirt.R.003.seg3.seg2", + "DEF-skirt.R.003.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.03347, + 0.0 + ], + [ + 0.0, + 0.03347, + -0.0 + ], + [ + -0.0, + 0.03347, + 0.0 + ], + [ + 0.0, + 0.05424, + 0.0 + ], + [ + -0.0, + 0.05424, + -0.0 + ], + [ + 0.0, + 0.05424, + -0.0 + ], + [ + 0.01997, + 0.0428, + 0.00059 + ] + ], + "hulls": [ + [ + [ + 0.08441, + -0.03265, + 0.07657 + ], + [ + -0.05994, + -0.02462, + -0.01868 + ], + [ + 0.04684, + 0.03075, + -0.00621 + ], + [ + 0.01437, + -0.03527, + -0.00805 + ], + [ + 0.05972, + -0.01943, + 0.03067 + ], + [ + -0.02106, + -0.00097, + 0.013 + ], + [ + 0.08081, + 0.02143, + 0.01002 + ], + [ + -0.02151, + -0.02616, + -0.02293 + ], + [ + 0.05128, + -0.00727, + -0.00235 + ], + [ + -0.05568, + 0.00448, + 0.0053 + ] + ], + [ + [ + -0.06047, + 0.01677, + -0.03231 + ], + [ + 0.00516, + 0.03249, + 0.00244 + ], + [ + -0.02776, + 0.02669, + -0.00895 + ], + [ + -0.04241, + 0.02239, + -0.02278 + ], + [ + -0.01253, + 0.03006, + 0.00094 + ], + [ + 0.00139, + 0.01878, + 0.00639 + ], + [ + -0.06013, + 0.00359, + -0.03558 + ], + [ + -0.04959, + 0.02001, + -0.02242 + ], + [ + -0.02713, + 0.02685, + -0.00984 + ], + [ + -0.01219, + 0.03018, + -9e-05 + ] + ], + [ + [ + -0.10588, + 0.01972, + -0.04355 + ], + [ + 0.04176, + 0.00027, + 0.00208 + ], + [ + 0.02368, + 0.00034, + 3e-05 + ], + [ + -0.1051, + 0.02009, + -0.04422 + ], + [ + 0.04184, + 0.00032, + 0.00099 + ], + [ + 0.02367, + 0.0003, + 0.00112 + ] + ], + [ + [ + -0.08658, + 0.01833, + -0.02765 + ], + [ + 0.06321, + 0.01478, + 0.01859 + ], + [ + -0.02092, + 0.02691, + 0.0053 + ], + [ + 0.02247, + 0.02808, + -0.0008 + ], + [ + -0.05619, + 0.01941, + -0.02115 + ], + [ + 0.04351, + 0.0273, + 0.00316 + ], + [ + 0.00015, + 0.02818, + 0.00422 + ], + [ + -0.03824, + 0.02375, + -0.00586 + ], + [ + 0.062, + 0.01598, + -0.00088 + ], + [ + -0.02908, + 0.01624, + 0.01605 + ] + ], + [ + [ + 0.06945, + 0.02769, + 0.00877 + ], + [ + -0.06566, + 0.02014, + -0.02231 + ], + [ + -0.02666, + 0.02952, + 0.00866 + ], + [ + 0.02302, + 0.02861, + -0.00064 + ], + [ + -0.04491, + 0.02546, + -0.00512 + ], + [ + 0.04728, + 0.02856, + 0.00364 + ], + [ + -0.02637, + 0.0296, + 0.00761 + ], + [ + -0.06517, + 0.0202, + -0.02328 + ], + [ + 0.04709, + 0.02853, + 0.00471 + ], + [ + 0.02305, + 0.02858, + 0.00045 + ] + ], + [ + [ + 0.07512, + 0.02606, + 0.01264 + ], + [ + -0.07358, + 0.01628, + -0.02424 + ], + [ + -0.00566, + 0.02729, + 0.00929 + ], + [ + -0.05143, + 0.03865, + -0.00483 + ], + [ + 0.0236, + 0.02459, + 0.00069 + ], + [ + -0.03187, + 0.02848, + 0.0129 + ], + [ + 0.05079, + 0.02573, + 0.00618 + ], + [ + -0.05067, + 0.02301, + -0.00385 + ], + [ + 0.07506, + 0.02612, + 0.01155 + ], + [ + -0.0731, + 0.01631, + -0.02521 + ] + ], + [ + [ + 0.05373, + 0.02172, + 0.00384 + ], + [ + -0.05409, + 0.01942, + -0.00813 + ], + [ + -0.00759, + 0.0236, + 0.00643 + ], + [ + 0.02437, + 0.01996, + -0.00373 + ], + [ + -0.00712, + 0.02246, + -0.00224 + ], + [ + 0.05389, + 0.02082, + 0.00324 + ], + [ + -0.00778, + 0.02273, + 0.0058 + ], + [ + -0.00703, + 0.02159, + -0.00158 + ], + [ + 0.02441, + 0.02091, + -0.00318 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.L.003.seg2": 5.067051489313482, + "DEF-skirt.L.003.seg3": 4.979046773653648, + "DEF-skirt.007": 1.045549316234618, + "DEF-skirt.R": 20.664438102943322, + "DEF-skirt.007.seg1": 0.22713862584992145, + "DEF-skirt.L.003.seg1": 3.9250394935935145, + "DEF-skirt.L.003": 4.080404754242859, + "DEF-skirt.L": 3.2402381303709697, + "DEF-skirt.R.seg1": 1.2574131766197811, + "DEF-skirt.R.003.seg1": 1.0807318309857437 + }, + { + "DEF-skirt.L.003.seg2": 0.12163491119969849, + "DEF-skirt.L.003.seg1": 0.007890762955204217, + "DEF-skirt.R.003.seg2": 2.41953127083724, + "DEF-skirt.L.003.seg3": 0.713636469437537, + "DEF-skirt.L.003.seg3.seg1": 0.9853169053148589, + "DEF-skirt.R.seg1": 2.6397020337397588, + "DEF-skirt.R": 0.9523467435374232, + "DEF-skirt.R.003": 1.0807318309857437, + "DEF-skirt.R.seg2": 0.04449534258909127 + }, + { + "DEF-skirt.R.seg2": 0.8756941281473589, + "DEF-skirt.R.003.seg3": 0.34858614662801096, + "DEF-skirt.R.001": 0.14694119993326393, + "DEF-skirt.R.003.seg1": 2.41953127083724, + "DEF-skirt.L.003.seg3": 0.36710195659358036, + "DEF-skirt.L.003.seg3.seg1": 0.8474038647326166, + "DEF-skirt.R.004": 0.01615965716406309, + "DEF-skirt.R.seg1": 0.2931021575939686, + "DEF-skirt.R": 0.01543002782848818 + }, + { + "DEF-skirt.L.003.seg3.seg3": 0.27500241000605974, + "DEF-skirt.L.004": 0.4550022757961103, + "DEF-skirt.L.003.seg3.seg2": 1.7964455407544628, + "DEF-skirt.L.003.seg3.seg1": 0.06104741856652929, + "DEF-skirt.R.seg2": 1.7333337308253811, + "DEF-skirt.R.003.seg2": 0.34858614662801096, + "DEF-skirt.R.004": 1.5048650248963797, + "DEF-skirt.R.001": 0.0656762465849694, + "DEF-skirt.R.seg3": 0.3932421265374052, + "DEF-skirt.R.003.seg3.seg1": 0.24624006919381858, + "DEF-skirt.R.seg3.seg1": 0.01640311467330441, + "DEF-skirt.R.004.seg1": 0.017631318202639384, + "DEF-skirt.R.005": 0.04879787543721603, + "DEF-skirt.L.005": 0.05610536797568777, + "DEF-skirt.R.seg1": 0.025832215232391542 + }, + { + "DEF-skirt.R.001.seg1": 0.0821030406636305, + "DEF-skirt.R.005": 0.016410842906184386, + "DEF-skirt.R.002": 0.00932822018436949, + "DEF-skirt.R.seg3.seg2": 0.18638949252082215, + "DEF-skirt.R.seg3.seg1": 0.5253397092460157, + "DEF-skirt.R.003.seg3.seg2": 0.44319472166767565, + "DEF-skirt.R.004.seg2": 0.01009878183119231, + "DEF-skirt.R.004.seg1": 0.12293567218380608, + "DEF-skirt.R.seg3": 1.2722399671513487, + "DEF-skirt.L.003.seg3.seg3": 1.0131085352053377, + "DEF-skirt.R.004": 1.9555913441755948, + "DEF-skirt.L.004.seg1": 0.7411719285966271, + "DEF-skirt.L.004": 0.24064906026527932, + "DEF-skirt.L.003.seg3.seg2": 0.04110595294283381, + "DEF-skirt.R.003.seg3": 0.24624006919381858, + "DEF-skirt.L.004.seg2": 0.05539768250199251 + }, + { + "DEF-skirt.R.005": 0.8597751017059654, + "DEF-skirt.L.005": 0.09981203683251794, + "DEF-skirt.L.003.seg3.seg3": 1.5724887392051308, + "DEF-skirt.R.005.seg3.seg1": 0.05431410032137607, + "DEF-skirt.R.005.seg3.seg2": 0.047532298070888546, + "DEF-skirt.L.004.seg2": 2.6459612955234846, + "DEF-skirt.R.004": 0.5334465694740952, + "DEF-skirt.R.003.seg3.seg3": 1.4925820758332369, + "DEF-skirt.L.004.seg1": 1.045940511472522, + "DEF-skirt.R.seg3.seg3": 0.3416790233670112, + "DEF-skirt.R.seg3.seg2": 1.94829004622412, + "DEF-skirt.R.004.seg2": 0.7231921967327264, + "DEF-skirt.R.004.seg1": 1.99888569391325, + "DEF-skirt.R.seg3.seg1": 1.0999419593470767, + "DEF-skirt.R.003.seg3.seg1": 0.44319472166767565, + "DEF-skirt.R.seg3": 0.07119304961039236, + "DEF-skirt.R.005.seg1": 0.11678245339473747 + }, + { + "DEF-skirt.L.004.seg2": 2.9099697763658727, + "DEF-skirt.L.003.seg3.seg3": 1.1741019645273445, + "DEF-skirt.R.004": 0.12212243476651818, + "DEF-skirt.R.003.seg3.seg2": 1.4925820758332369, + "DEF-skirt.R.seg3.seg3": 0.16450454396592246, + "DEF-skirt.R.seg3.seg2": 0.6034120638045861, + "DEF-skirt.R.004.seg2": 2.3902905726493904, + "DEF-skirt.L.004.seg3": 0.6609395685885446, + "DEF-skirt.R.004.seg1": 1.442350862938932, + "DEF-skirt.R.005": 0.12668430650905993, + "DEF-skirt.R.seg3.seg1": 0.021905167671693704 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine.001", + "bones": [ + "DEF-skirt.007", + "DEF-skirt.007.seg1", + "DEF-skirt.007.seg2", + "DEF-skirt.007.seg3", + "DEF-skirt.007.seg3.seg1", + "DEF-skirt.007.seg3.seg2", + "DEF-skirt.007.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.02335, + 0.0 + ], + [ + -0.0, + 0.02335, + 0.0 + ], + [ + -0.0, + 0.02335, + -0.0 + ], + [ + -0.0, + 0.04214, + -0.0 + ], + [ + -0.0, + 0.04214, + -0.0 + ], + [ + -0.0, + 0.04214, + 0.0 + ], + [ + -0.00785, + 0.01364, + 0.01563 + ] + ], + "hulls": [ + [ + [ + 0.07416, + 0.02127, + -0.0527 + ], + [ + -0.02712, + -0.02362, + 0.00114 + ], + [ + 0.04523, + -0.0157, + 0.01881 + ], + [ + 0.029, + 0.01019, + -0.02257 + ], + [ + 0.00178, + 0.0014, + 0.02398 + ], + [ + 0.01602, + -0.0364, + 0.01125 + ], + [ + 0.05428, + -0.02102, + -0.0099 + ], + [ + 0.02523, + 0.01242, + 0.00803 + ], + [ + -0.00408, + -0.00653, + -0.00666 + ], + [ + 0.04906, + 0.00665, + 0.00352 + ] + ], + [ + [ + 0.03808, + 0.02265, + -0.08554 + ], + [ + 0.02253, + 0.01262, + -0.04982 + ], + [ + 0.01323, + 0.01748, + -0.09036 + ], + [ + 0.02545, + 0.01511, + -0.05991 + ], + [ + 0.01421, + 0.01749, + -0.08988 + ], + [ + 0.02174, + 0.01292, + -0.05051 + ], + [ + 0.02635, + 0.0152, + -0.0593 + ], + [ + 0.03733, + 0.02202, + -0.08505 + ] + ], + [ + [ + 0.01812, + 0.00211, + -0.04788 + ], + [ + 0.01567, + 0.01366, + -0.02762 + ], + [ + 0.01857, + 0.00977, + -0.03272 + ], + [ + 0.01752, + 0.01005, + -0.0328 + ], + [ + 0.01709, + 0.00219, + -0.04823 + ], + [ + 0.0163, + 0.01437, + -0.02815 + ] + ], + [ + [ + 0.03408, + 0.03455, + -0.03196 + ], + [ + -0.02179, + 0.01171, + 0.00989 + ], + [ + -0.00974, + 0.04208, + -0.00746 + ], + [ + 0.00785, + 0.01007, + -0.03639 + ], + [ + 0.00907, + 0.01469, + 0.00622 + ], + [ + -0.01197, + 0.02357, + -0.00372 + ], + [ + 0.01274, + 0.02726, + -0.04193 + ], + [ + -0.01718, + 0.01174, + 0.00524 + ], + [ + -0.01713, + 0.01191, + 0.00988 + ], + [ + -0.02178, + 0.01154, + 0.00528 + ] + ], + [ + [ + 0.08409, + 0.01101, + -0.07622 + ], + [ + -0.02335, + 0.02357, + 0.02321 + ], + [ + 0.01677, + 0.00173, + -0.04782 + ], + [ + 0.01838, + 0.03233, + -0.00091 + ], + [ + 0.0452, + 0.02394, + -0.0586 + ], + [ + -0.00882, + 0.01879, + -0.01193 + ], + [ + 0.00557, + 0.01864, + 0.02368 + ], + [ + 0.01208, + 0.00547, + -0.00097 + ], + [ + 0.0348, + 0.02881, + -0.01657 + ], + [ + 0.06289, + 0.01809, + -0.06817 + ] + ], + [ + [ + 0.09818, + 0.02637, + -0.09172 + ], + [ + 0.07235, + 0.03546, + -0.08462 + ], + [ + 0.07274, + 0.03568, + -0.08363 + ], + [ + 0.09882, + 0.02647, + -0.09084 + ] + ], + [ + [ + 0.05003, + 0.00083, + -0.07647 + ], + [ + -0.02579, + 0.00546, + 0.02943 + ], + [ + 0.01961, + 0.00974, + -0.00691 + ], + [ + 0.04063, + 0.00441, + -0.02433 + ], + [ + 0.04585, + 0.00297, + -0.05191 + ], + [ + 0.00158, + 0.01358, + 0.00804 + ], + [ + -0.0202, + 0.01102, + 0.02926 + ], + [ + -0.02117, + 0.00705, + 0.02506 + ], + [ + -0.0258, + 0.00686, + 0.02501 + ], + [ + -0.02116, + 0.00566, + 0.02947 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.R.003": 1.045549316234618, + "DEF-skirt.R": 18.976689667726873, + "DEF-skirt.007.seg1": 1.8316867976512792, + "DEF-skirt.003": 17.307897292439947, + "DEF-skirt": 0.09740479559841991, + "DEF-skirt.003.seg3": 1.5368390145847437, + "DEF-skirt.seg1": 0.05097562226131125, + "DEF-skirt.003.seg2": 0.17083154774496911 + }, + { + "DEF-skirt.007": 1.8316867976512792, + "DEF-skirt.R.003": 0.22713862584992145, + "DEF-skirt.R": 0.7743691673186472, + "DEF-skirt.seg2": 0.10602302515859996, + "DEF-skirt.003.seg3.seg1": 0.6158521164263719, + "DEF-skirt.003.seg3.seg2": 0.06492517685354893, + "DEF-skirt.007.seg2": 0.7156527587634578, + "DEF-skirt.003.seg3": 2.2966332003201027, + "DEF-skirt.003": 0.03732637437143671 + }, + { + "DEF-skirt.seg1": 0.013958547848881953, + "DEF-skirt.seg2": 0.062276340879366066, + "DEF-skirt.003.seg3.seg1": 0.666448692941354, + "DEF-skirt.003.seg3.seg2": 0.24037750336559105, + "DEF-skirt.seg3.seg2": 0.027715837136825017, + "DEF-skirt.005": 0.017320884952562676, + "DEF-skirt.seg3.seg1": 0.022665236298227698, + "DEF-skirt.003.seg3.seg3": 0.027393346065210267, + "DEF-skirt.007.seg1": 0.7156527587634578, + "DEF-skirt.003.seg3": 1.053151290505093, + "DEF-skirt.007.seg3.seg1": 0.016657051146981294, + "DEF-skirt.007.seg3": 0.6552197899116843, + "DEF-skirt.R": 0.1646787332505868 + }, + { + "DEF-skirt.003.seg3.seg2": 0.05651045472693261, + "DEF-skirt.seg1": 0.013931251322537197, + "DEF-skirt.003.seg3.seg1": 0.11857959460915246, + "DEF-skirt.007.seg3.seg1": 3.1417127205232833, + "DEF-skirt.R": 0.5966556679136138, + "DEF-skirt.R.seg1": 3.518399851047917, + "DEF-skirt.007.seg2": 0.6552197899116843, + "DEF-skirt.003.seg3": 0.24521446761690108, + "DEF-skirt.R.seg2": 0.35272205865821704 + }, + { + "DEF-skirt.003.seg3.seg2": 0.7782740178336178, + "DEF-skirt.003.seg3.seg1": 0.4365502716799038, + "DEF-skirt.R.seg1": 2.26178809882746, + "DEF-skirt.R.seg2": 2.0493244337048684, + "DEF-skirt.007.seg3.seg2": 2.11863794450706, + "DEF-skirt.seg1": 0.10326649938885435, + "DEF-skirt.007.seg3": 3.1417127205232833, + "DEF-skirt.R.001": 0.09218237179130262, + "DEF-skirt.seg2": 0.007769999773823699, + "DEF-skirt.007.seg2": 0.016657051146981294, + "DEF-skirt.seg3.seg2": 0.018364528679975622, + "DEF-skirt.003.seg3.seg3": 0.03659960675887097, + "DEF-skirt.005": 0.014676506286199487, + "DEF-skirt.seg3.seg1": 0.010801484237957126 + }, + { + "DEF-skirt.R.seg1": 0.03095310372400928, + "DEF-skirt.R.seg2": 0.1707793119895472, + "DEF-skirt.007.seg3.seg1": 2.11863794450706, + "DEF-skirt.003.seg3.seg2": 0.04805373450254019, + "DEF-skirt.003.seg3.seg1": 0.020351700754680624, + "DEF-skirt.seg3.seg2": 0.10017902235084042, + "DEF-skirt.005": 0.12732373542491637, + "DEF-skirt.003.seg3.seg3": 0.5731106537743211, + "DEF-skirt.seg3.seg1": 0.03270002877903588, + "DEF-skirt.009": 3.886224157806516, + "DEF-skirt.007.seg3.seg3": 5.478468437728212, + "DEF-skirt.R.seg3.seg1": 0.09501346050256937, + "DEF-skirt.R.seg3": 0.1445767692500946, + "DEF-skirt.seg2": 0.14838076659398158, + "DEF-skirt.009.seg1": 0.46220673472213736 + }, + { + "DEF-skirt.R.001": 0.1038098999130459, + "DEF-skirt.009.seg2": 4.7681596275634295, + "DEF-skirt.010": 0.09792058046352847, + "DEF-skirt.009.seg3": 1.2296093987857097, + "DEF-skirt.009.seg1": 2.3938596563287686, + "DEF-skirt.seg3.seg3": 0.16922538313054947, + "DEF-skirt.005.seg1": 0.5804974576499621, + "DEF-skirt.009": 5.738163745911422, + "DEF-skirt.005.seg2": 0.19316734074695407, + "DEF-skirt.007.seg3.seg2": 5.478468437728212, + "DEF-skirt.R.seg3.seg1": 0.2975982906731034, + "DEF-skirt.R.seg3": 0.4011411525419204, + "DEF-skirt.003.seg3.seg3": 0.160640208594645, + "DEF-skirt.seg2": 0.04492484052275092, + "DEF-skirt.009.seg3.seg2": 0.2980328553075786 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.002", + "DEF-skirt.002.seg1", + "DEF-skirt.002.seg2", + "DEF-skirt.002.seg3" + ], + "tips": [ + [ + -0.0, + 0.02131, + -0.0 + ], + [ + 0.0, + 0.02131, + 0.0 + ], + [ + -0.0, + 0.02131, + 0.0 + ], + [ + 0.00417, + 0.04451, + 0.05542 + ] + ], + "hulls": [ + [ + [ + 0.00491, + 0.02036, + 0.02338 + ], + [ + 0.00118, + -0.01008, + -0.00922 + ], + [ + -0.00535, + 0.02023, + 0.02123 + ], + [ + 0.0043, + 0.02083, + 0.0226 + ], + [ + -0.00449, + 0.02079, + 0.02086 + ], + [ + 0.00206, + -0.00953, + -0.00957 + ] + ], + [], + [ + [ + -0.00599, + 0.00129, + -0.04552 + ], + [ + 0.00173, + 0.0009, + -0.04551 + ], + [ + 0.00111, + 0.00086, + -0.04461 + ], + [ + -0.00544, + 0.00115, + -0.04459 + ] + ], + [ + [ + 0.00349, + 0.00442, + 0.04343 + ], + [ + -0.0032, + 0.02814, + 0.02127 + ], + [ + 0.00456, + 0.02879, + 0.02285 + ], + [ + 0.00243, + 0.00429, + 0.04321 + ], + [ + 0.00375, + 0.0281, + 0.0231 + ], + [ + -0.00265, + 0.02743, + 0.02189 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.002.seg1": 0.9936210139550372, + "DEF-skirt.011.seg2": 0.6140337540771341, + "DEF-skirt.011.seg1": 1.0693271346551072, + "DEF-skirt.L.001": 0.18002089240525332, + "DEF-skirt.seg3.seg2": 0.1676262080621833, + "DEF-skirt.011": 0.19676162578912937, + "DEF-skirt.seg3.seg3": 0.08269355505130749 + }, + { + "DEF-skirt.002.seg2": 1.1073648990550695, + "DEF-skirt.011.seg3": 0.32109892175589216, + "DEF-skirt.002": 0.9936210139550372, + "DEF-skirt.011.seg2": 0.6766831613899087, + "DEF-skirt.L.001": 0.3520336011105645, + "DEF-skirt.L.002": 0.06381509987110245 + }, + { + "DEF-skirt.002.seg1": 1.1073648990550695, + "DEF-skirt.011.seg3": 0.5918997154080832, + "DEF-skirt.002.seg3": 0.276950550635245, + "DEF-skirt.L.001.seg1": 0.06224908278966823, + "DEF-skirt.L.001": 0.058560889596170895, + "DEF-skirt.011.seg2": 0.11717771662678933 + }, + { + "DEF-skirt.L.001.seg1": 0.49871227853337197, + "DEF-skirt.011.seg3": 1.0194056366657809, + "DEF-skirt.L.002.seg1": 0.29504055599472157, + "DEF-skirt.002.seg2": 0.276950550635245, + "DEF-skirt.011.seg2": 0.07863041193671272 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.011", + "DEF-skirt.011.seg1", + "DEF-skirt.011.seg2", + "DEF-skirt.011.seg3", + "DEF-skirt.011.seg3.seg1", + "DEF-skirt.011.seg3.seg2", + "DEF-skirt.011.seg3.seg3" + ], + "tips": [ + [ + -0.0, + 0.04148, + 0.0 + ], + [ + -0.0, + 0.04148, + 0.0 + ], + [ + 0.0, + 0.04148, + 0.0 + ], + [ + 0.0, + 0.02728, + -0.0 + ], + [ + -0.0, + 0.02728, + -0.0 + ], + [ + -0.0, + 0.02728, + -0.0 + ], + [ + -0.03033, + 0.03872, + 0.00948 + ] + ], + "hulls": [ + [ + [ + 0.04555, + 0.01719, + 0.05065 + ], + [ + -0.01818, + 0.0315, + -0.01141 + ], + [ + 0.04426, + 0.04093, + 0.00452 + ], + [ + -0.01417, + 0.04083, + 0.02064 + ], + [ + 0.01652, + 0.02852, + 0.01278 + ], + [ + 0.0061, + 0.04136, + -0.0152 + ], + [ + 0.04377, + 0.02788, + 0.02971 + ], + [ + -0.00782, + 0.01913, + 0.01736 + ], + [ + 0.01608, + 0.02948, + 0.01247 + ], + [ + 0.00605, + 0.04099, + -0.01622 + ] + ], + [ + [ + -0.03298, + 0.02109, + -0.1014 + ], + [ + -0.02323, + 0.03402, + 0.07534 + ], + [ + 0.00949, + 0.00032, + -0.02296 + ], + [ + 0.044, + 0.0191, + 0.03982 + ], + [ + -0.03797, + 0.03755, + -0.02293 + ], + [ + -0.02195, + 0.00929, + 0.01784 + ], + [ + 0.0147, + 0.03475, + -0.05242 + ], + [ + -0.02705, + 0.01748, + -0.06127 + ], + [ + 0.00314, + 0.0331, + -0.00371 + ], + [ + 0.01176, + 0.03718, + 0.04379 + ] + ], + [ + [ + 0.00183, + 0.02126, + 0.06646 + ], + [ + -0.00828, + 0.0029, + -0.04347 + ], + [ + 0.05092, + 0.0052, + 0.02605 + ], + [ + 0.0205, + 0.03405, + -0.00684 + ], + [ + -0.00219, + 0.02441, + 0.02832 + ], + [ + -0.012, + 0.00779, + -0.00708 + ], + [ + 0.02938, + 0.00509, + -0.0261 + ], + [ + 0.02464, + 0.00163, + 0.01023 + ], + [ + 0.01711, + 0.04353, + 0.01798 + ], + [ + -0.03066, + 0.01182, + -0.03711 + ] + ], + [], + [ + [ + -0.01417, + 0.02176, + -0.00419 + ], + [ + -0.00653, + 0.02381, + -0.00317 + ], + [ + -0.00735, + 0.02386, + -0.00245 + ], + [ + -0.01373, + 0.02204, + -0.00324 + ] + ], + [], + [] + ], + "neighbours": [ + { + "DEF-skirt.011.seg1": 4.283922914470186, + "DEF-skirt.002": 0.19676162578912937, + "DEF-skirt.seg3.seg2": 0.6159119122636163, + "DEF-skirt.seg3.seg3": 1.8142268693202859, + "DEF-skirt.005.seg2": 0.5038940231942797, + "DEF-skirt.005.seg1": 1.63718272292887, + "DEF-skirt.003.seg3.seg3": 1.1737566849790808, + "DEF-skirt.seg3": 0.9369690571555245, + "DEF-skirt.seg3.seg1": 1.2212581862234682, + "DEF-skirt.005": 0.08082041662935369, + "DEF-skirt.003.seg3.seg2": 0.23261883158359986, + "DEF-skirt.L.seg3.seg1": 0.08485950409668952, + "DEF-skirt.seg2": 0.10947330483127027 + }, + { + "DEF-skirt.011": 4.283922914470186, + "DEF-skirt.011.seg2": 7.330714473832888, + "DEF-skirt.002": 1.0693271346551072, + "DEF-skirt.L.001": 1.8334504302259091, + "DEF-skirt.seg3.seg2": 1.9490406902957946, + "DEF-skirt.seg3.seg3": 3.841728980444033, + "DEF-skirt.005.seg2": 3.467399917835377, + "DEF-skirt.003.seg3.seg3": 1.22743385271503, + "DEF-skirt.L.seg3.seg3": 0.4505585897398284, + "DEF-skirt.L.seg3.seg2": 0.15118301815151153, + "DEF-skirt.005.seg1": 0.9020129951405644, + "DEF-skirt.seg3.seg1": 0.07114842230691942, + "DEF-skirt.L.001.seg1": 0.04079192576080093 + }, + { + "DEF-skirt.011.seg1": 7.330714473832888, + "DEF-skirt.002": 0.6140337540771341, + "DEF-skirt.002.seg1": 0.6766831613899087, + "DEF-skirt.L.001": 1.1580020121598626, + "DEF-skirt.002.seg2": 0.11717771662678933, + "DEF-skirt.L.001.seg1": 0.76678062536886, + "DEF-skirt.002.seg3": 0.07863041193671272, + "DEF-skirt.011.seg3": 1.5599338025048093, + "DEF-skirt.L.002": 0.08959631170822746, + "DEF-skirt.L.seg3.seg3": 3.6437474555280827, + "DEF-skirt.005.seg2": 1.5689115516411916, + "DEF-skirt.seg3.seg3": 2.3263942659603747, + "DEF-skirt.011.seg3.seg1": 0.5091501426894913, + "DEF-skirt.011.seg3.seg2": 0.2990211070015478, + "DEF-skirt.seg3.seg2": 0.4824420103059537, + "DEF-skirt.L.seg3.seg2": 0.08240344936733568, + "DEF-skirt.011.seg3.seg3": 0.23419562473822533, + "DEF-skirt.003.seg3.seg3": 0.1914863875935997, + "DEF-skirt.seg3.seg1": 0.1828297678733849, + "DEF-skirt.005.seg1": 0.0521996686653603, + "DEF-skirt.L.001.seg2": 0.02610022222928776 + }, + { + "DEF-skirt.002.seg2": 0.5918997154080832, + "DEF-skirt.002.seg1": 0.32109892175589216, + "DEF-skirt.002.seg3": 1.0194056366657809, + "DEF-skirt.L.001.seg1": 0.20992149750158084, + "DEF-skirt.L.002.seg1": 0.14250133274444488, + "DEF-skirt.011.seg2": 1.5599338025048093, + "DEF-skirt.L.seg3.seg3": 0.5129986719242634, + "DEF-skirt.011.seg3.seg1": 0.1458175845727503, + "DEF-skirt.005.seg2": 0.06755428213227821, + "DEF-skirt.seg3.seg3": 0.030706543998581193 + }, + { + "DEF-skirt.L.seg3.seg3": 0.6725704569200012, + "DEF-skirt.011.seg2": 0.5091501426894913, + "DEF-skirt.011.seg3.seg2": 0.31035160060700284, + "DEF-skirt.011.seg3": 0.1458175845727503 + }, + { + "DEF-skirt.L.seg3.seg3": 0.9627032716458411, + "DEF-skirt.011.seg2": 0.2990211070015478, + "DEF-skirt.011.seg3.seg1": 0.31035160060700284, + "DEF-skirt.011.seg3.seg3": 0.3847642450467892 + }, + { + "DEF-skirt.L.seg3.seg3": 2.4290242543951024, + "DEF-skirt.011.seg2": 0.23419562473822533, + "DEF-skirt.011.seg3.seg2": 0.3847642450467892, + "DEF-skirt.seg3.seg3": 0.048795000199878036, + "DEF-skirt.seg3.seg2": 0.05145194199315073, + "DEF-skirt.L.001.seg1": 0.2004773422902515, + "DEF-skirt.L.001.seg2": 0.022788314650780933 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.006", + "DEF-skirt.006.seg1", + "DEF-skirt.006.seg2", + "DEF-skirt.006.seg3", + "DEF-skirt.006.seg3.seg1", + "DEF-skirt.006.seg3.seg2", + "DEF-skirt.006.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.00893, + -0.0 + ], + [ + -0.0, + 0.00893, + 0.0 + ], + [ + 0.0, + 0.00893, + -0.0 + ], + [ + 0.0, + 0.01124, + 0.0 + ], + [ + 0.0, + 0.01124, + 0.0 + ], + [ + 0.0, + 0.01123, + 0.0 + ], + [ + -0.00277, + 0.01039, + -0.00047 + ] + ], + "hulls": [ + [ + [ + 0.00216, + -0.04675, + -0.00554 + ], + [ + 0.00152, + -0.01123, + -0.00176 + ], + [ + 0.00053, + -0.01138, + -0.00133 + ], + [ + 0.00319, + -0.04662, + -0.00587 + ] + ], + [], + [], + [ + [ + 0.00747, + 0.00667, + 0.00618 + ], + [ + -0.0012, + 0.00577, + -0.00316 + ], + [ + -0.00161, + 0.00576, + -0.00214 + ], + [ + 0.00646, + 0.0065, + 0.00582 + ] + ], + [], + [], + [ + [ + -0.00022, + 0.00577, + 0.00459 + ], + [ + -0.00165, + 0.00543, + -0.003 + ], + [ + -0.00083, + 0.00491, + 0.0043 + ], + [ + -0.00197, + 0.00482, + -0.00215 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.005.seg1": 1.3111212178497302, + "DEF-skirt.005.seg2": 0.3865039067287408, + "DEF-skirt.005": 0.2789239352006696, + "DEF-skirt.003.seg3.seg3": 0.3897890162556754, + "DEF-skirt.seg3.seg3": 0.06978071994562751 + }, + {}, + {}, + { + "DEF-skirt.005.seg3": 0.24084290057003344, + "DEF-skirt.005.seg2": 0.6022493807084146, + "DEF-skirt.006.seg3.seg1": 0.15229559347173627 + }, + { + "DEF-skirt.006.seg3": 0.15229559347173627 + }, + { + "DEF-skirt.006.seg3.seg3": 0.19128683580063743 + }, + { + "DEF-skirt.006.seg3.seg2": 0.19128683580063743 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.005", + "DEF-skirt.005.seg1", + "DEF-skirt.005.seg2", + "DEF-skirt.005.seg3" + ], + "tips": [ + [ + -0.0, + 0.04557, + 0.0 + ], + [ + 0.0, + 0.04557, + -0.0 + ], + [ + 0.0, + 0.04557, + -0.0 + ], + [ + 0.0, + 0.04557, + -0.0 + ] + ], + "hulls": [ + [], + [ + [ + 0.0983, + 0.02356, + 0.03977 + ], + [ + -0.07599, + 0.02351, + 0.00106 + ], + [ + 0.0183, + 0.01346, + 0.04315 + ], + [ + -0.02303, + 0.03049, + -0.02364 + ], + [ + -0.07115, + 0.00272, + 0.03444 + ], + [ + 0.02439, + 0.03557, + 0.01084 + ], + [ + -0.0011, + 0.01791, + 9e-05 + ], + [ + 0.04661, + 0.01346, + 0.04007 + ], + [ + -0.00419, + 0.01087, + 0.0282 + ], + [ + -0.0011, + 0.0334, + -0.00759 + ] + ], + [ + [ + -0.08284, + 0.01524, + -0.00176 + ], + [ + 0.0806, + 0.01055, + 0.01715 + ], + [ + -0.00087, + 0.0032, + -0.02656 + ], + [ + 0.02081, + 0.01037, + 0.02289 + ], + [ + -0.05224, + 0.01847, + -0.02383 + ], + [ + 0.06023, + 0.0273, + -0.00431 + ], + [ + -0.02686, + 0.00486, + -0.00898 + ], + [ + -0.02613, + 0.02092, + -0.0363 + ], + [ + 0.02408, + 0.02704, + 0.00052 + ], + [ + -0.00454, + 0.02534, + -0.01046 + ] + ], + [] + ], + "neighbours": [ + { + "DEF-skirt.006": 0.2789239352006696, + "DEF-skirt.005.seg1": 0.7202991839892734, + "DEF-skirt.003.seg3.seg3": 1.1916771231975871, + "DEF-skirt.seg3.seg1": 0.6138460617352536, + "DEF-skirt.003.seg3.seg2": 0.9896900858102016, + "DEF-skirt.011": 0.08082041662935369, + "DEF-skirt.009": 0.5119326758415601, + "DEF-skirt.009.seg1": 0.3651368285367087, + "DEF-skirt.seg2": 0.3701904039963759, + "DEF-skirt.seg1": 0.12456313680711745, + "DEF-skirt.seg3.seg2": 0.41073438050864913, + "DEF-skirt.007.seg3.seg2": 0.12732373542491637, + "DEF-skirt.007.seg2": 0.017320884952562676, + "DEF-skirt.seg3.seg3": 0.2100940425553619, + "DEF-skirt.003.seg3.seg1": 0.2785878408378042, + "DEF-skirt.007.seg3.seg1": 0.014676506286199487 + }, + { + "DEF-skirt.006": 1.3111212178497302, + "DEF-skirt.005.seg2": 1.8384273369901567, + "DEF-skirt.005": 0.7202991839892734, + "DEF-skirt.003.seg3.seg3": 4.719704061300272, + "DEF-skirt.seg3.seg3": 3.8134393417781873, + "DEF-skirt.011": 1.63718272292887, + "DEF-skirt.009": 1.0724076109246674, + "DEF-skirt.011.seg1": 0.9020129951405644, + "DEF-skirt.seg3.seg2": 0.2788327945314417, + "DEF-skirt.009.seg1": 1.3243936900477413, + "DEF-skirt.010": 0.09132764503221136, + "DEF-skirt.009.seg2": 0.508727444611632, + "DEF-skirt.007.seg3.seg3": 0.5804974576499621, + "DEF-skirt.011.seg2": 0.0521996686653603 + }, + { + "DEF-skirt.005.seg3": 0.0780807621047086, + "DEF-skirt.006.seg3": 0.6022493807084146, + "DEF-skirt.006": 0.3865039067287408, + "DEF-skirt.005.seg1": 1.8384273369901567, + "DEF-skirt.seg3.seg3": 5.03848026532854, + "DEF-skirt.009": 0.1522403836076065, + "DEF-skirt.003.seg3.seg3": 5.299175017577895, + "DEF-skirt.011.seg1": 3.467399917835377, + "DEF-skirt.011.seg2": 1.5689115516411916, + "DEF-skirt.011": 0.5038940231942797, + "DEF-skirt.L.seg3.seg3": 0.03948786979380725, + "DEF-skirt.009.seg1": 1.1706993434027797, + "DEF-skirt.009.seg2": 0.9486564797789065, + "DEF-skirt.011.seg3": 0.06755428213227821, + "DEF-skirt.007.seg3.seg3": 0.19316734074695407, + "DEF-skirt.010": 0.023801487298321877 + }, + { + "DEF-skirt.005.seg2": 0.0780807621047086, + "DEF-skirt.006.seg3": 0.24084290057003344 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.L.002", + "DEF-skirt.L.002.seg1", + "DEF-skirt.L.002.seg2", + "DEF-skirt.L.002.seg3" + ], + "tips": [ + [ + 0.0, + 0.02712, + -0.0 + ], + [ + -0.0, + 0.02712, + -0.0 + ], + [ + 0.0, + 0.02712, + -0.0 + ], + [ + 0.0, + 0.02712, + -0.0 + ] + ], + "hulls": [ + [], + [ + [ + -0.00691, + 0.01182, + -0.01495 + ], + [ + 0.01093, + 0.00848, + -0.00776 + ], + [ + 0.00993, + 0.00866, + -0.00817 + ], + [ + -0.00598, + 0.01145, + -0.01452 + ] + ], + [ + [ + 0.01871, + 0.01042, + -0.01446 + ], + [ + 0.01152, + 0.01194, + -0.01734 + ], + [ + 0.01177, + 0.01093, + -0.01699 + ], + [ + 0.01778, + 0.00986, + -0.01463 + ] + ], + [] + ], + "neighbours": [ + { + "DEF-skirt.L.001.seg1": 1.2521681773165678, + "DEF-skirt.L.001.seg2": 0.9059128727013415, + "DEF-skirt.L.002.seg1": 0.195486780885185, + "DEF-skirt.L.005": 0.13568216323168258, + "DEF-skirt.011.seg2": 0.08959631170822746, + "DEF-skirt.L.001": 0.06609502404905765, + "DEF-skirt.002.seg1": 0.06381509987110245, + "DEF-skirt.L.004": 0.8474234237006839 + }, + { + "DEF-skirt.002.seg3": 0.29504055599472157, + "DEF-skirt.L.001.seg1": 0.17224243855583787, + "DEF-skirt.011.seg3": 0.14250133274444488, + "DEF-skirt.L.002.seg2": 0.662231698639228, + "DEF-skirt.L.001.seg2": 0.4638847820902827, + "DEF-skirt.L.002": 0.195486780885185, + "DEF-skirt.L.005.seg2": 0.07711775446670888, + "DEF-skirt.L.001.seg3": 0.09464254593464361 + }, + { + "DEF-skirt.L.002.seg1": 0.662231698639228, + "DEF-skirt.L.001.seg3": 0.403548752663742, + "DEF-skirt.L.001.seg2": 0.2637003859272131, + "DEF-skirt.L.005.seg3": 0.09306542628326586, + "DEF-skirt.L.002.seg3": 0.10320578579895667, + "DEF-skirt.L.005.seg2": 0.1092308976699008 + }, + { + "DEF-skirt.L.001.seg3": 0.5443336104188372, + "DEF-skirt.L.001.seg2": 0.07451138760554099, + "DEF-skirt.L.005.seg3": 0.18788502624155878, + "DEF-skirt.L.002.seg2": 0.10320578579895667 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.L.001", + "DEF-skirt.L.001.seg1", + "DEF-skirt.L.001.seg2", + "DEF-skirt.L.001.seg3" + ], + "tips": [ + [ + 0.0, + 0.05706, + -0.0 + ], + [ + 0.0, + 0.05706, + 0.0 + ], + [ + -0.0, + 0.05706, + 0.0 + ], + [ + -0.01745, + 0.0433, + 0.00879 + ] + ], + "hulls": [ + [], + [ + [ + 0.0453, + 0.03397, + -0.03352 + ], + [ + -0.02599, + 0.04117, + 0.03719 + ], + [ + 0.01442, + 0.0089, + -0.03226 + ], + [ + 0.03648, + 0.00736, + -0.02149 + ], + [ + 0.02389, + 0.0376, + -0.04501 + ], + [ + -0.03656, + 0.03795, + 0.02107 + ], + [ + 0.02876, + 0.0087, + -0.02573 + ], + [ + 0.03719, + 0.03412, + -0.0371 + ], + [ + 0.04134, + 0.03589, + -0.03638 + ], + [ + 0.03182, + 0.0073, + -0.02351 + ] + ], + [ + [ + -0.01266, + 0.05119, + 0.01588 + ], + [ + -0.03027, + 0.01093, + 0.01132 + ], + [ + -0.02001, + 0.01423, + 0.02762 + ], + [ + -0.02291, + 0.04811, + -0.00045 + ], + [ + -0.02058, + 0.01404, + 0.02671 + ], + [ + -0.02969, + 0.01111, + 0.01223 + ], + [ + -0.01323, + 0.051, + 0.01496 + ], + [ + -0.02238, + 0.0481, + 0.00051 + ] + ], + [ + [ + -0.0072, + 0.02243, + 0.00665 + ], + [ + -0.01133, + 0.02133, + 2e-05 + ], + [ + -0.01118, + 0.02051, + 0.00072 + ], + [ + -0.00772, + 0.0216, + 0.00617 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.002.seg1": 0.3520336011105645, + "DEF-skirt.011.seg2": 1.1580020121598626, + "DEF-skirt.002.seg2": 0.058560889596170895, + "DEF-skirt.011.seg1": 1.8334504302259091, + "DEF-skirt.002": 0.18002089240525332, + "DEF-skirt.seg3.seg2": 0.2335541231476439, + "DEF-skirt.L.001.seg1": 1.6723670399728074, + "DEF-skirt.L.002": 0.06609502404905765, + "DEF-skirt.L.seg3": 1.1794753370285003, + "DEF-skirt.L.seg3.seg1": 0.4072310229783051, + "DEF-skirt.seg3": 0.15234850550117335, + "DEF-skirt.L.003": 0.24252811304330446, + "DEF-skirt.L.seg3.seg2": 0.2261690626343804, + "DEF-skirt.seg3.seg1": 0.02105309606015293, + "DEF-skirt.L.seg2": 0.10537824781740257 + }, + { + "DEF-skirt.002.seg3": 0.49871227853337197, + "DEF-skirt.011.seg3": 0.20992149750158084, + "DEF-skirt.L.002.seg1": 0.17224243855583787, + "DEF-skirt.002.seg2": 0.06224908278966823, + "DEF-skirt.L.001.seg2": 1.7453185674821454, + "DEF-skirt.L.002": 1.2521681773165678, + "DEF-skirt.L.005": 0.3245264364462497, + "DEF-skirt.L.001": 1.6723670399728074, + "DEF-skirt.011.seg2": 0.76678062536886, + "DEF-skirt.L.004": 0.8698332068849335, + "DEF-skirt.L.seg3.seg3": 0.16373520808554431, + "DEF-skirt.011.seg3.seg3": 0.2004773422902515, + "DEF-skirt.011.seg1": 0.04079192576080093 + }, + { + "DEF-skirt.L.001.seg3": 0.4770589501679676, + "DEF-skirt.L.005.seg3": 0.06730310147578078, + "DEF-skirt.L.002.seg2": 0.2637003859272131, + "DEF-skirt.L.002.seg3": 0.07451138760554099, + "DEF-skirt.L.001.seg1": 1.7453185674821454, + "DEF-skirt.L.002": 0.9059128727013415, + "DEF-skirt.L.002.seg1": 0.4638847820902827, + "DEF-skirt.L.005.seg2": 0.23469038268051, + "DEF-skirt.L.005": 0.39711292376471063, + "DEF-skirt.L.004": 0.6790552683210553, + "DEF-skirt.L.seg3.seg3": 0.03277273825234783, + "DEF-skirt.011.seg2": 0.02610022222928776, + "DEF-skirt.011.seg3.seg3": 0.022788314650780933 + }, + { + "DEF-skirt.L.001.seg2": 0.4770589501679676, + "DEF-skirt.L.005.seg3": 0.490271304327039, + "DEF-skirt.L.002.seg2": 0.403548752663742, + "DEF-skirt.L.002.seg3": 0.5443336104188372, + "DEF-skirt.L.005.seg2": 0.13555906348859614, + "DEF-skirt.L.002.seg1": 0.09464254593464361 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.L.005", + "DEF-skirt.L.005.seg1", + "DEF-skirt.L.005.seg2", + "DEF-skirt.L.005.seg3" + ], + "tips": [ + [ + 0.0, + 0.01609, + -0.0 + ], + [ + 0.0, + 0.01609, + 0.0 + ], + [ + 0.0, + 0.01609, + 0.0 + ], + [ + -0.00328, + 0.03949, + 0.01666 + ] + ], + "hulls": [ + [ + [ + 0.13566, + 0.00394, + 0.0337 + ], + [ + -0.00485, + -0.03063, + 0.00248 + ], + [ + 0.11133, + -0.0602, + 0.03829 + ], + [ + -0.00254, + 0.0033, + 0.01753 + ], + [ + 0.12245, + -0.03151, + 0.03567 + ], + [ + -0.00239, + 0.00046, + -0.0018 + ], + [ + 0.12944, + 0.00753, + 0.05185 + ], + [ + 0.12972, + 0.00715, + 0.05087 + ], + [ + 0.1221, + -0.03132, + 0.03669 + ], + [ + 0.13531, + 0.00413, + 0.03472 + ] + ], + [], + [ + [ + -0.00236, + 0.00948, + -0.012 + ], + [ + 0.00046, + 0.00643, + 0.00707 + ], + [ + 0.00031, + 0.00659, + 0.006 + ], + [ + -0.00219, + 0.00911, + -0.01099 + ] + ], + [ + [ + -0.00127, + 0.01968, + 0.01112 + ], + [ + -0.00243, + 0.02102, + 0.00345 + ], + [ + -0.00131, + 0.01905, + 0.01023 + ], + [ + -0.00225, + 0.02, + 0.00382 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.L.003.seg3.seg3": 1.0710938956248561, + "DEF-skirt.L.004.seg2": 0.5683273158772963, + "DEF-skirt.R.005.seg3.seg1": 0.4947092006738334, + "DEF-skirt.R.005.seg3.seg2": 0.4616601121976467, + "DEF-skirt.L.001.seg2": 0.39711292376471063, + "DEF-skirt.L.001.seg1": 0.3245264364462497, + "DEF-skirt.L.002": 0.13568216323168258, + "DEF-skirt.R.003.seg3.seg2": 0.09981203683251794, + "DEF-skirt.L.004.seg3": 0.13052172717024924, + "DEF-skirt.R.004": 0.037172193149486166, + "DEF-skirt.L.004.seg1": 1.1176660180768292, + "DEF-skirt.L.004": 0.1752801916924968, + "DEF-skirt.L.003.seg3.seg2": 0.06826316269005073, + "DEF-skirt.R.005": 0.00681448318365796, + "DEF-skirt.R.003.seg3": 0.05610536797568777 + }, + { + "DEF-skirt.L.005.seg2": 0.33346096644056367, + "DEF-skirt.L.004.seg3": 0.3626712751649195, + "DEF-skirt.R.005.seg3.seg1": 0.1731091652564991, + "DEF-skirt.R.005.seg3.seg2": 0.19300246021116618 + }, + { + "DEF-skirt.L.004.seg3": 0.2990422252541247, + "DEF-skirt.L.005.seg1": 0.33346096644056367, + "DEF-skirt.R.005.seg3.seg1": 0.14196014532202939, + "DEF-skirt.R.005.seg3.seg2": 0.15748844353115632, + "DEF-skirt.L.001.seg2": 0.23469038268051, + "DEF-skirt.L.001.seg3": 0.13555906348859614, + "DEF-skirt.L.002.seg1": 0.07711775446670888, + "DEF-skirt.L.002.seg2": 0.1092308976699008, + "DEF-skirt.L.005.seg3": 0.14366482829699256 + }, + { + "DEF-skirt.L.001.seg3": 0.490271304327039, + "DEF-skirt.L.001.seg2": 0.06730310147578078, + "DEF-skirt.L.002.seg2": 0.09306542628326586, + "DEF-skirt.L.002.seg3": 0.18788502624155878, + "DEF-skirt.L.005.seg2": 0.14366482829699256 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.L.004", + "DEF-skirt.L.004.seg1", + "DEF-skirt.L.004.seg2", + "DEF-skirt.L.004.seg3" + ], + "tips": [ + [ + -0.0, + 0.05681, + -0.0 + ], + [ + -0.0, + 0.05681, + 0.0 + ], + [ + 0.0, + 0.05681, + -0.0 + ], + [ + -0.03198, + 0.05267, + 0.09074 + ] + ], + "hulls": [ + [ + [ + -0.15102, + 0.0078, + 0.08207 + ], + [ + 0.00824, + 0.03576, + 0.00178 + ], + [ + -0.06712, + 0.04687, + 0.04402 + ], + [ + -0.05826, + 0.05446, + -0.01011 + ], + [ + -0.10647, + 0.04332, + 0.06105 + ], + [ + -0.12285, + -0.00656, + 0.08433 + ], + [ + -0.09679, + 0.03058, + 0.05911 + ], + [ + -0.12841, + -0.01172, + 0.07969 + ], + [ + -0.1452, + 0.0128, + 0.08653 + ], + [ + -0.05835, + 0.0553, + -0.00941 + ] + ], + [ + [ + 0.08615, + 0.03472, + -0.05979 + ], + [ + -0.05431, + 0.05202, + 0.02224 + ], + [ + 0.02518, + 0.0375, + 0.00055 + ], + [ + -0.02527, + 0.02548, + -0.02003 + ], + [ + 0.07674, + 0.02108, + 0.00322 + ], + [ + 0.04988, + 0.00338, + -0.02549 + ], + [ + 0.05354, + 0.035, + -0.04159 + ], + [ + 0.06927, + 0.00371, + -0.05393 + ], + [ + -0.0043, + 0.04893, + 0.0021 + ], + [ + 0.05709, + 0.03934, + -0.00321 + ] + ], + [ + [ + -0.0979, + 0.0264, + 0.02789 + ], + [ + 0.06661, + 0.00056, + -0.00534 + ], + [ + -0.01302, + 0.03632, + 0.01466 + ], + [ + -0.03914, + 0.01853, + 0.04737 + ], + [ + -0.00376, + 0.00758, + -0.01638 + ], + [ + -0.02557, + 0.00374, + 0.01734 + ], + [ + -0.06965, + 0.03502, + 0.00938 + ], + [ + 0.03762, + 0.0123, + -0.00983 + ], + [ + -0.03363, + 0.04644, + 0.02948 + ], + [ + 0.00975, + 0.02426, + -0.00104 + ] + ], + [ + [ + -0.01532, + 0.02619, + 0.04811 + ], + [ + -0.01679, + 0.02767, + 0.04048 + ], + [ + -0.01702, + 0.02671, + 0.04095 + ], + [ + -0.01575, + 0.02562, + 0.04729 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.L.004.seg1": 3.414259861156586, + "DEF-skirt.L.001.seg1": 0.8698332068849335, + "DEF-skirt.L.001.seg2": 0.6790552683210553, + "DEF-skirt.L.seg3.seg3": 2.4919195177858766, + "DEF-skirt.L.seg2": 0.030678498443618918, + "DEF-skirt.L.003": 0.22795107245664514, + "DEF-skirt.L.seg3": 0.16873546962733288, + "DEF-skirt.L.seg3.seg1": 0.09720520424174028, + "DEF-skirt.L.003.seg3.seg1": 1.4763529281077483, + "DEF-skirt.L.003.seg3.seg3": 1.1637832999210915, + "DEF-skirt.L.003.seg3.seg2": 2.272135412934264, + "DEF-skirt.R.003.seg3": 0.4550022757961103, + "DEF-skirt.L.003.seg2": 0.7080545383425245, + "DEF-skirt.L.003.seg3": 0.12941547114153995, + "DEF-skirt.L.005": 0.1752801916924968, + "DEF-skirt.seg3.seg2": 0.04960925122223636, + "DEF-skirt.seg3.seg1": 0.48471971803661384, + "DEF-skirt": 0.19723534186546737, + "DEF-skirt.seg3": 0.25244402955330414, + "DEF-skirt.seg2": 0.10706301785350203, + "DEF-skirt.L.003.seg1": 0.31925299788853856, + "DEF-skirt.L.seg3.seg2": 0.7253384654734396, + "DEF-skirt.R.003.seg3.seg1": 0.24064906026527932, + "DEF-skirt.seg1": 0.2166604484120559, + "DEF-skirt.L.002": 0.8474234237006839 + }, + { + "DEF-skirt.L.004": 3.414259861156586, + "DEF-skirt.L.005": 1.1176660180768292, + "DEF-skirt.L.004.seg2": 4.398717155822705, + "DEF-skirt.R.004": 1.137863464467344, + "DEF-skirt.L.003.seg3.seg3": 7.3379864950231966, + "DEF-skirt.L.seg3.seg3": 0.8945923490834223, + "DEF-skirt.R.003.seg3.seg2": 1.045940511472522, + "DEF-skirt.L.003.seg3.seg2": 0.4210161758607365, + "DEF-skirt.R.003.seg3.seg1": 0.7411719285966271 + }, + { + "DEF-skirt.L.003.seg3.seg3": 6.411217302681206, + "DEF-skirt.L.005": 0.5683273158772963, + "DEF-skirt.R.005.seg3.seg1": 0.2787690934996667, + "DEF-skirt.R.005.seg3.seg2": 0.24706485523619137, + "DEF-skirt.L.004.seg3": 0.08785338409988164, + "DEF-skirt.R.004": 1.8915574818389527, + "DEF-skirt.L.004.seg1": 4.398717155822705, + "DEF-skirt.R.003.seg3.seg3": 2.9099697763658727, + "DEF-skirt.R.003.seg3.seg2": 2.6459612955234846, + "DEF-skirt.R.004.seg1": 1.580936268998669, + "DEF-skirt.R.005.seg1": 0.04847139189688332, + "DEF-skirt.R.003.seg3.seg1": 0.05539768250199251 + }, + { + "DEF-skirt.L.005.seg2": 0.2990422252541247, + "DEF-skirt.L.005.seg1": 0.3626712751649195, + "DEF-skirt.R.005.seg3.seg1": 0.38957429026409307, + "DEF-skirt.R.005.seg3.seg2": 0.41628226418510317, + "DEF-skirt.L.004.seg2": 0.08785338409988164, + "DEF-skirt.L.005": 0.13052172717024924, + "DEF-skirt.R.004.seg1": 0.22737010706344352, + "DEF-skirt.R.003.seg3.seg3": 0.6609395685885446, + "DEF-skirt.R.004.seg2": 0.2314181005880216 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.R.002" + ], + "tips": [ + [ + -0.00968, + 0.11246, + 0.00454 + ] + ], + "hulls": [ + [ + [ + -0.00719, + 0.06892, + 0.00329 + ], + [ + -0.0171, + 0.03783, + 0.00801 + ], + [ + 0.00058, + 0.03804, + -0.00026 + ], + [ + -2e-05, + 0.06885, + 1e-05 + ], + [ + -0.01612, + 0.03765, + 0.00756 + ], + [ + -0.00041, + 0.03801, + 0.00021 + ], + [ + -0.00073, + 0.06811, + 0.00037 + ], + [ + -0.00668, + 0.06797, + 0.00311 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.R.001.seg2": 1.425391395714045, + "DEF-skirt.R.001.seg3": 0.47549151784307764, + "DEF-skirt.R.005": 0.5540109966676522, + "DEF-skirt.010.seg3": 0.30870686047564977, + "DEF-skirt.R.001.seg1": 1.0538634266011213, + "DEF-skirt.009.seg3.seg3": 0.5217283105738937, + "DEF-skirt.010.seg2": 0.18461656159518913, + "DEF-skirt.010.seg1": 0.036999542338918445, + "DEF-skirt.R.001": 0.0942479173170957, + "DEF-skirt.R.003.seg3.seg1": 0.00932822018436949, + "DEF-skirt.010": 0.038553722683227876, + "DEF-skirt.009.seg3.seg2": 0.10385306321059562 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.R.001", + "DEF-skirt.R.001.seg1", + "DEF-skirt.R.001.seg2", + "DEF-skirt.R.001.seg3" + ], + "tips": [ + [ + -0.0, + 0.0643, + 0.0 + ], + [ + 0.0, + 0.0643, + 0.0 + ], + [ + -0.0, + 0.0643, + -0.0 + ], + [ + -0.00053, + 0.0016, + 0.01591 + ] + ], + "hulls": [ + [ + [ + 0.0053, + 0.01949, + 0.11977 + ], + [ + 0.03099, + 0.06327, + 0.02728 + ], + [ + 0.00431, + 0.01991, + 0.11959 + ], + [ + 0.02996, + 0.0634, + 0.02764 + ] + ], + [ + [ + 0.0247, + 0.06188, + 0.01354 + ], + [ + -0.04416, + 0.02727, + -0.03656 + ], + [ + -0.01633, + 0.00129, + -0.03211 + ], + [ + 0.02157, + 0.03459, + 0.03826 + ], + [ + 0.03255, + 0.03188, + 0.02232 + ], + [ + -0.02664, + 0.0297, + -0.04487 + ], + [ + -0.02762, + 0.02956, + -0.0444 + ], + [ + -0.0173, + 0.00116, + -0.03165 + ], + [ + 0.0253, + 0.06173, + 0.01264 + ], + [ + 0.03194, + 0.03204, + 0.02321 + ] + ], + [ + [ + 0.0003, + 0.06429, + 0.00748 + ], + [ + 0.01463, + 0.00023, + 0.02877 + ], + [ + 0.01676, + 0.03439, + 0.00096 + ], + [ + 0.0061, + 0.03696, + 0.01711 + ], + [ + 0.00403, + 0.06421, + 0.00139 + ], + [ + 0.00669, + 0.0368, + 0.01621 + ], + [ + 0.0039, + 0.06337, + 0.00209 + ], + [ + 0.01523, + 7e-05, + 0.02787 + ], + [ + 0.01621, + 0.03434, + 0.0019 + ] + ], + [] + ], + "neighbours": [ + { + "DEF-skirt.R.001.seg1": 1.0626257464222642, + "DEF-skirt.R.002": 0.0942479173170957, + "DEF-skirt.010": 0.41345706217831535, + "DEF-skirt.010.seg1": 0.02102587878840989, + "DEF-skirt.009.seg3.seg2": 0.291723028754733, + "DEF-skirt.009.seg3.seg3": 0.042075054966386194, + "DEF-skirt.009.seg2": 0.4475886737702466, + "DEF-skirt.009.seg1": 0.07276506315391273, + "DEF-skirt.007.seg3.seg3": 0.1038098999130459, + "DEF-skirt.R.seg2": 1.5069530565044718, + "DEF-skirt.007.seg3.seg1": 0.09218237179130262, + "DEF-skirt.R.003.seg2": 0.14694119993326393, + "DEF-skirt.R.003.seg3": 0.0656762465849694, + "DEF-skirt.R.seg3": 0.37144023323246467, + "DEF-skirt.R.seg3.seg1": 0.2535603123931991, + "DEF-skirt.R.seg1": 0.05772151199563896 + }, + { + "DEF-skirt.010.seg3": 0.26437839910012917, + "DEF-skirt.R.002": 1.0538634266011213, + "DEF-skirt.009.seg3.seg3": 0.3565165426494057, + "DEF-skirt.R.001.seg2": 0.5129446025014981, + "DEF-skirt.R.005": 0.5474316828935764, + "DEF-skirt.R.001": 1.0626257464222642, + "DEF-skirt.R.003.seg3.seg1": 0.0821030406636305, + "DEF-skirt.010.seg2": 0.05987322137685602, + "DEF-skirt.R.004.seg1": 0.12464125697105377, + "DEF-skirt.R.seg3.seg1": 0.17581185285518597 + }, + { + "DEF-skirt.R.001.seg3": 0.4899838741900986, + "DEF-skirt.R.002": 1.425391395714045, + "DEF-skirt.R.005": 0.8284251931385853, + "DEF-skirt.R.001.seg1": 0.5129446025014981 + }, + { + "DEF-skirt.R.001.seg2": 0.4899838741900986, + "DEF-skirt.R.002": 0.47549151784307764, + "DEF-skirt.R.005": 0.26127859408935183 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.R.005", + "DEF-skirt.R.005.seg1", + "DEF-skirt.R.005.seg2", + "DEF-skirt.R.005.seg3", + "DEF-skirt.R.005.seg3.seg1", + "DEF-skirt.R.005.seg3.seg2", + "DEF-skirt.R.005.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.01912, + -0.0 + ], + [ + 0.0, + 0.01912, + 0.0 + ], + [ + -0.0, + 0.01912, + 0.0 + ], + [ + -0.0, + 0.04419, + -0.0 + ], + [ + 0.0, + 0.04419, + 0.0 + ], + [ + 0.0, + 0.04419, + 0.0 + ], + [ + 0.0, + 0.04419, + 0.0 + ] + ], + "hulls": [ + [ + [ + 0.0341, + 0.01431, + 0.01315 + ], + [ + -0.02492, + -0.01564, + -0.00996 + ], + [ + -0.0076, + 0.00361, + 0.01594 + ], + [ + -0.02029, + 0.01073, + -0.0016 + ], + [ + 0.00187, + -0.00238, + -7e-05 + ], + [ + -0.02088, + 0.01106, + -0.00074 + ], + [ + -0.02438, + -0.01598, + -0.01085 + ], + [ + 0.00134, + -0.00205, + 0.00082 + ], + [ + 0.03357, + 0.01464, + 0.01405 + ], + [ + -0.00707, + 0.00327, + 0.01504 + ] + ], + [ + [ + -0.05443, + 0.00812, + -0.03221 + ], + [ + -0.02008, + 0.00129, + -0.03264 + ], + [ + -0.04983, + 0.01063, + -0.03808 + ], + [ + -0.02056, + 0.00086, + -0.03176 + ], + [ + -0.05337, + 0.008, + -0.03245 + ], + [ + -0.04941, + 0.01002, + -0.03727 + ] + ], + [], + [], + [], + [], + [] + ], + "neighbours": [ + { + "DEF-skirt.R.005.seg1": 1.163783608621209, + "DEF-skirt.R.001.seg2": 0.8284251931385853, + "DEF-skirt.R.001.seg3": 0.26127859408935183, + "DEF-skirt.R.002": 0.5540109966676522, + "DEF-skirt.R.001.seg1": 0.5474316828935764, + "DEF-skirt.R.003.seg3.seg2": 0.8597751017059654, + "DEF-skirt.R.003.seg3.seg1": 0.016410842906184386, + "DEF-skirt.R.004.seg1": 6.50751062766, + "DEF-skirt.R.seg3.seg1": 0.022536525217506043, + "DEF-skirt.R.004.seg2": 9.738440866398614, + "DEF-skirt.R.003.seg3.seg3": 0.12668430650905993, + "DEF-skirt.R.004.seg3": 0.2621371357395973, + "DEF-skirt.L.003.seg3.seg2": 0.04556505075408657, + "DEF-skirt.L.005": 0.00681448318365796, + "DEF-skirt.R.003.seg3": 0.04879787543721603 + }, + { + "DEF-skirt.R.005": 1.163783608621209, + "DEF-skirt.R.004.seg1": 0.0803170826628623, + "DEF-skirt.L.004.seg2": 0.04847139189688332, + "DEF-skirt.R.003.seg3.seg2": 0.11678245339473747, + "DEF-skirt.R.005.seg2": 0.05278311122623336 + }, + { + "DEF-skirt.R.005.seg1": 0.05278311122623336 + }, + {}, + { + "DEF-skirt.L.005.seg2": 0.14196014532202939, + "DEF-skirt.L.004.seg3": 0.38957429026409307, + "DEF-skirt.L.005.seg1": 0.1731091652564991, + "DEF-skirt.R.005.seg3.seg2": 0.4125220055789728, + "DEF-skirt.L.003.seg3.seg3": 0.1732640283463538, + "DEF-skirt.L.005": 0.4947092006738334, + "DEF-skirt.L.004.seg2": 0.2787690934996667, + "DEF-skirt.R.003.seg3.seg2": 0.05431410032137607, + "DEF-skirt.R.004": 0.02162439256300741 + }, + { + "DEF-skirt.L.005.seg2": 0.15748844353115632, + "DEF-skirt.L.004.seg3": 0.41628226418510317, + "DEF-skirt.L.005.seg1": 0.19300246021116618, + "DEF-skirt.R.005.seg3.seg1": 0.4125220055789728, + "DEF-skirt.L.003.seg3.seg3": 0.1646855340041311, + "DEF-skirt.L.005": 0.4616601121976467, + "DEF-skirt.L.004.seg2": 0.24706485523619137, + "DEF-skirt.R.003.seg3.seg2": 0.047532298070888546, + "DEF-skirt.R.004": 0.017154372789296213 + }, + {} + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.R.004", + "DEF-skirt.R.004.seg1", + "DEF-skirt.R.004.seg2", + "DEF-skirt.R.004.seg3" + ], + "tips": [ + [ + 0.0, + 0.0491, + -0.0 + ], + [ + 0.0, + 0.0491, + -0.0 + ], + [ + -0.0, + 0.0491, + -0.0 + ], + [ + -0.0, + 0.0491, + -0.0 + ] + ], + "hulls": [ + [ + [ + -0.1167, + 0.01819, + 0.04147 + ], + [ + -0.02518, + 0.01477, + 0.0226 + ], + [ + -0.11245, + 0.01782, + 0.04839 + ], + [ + -0.11668, + 0.01753, + 0.04061 + ], + [ + -0.02577, + 0.01467, + 0.02169 + ], + [ + -0.11302, + 0.01872, + 0.0486 + ] + ], + [ + [ + 0.08322, + 0.02268, + -0.04538 + ], + [ + -0.03404, + 0.04837, + 0.02754 + ], + [ + 0.03012, + 0.01732, + 0.01002 + ], + [ + -0.00958, + 0.03139, + -0.00531 + ], + [ + 0.05572, + 0.02675, + -0.02574 + ], + [ + 0.01318, + 0.02813, + -0.01841 + ], + [ + 0.01459, + 0.03034, + 0.00518 + ], + [ + -0.00761, + 0.03188, + 0.01466 + ], + [ + 0.02743, + 0.01628, + -0.00983 + ], + [ + -0.00983, + 0.03887, + 0.00271 + ] + ], + [ + [ + 0.04627, + 0.02113, + -0.03047 + ], + [ + -0.04971, + 0.00448, + 0.02323 + ], + [ + 0.00662, + 0.02205, + 0.01108 + ], + [ + -0.03678, + 0.02978, + -0.00441 + ], + [ + 0.04436, + 0.00749, + 0.00644 + ], + [ + -0.02466, + 0.02701, + 0.02605 + ], + [ + 0.01614, + 0.00861, + -0.02715 + ], + [ + -0.0462, + 0.00208, + -0.00291 + ], + [ + 0.02258, + 0.00845, + -0.00476 + ], + [ + 0.04055, + 0.00468, + -0.01685 + ] + ], + [] + ], + "neighbours": [ + { + "DEF-skirt.L.004.seg2": 1.8915574818389527, + "DEF-skirt.L.005": 0.037172193149486166, + "DEF-skirt.R.005.seg3.seg1": 0.02162439256300741, + "DEF-skirt.R.005.seg3.seg2": 0.017154372789296213, + "DEF-skirt.L.004.seg1": 1.137863464467344, + "DEF-skirt.L.003.seg3.seg3": 1.4751940750756236, + "DEF-skirt.R.003.seg3.seg2": 0.5334465694740952, + "DEF-skirt.R.003.seg3.seg3": 0.12212243476651818, + "DEF-skirt.R.003.seg3": 1.5048650248963797, + "DEF-skirt.R.seg2": 0.16451946219773894, + "DEF-skirt.L.003.seg3.seg2": 0.06144282445376237, + "DEF-skirt.R.003.seg3.seg1": 1.9555913441755948, + "DEF-skirt.R.003.seg2": 0.01615965716406309, + "DEF-skirt.R.seg3": 0.216802611048998, + "DEF-skirt.R.004.seg1": 0.9317594077661442 + }, + { + "DEF-skirt.R.004.seg2": 10.44418664394788, + "DEF-skirt.R.001.seg1": 0.12464125697105377, + "DEF-skirt.R.005": 6.50751062766, + "DEF-skirt.R.seg3.seg1": 0.09888366732501774, + "DEF-skirt.L.004.seg3": 0.22737010706344352, + "DEF-skirt.R.003.seg3.seg3": 1.442350862938932, + "DEF-skirt.R.003.seg3.seg2": 1.99888569391325, + "DEF-skirt.L.004.seg2": 1.580936268998669, + "DEF-skirt.R.seg3": 0.10570511559391954, + "DEF-skirt.R.003.seg3.seg1": 0.12293567218380608, + "DEF-skirt.L.003.seg3.seg3": 0.15432767264401315, + "DEF-skirt.R.005.seg1": 0.0803170826628623, + "DEF-skirt.R.004": 0.9317594077661442, + "DEF-skirt.R.003.seg3": 0.017631318202639384 + }, + { + "DEF-skirt.R.004.seg1": 10.44418664394788, + "DEF-skirt.R.003.seg3.seg3": 2.3902905726493904, + "DEF-skirt.R.003.seg3.seg2": 0.7231921967327264, + "DEF-skirt.R.005": 9.738440866398614, + "DEF-skirt.R.seg3.seg1": 0.018094137279125344, + "DEF-skirt.R.seg3": 0.008828194437185716, + "DEF-skirt.R.003.seg3.seg1": 0.01009878183119231, + "DEF-skirt.L.004.seg3": 0.2314181005880216, + "DEF-skirt.R.004.seg3": 0.5314994966933353 + }, + { + "DEF-skirt.R.004.seg2": 0.5314994966933353, + "DEF-skirt.R.005": 0.2621371357395973 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.010", + "DEF-skirt.010.seg1", + "DEF-skirt.010.seg2", + "DEF-skirt.010.seg3" + ], + "tips": [ + [ + -0.0, + 0.02681, + -0.0 + ], + [ + 0.0, + 0.02681, + -0.0 + ], + [ + -0.0, + 0.02681, + 0.0 + ], + [ + -0.04852, + 0.01964, + 0.12108 + ] + ], + "hulls": [ + [ + [ + 0.08387, + 0.01976, + -0.01391 + ], + [ + -0.06183, + 0.02188, + -0.00739 + ], + [ + 0.02908, + 0.01198, + -0.02442 + ], + [ + -0.03781, + -0.01789, + 0.00557 + ], + [ + 0.05871, + 0.0103, + -0.02098 + ], + [ + -0.05956, + 0.01908, + 0.01173 + ], + [ + 0.08342, + 0.01978, + -0.01491 + ], + [ + 0.02931, + 0.01215, + -0.02548 + ], + [ + -0.06175, + 0.02156, + -0.00635 + ], + [ + 0.05915, + 0.0103, + -0.01999 + ] + ], + [ + [ + 0.02077, + 0.02144, + -0.07553 + ], + [ + 0.02768, + 0.02288, + -0.07198 + ], + [ + 0.0271, + 0.02196, + -0.07183 + ], + [ + 0.02138, + 0.02087, + -0.07483 + ] + ], + [], + [ + [ + -0.02616, + 0.01002, + 0.06035 + ], + [ + -0.01829, + 0.01042, + 0.06088 + ], + [ + -0.02535, + 0.00932, + 0.06056 + ] + ] + ], + "neighbours": [ + { + "DEF-skirt.010.seg1": 0.5450657534185466, + "DEF-skirt.009.seg2": 2.31631370282569, + "DEF-skirt.009.seg3": 0.5794516644227148, + "DEF-skirt.009.seg1": 1.0326567638668116, + "DEF-skirt.R.001": 0.41345706217831535, + "DEF-skirt.009.seg3.seg2": 0.4534153845361102, + "DEF-skirt.009.seg3.seg3": 0.11866978878245926, + "DEF-skirt.009": 0.20176207493055942, + "DEF-skirt.R.002": 0.038553722683227876, + "DEF-skirt.007.seg3.seg3": 0.09792058046352847, + "DEF-skirt.005.seg1": 0.09132764503221136, + "DEF-skirt.009.seg3.seg1": 0.2541971405848624, + "DEF-skirt.005.seg2": 0.023801487298321877 + }, + { + "DEF-skirt.010.seg2": 0.9090205213277258, + "DEF-skirt.009.seg3.seg1": 0.4539345266652154, + "DEF-skirt.009.seg3.seg2": 0.5193848176853515, + "DEF-skirt.010": 0.5450657534185466, + "DEF-skirt.009.seg2": 0.029688515931832526, + "DEF-skirt.009.seg3": 0.1588504654978703, + "DEF-skirt.R.002": 0.036999542338918445, + "DEF-skirt.009.seg3.seg3": 0.39906031362545213, + "DEF-skirt.R.001": 0.02102587878840989 + }, + { + "DEF-skirt.010.seg1": 0.9090205213277258, + "DEF-skirt.009.seg3.seg1": 0.16089618696169472, + "DEF-skirt.009.seg3.seg2": 0.10115360245832161, + "DEF-skirt.R.002": 0.18461656159518913, + "DEF-skirt.009.seg3.seg3": 0.4871734646557071, + "DEF-skirt.R.001.seg1": 0.05987322137685602 + }, + { + "DEF-skirt.R.001.seg1": 0.26437839910012917, + "DEF-skirt.R.002": 0.30870686047564977, + "DEF-skirt.009.seg3.seg3": 0.8574145820931012 + } + ] + }, + { + "class": "skirt", + "root_parent": "DEF-spine", + "bones": [ + "DEF-skirt.009", + "DEF-skirt.009.seg1", + "DEF-skirt.009.seg2", + "DEF-skirt.009.seg3", + "DEF-skirt.009.seg3.seg1", + "DEF-skirt.009.seg3.seg2", + "DEF-skirt.009.seg3.seg3" + ], + "tips": [ + [ + 0.0, + 0.02817, + 0.0 + ], + [ + 0.0, + 0.02818, + -0.0 + ], + [ + -0.0, + 0.02818, + -0.0 + ], + [ + -0.0, + 0.01365, + 0.0 + ], + [ + 0.0, + 0.01365, + -0.0 + ], + [ + -0.0, + 0.01365, + -0.0 + ], + [ + -0.00166, + 0.07753, + 0.03597 + ] + ], + "hulls": [ + [ + [ + -0.0934, + 0.01308, + -0.05741 + ], + [ + 0.09711, + 0.01762, + 0.00247 + ], + [ + -0.04856, + 0.02623, + 0.00682 + ], + [ + -0.07845, + 0.01877, + -0.02838 + ], + [ + -0.07587, + 0.01387, + -0.07617 + ], + [ + -0.06331, + 0.02572, + -0.00594 + ], + [ + 0.08192, + 0.01898, + -0.00309 + ], + [ + 0.08445, + 0.01441, + 0.00469 + ], + [ + 0.08905, + 0.01503, + -0.00772 + ], + [ + 0.09157, + 0.01046, + 6e-05 + ] + ], + [ + [ + 0.09362, + 0.00863, + -0.08179 + ], + [ + -0.05087, + 0.02478, + 0.04259 + ], + [ + 0.02526, + 0.01004, + -0.02974 + ], + [ + 0.02918, + 0.02075, + -0.07815 + ], + [ + 0.05947, + 0.003, + -0.06983 + ], + [ + 0.0185, + 0.0247, + -0.00059 + ], + [ + 0.06892, + 0.02449, + -0.06689 + ], + [ + 0.10145, + 0.01899, + -0.06785 + ], + [ + 0.02461, + 0.02325, + -0.04076 + ], + [ + 0.03153, + 0.00983, + -0.06629 + ] + ], + [ + [ + -0.07292, + 0.02213, + 0.09532 + ], + [ + 0.06001, + 0.00472, + -0.09372 + ], + [ + -0.01288, + 0.01989, + 0.00375 + ], + [ + 0.0358, + 0.01343, + -0.04136 + ], + [ + -0.04799, + 0.00526, + 0.04757 + ], + [ + 0.01666, + 0.01021, + -0.01058 + ], + [ + 0.02945, + 0.01043, + -0.0912 + ], + [ + -0.03094, + 0.01476, + 0.02644 + ], + [ + 0.02254, + 0.02536, + -0.06376 + ], + [ + 0.04226, + 0.00062, + -0.06501 + ] + ], + [ + [ + 0.01279, + -0.00059, + -0.03573 + ], + [ + 0.00605, + 0.00285, + -0.03705 + ], + [ + 0.00627, + 0.002, + -0.0364 + ], + [ + 0.01183, + -0.0009, + -0.0353 + ] + ], + [], + [ + [ + 0.03714, + 0.00755, + 0.0599 + ], + [ + -0.01547, + 0.00856, + 0.01849 + ], + [ + -0.00551, + 0.00678, + 0.04315 + ], + [ + -0.01449, + 0.00811, + 0.01831 + ], + [ + -0.00651, + 0.00713, + 0.0434 + ] + ], + [] + ], + "neighbours": [ + { + "DEF-skirt.009.seg1": 2.931302899954413, + "DEF-skirt.010": 0.20176207493055942, + "DEF-skirt.005.seg2": 0.1522403836076065, + "DEF-skirt.seg3.seg3": 0.5669906833388019, + "DEF-skirt.003.seg3.seg3": 0.9255952835673218, + "DEF-skirt.005.seg1": 1.0724076109246674, + "DEF-skirt.005": 0.5119326758415601, + "DEF-skirt.007.seg3.seg3": 5.738163745911422, + "DEF-skirt.003.seg3.seg2": 0.05579521783738445, + "DEF-skirt.seg2": 0.15624594621636767, + "DEF-skirt.seg1": 0.07365434578813679, + "DEF-skirt.007.seg3.seg2": 3.886224157806516, + "DEF-skirt.R.seg3": 0.061572100826446596, + "DEF-skirt.seg3.seg2": 0.07027978047871075, + "DEF-skirt.R.seg3.seg1": 0.025827454314786835 + }, + { + "DEF-skirt.010": 1.0326567638668116, + "DEF-skirt.009.seg2": 2.8177935958448073, + "DEF-skirt.009": 2.931302899954413, + "DEF-skirt.R.001": 0.07276506315391273, + "DEF-skirt.005.seg2": 1.1706993434027797, + "DEF-skirt.003.seg3.seg3": 0.803862237313018, + "DEF-skirt.005.seg1": 1.3243936900477413, + "DEF-skirt.seg3.seg3": 0.6259572671341773, + "DEF-skirt.007.seg3.seg3": 2.3938596563287686, + "DEF-skirt.005": 0.3651368285367087, + "DEF-skirt.R.seg3": 0.019597212899127414, + "DEF-skirt.seg2": 0.05715209920697695, + "DEF-skirt.seg3.seg2": 0.03079948105880803, + "DEF-skirt.007.seg3.seg2": 0.46220673472213736 + }, + { + "DEF-skirt.010": 2.31631370282569, + "DEF-skirt.010.seg1": 0.029688515931832526, + "DEF-skirt.009.seg3": 0.9418924865121523, + "DEF-skirt.009.seg1": 2.8177935958448073, + "DEF-skirt.R.001": 0.4475886737702466, + "DEF-skirt.007.seg3.seg3": 4.7681596275634295, + "DEF-skirt.005.seg2": 0.9486564797789065, + "DEF-skirt.003.seg3.seg3": 0.2763643729262093, + "DEF-skirt.005.seg1": 0.508727444611632, + "DEF-skirt.R.seg3.seg2": 0.4464423540921278, + "DEF-skirt.R.seg3.seg1": 0.23290165419269715, + "DEF-skirt.009.seg3.seg1": 0.18042145327096382, + "DEF-skirt.009.seg3.seg2": 0.6231933511586607, + "DEF-skirt.009.seg3.seg3": 0.017367935292414982, + "DEF-skirt.seg3.seg3": 0.04456463799778154 + }, + { + "DEF-skirt.010": 0.5794516644227148, + "DEF-skirt.010.seg1": 0.1588504654978703, + "DEF-skirt.009.seg2": 0.9418924865121523, + "DEF-skirt.007.seg3.seg3": 1.2296093987857097, + "DEF-skirt.R.seg3.seg2": 0.20040029934984271, + "DEF-skirt.R.seg3.seg1": 0.16415101097724621, + "DEF-skirt.009.seg3.seg1": 0.059563272428202474 + }, + { + "DEF-skirt.010.seg1": 0.4539345266652154, + "DEF-skirt.010.seg2": 0.16089618696169472, + "DEF-skirt.009.seg3.seg2": 0.3572268890396673, + "DEF-skirt.R.seg3.seg3": 0.07258592586402557, + "DEF-skirt.R.seg3.seg2": 0.5977067894809132, + "DEF-skirt.009.seg2": 0.18042145327096382, + "DEF-skirt.010": 0.2541971405848624, + "DEF-skirt.009.seg3": 0.059563272428202474, + "DEF-skirt.R.seg3.seg1": 0.03219642002010603 + }, + { + "DEF-skirt.010.seg1": 0.5193848176853515, + "DEF-skirt.010.seg2": 0.10115360245832161, + "DEF-skirt.009.seg3.seg1": 0.3572268890396673, + "DEF-skirt.010": 0.4534153845361102, + "DEF-skirt.R.001": 0.291723028754733, + "DEF-skirt.009.seg3.seg3": 0.10852419665975874, + "DEF-skirt.R.002": 0.10385306321059562, + "DEF-skirt.R.seg3.seg3": 0.17109059074600252, + "DEF-skirt.R.seg3.seg2": 0.6035550265175813, + "DEF-skirt.R.seg3.seg1": 0.05356704252333344, + "DEF-skirt.009.seg2": 0.6231933511586607, + "DEF-skirt.007.seg3.seg3": 0.2980328553075786 + }, + { + "DEF-skirt.010.seg3": 0.8574145820931012, + "DEF-skirt.R.001.seg1": 0.3565165426494057, + "DEF-skirt.R.002": 0.5217283105738937, + "DEF-skirt.010.seg2": 0.4871734646557071, + "DEF-skirt.010.seg1": 0.39906031362545213, + "DEF-skirt.010": 0.11866978878245926, + "DEF-skirt.R.001": 0.042075054966386194, + "DEF-skirt.009.seg3.seg2": 0.10852419665975874, + "DEF-skirt.R.seg3.seg3": 1.2648193653775424, + "DEF-skirt.R.seg3.seg2": 0.6722838322683956, + "DEF-skirt.R.seg3.seg1": 0.019978953841021385, + "DEF-skirt.009.seg2": 0.017367935292414982 + } + ] + } ], - "shoulder.R": "DEF-shoulder.R", - "shoulder.L": "DEF-shoulder.L", - "upper_arm.L": "DEF-upper_arm.L", - "upper_arm.R": "DEF-upper_arm.R", - "forearm.L": "DEF-forearm.L", - "forearm.R": "DEF-forearm.R", - "thigh.R": "DEF-thigh.R", - "thigh.L": "DEF-thigh.L", - "foot.R": "DEF-foot.R", - "foot.L": "DEF-foot.L", - "shin.R": "DEF-shin.R", - "shin.L": "DEF-shin.L", - "hand.R": "DEF-hand.R", - "hand.L": "DEF-hand.L", - "toe.L": "DEF-toe.L", - "toe.R": "DEF-toe.R" - }, - "fingers": { - "thumb.L": [ - "DEF-thumb.01.L", - "DEF-thumb.02.L", - "DEF-thumb.03.L" + "twist": [ + { + "bone": "DEF-upper_arm.L.001", + "parent": "DEF-upper_arm.L", + "child": "DEF-forearm.L" + }, + { + "bone": "DEF-forearm.L.001", + "parent": "DEF-forearm.L", + "child": "DEF-hand.L" + }, + { + "bone": "DEF-upper_arm.R.001", + "parent": "DEF-upper_arm.R", + "child": "DEF-forearm.R" + }, + { + "bone": "DEF-forearm.R.001", + "parent": "DEF-forearm.R", + "child": "DEF-hand.R" + }, + { + "bone": "DEF-thigh.L.001", + "parent": "DEF-thigh.L", + "child": "DEF-shin.L" + }, + { + "bone": "DEF-shin.L.001", + "parent": "DEF-shin.L", + "child": "DEF-foot.L" + }, + { + "bone": "DEF-thigh.R.001", + "parent": "DEF-thigh.R", + "child": "DEF-shin.R" + }, + { + "bone": "DEF-shin.R.001", + "parent": "DEF-shin.R", + "child": "DEF-foot.R" + } ], - "index.L": [ - "DEF-f_index.01.L", - "DEF-f_index.02.L", - "DEF-f_index.03.L" + "surfaces": [ + { + "mesh": "Object_10", + "surface": 0, + "material": "EyesInvL", + "class": "body", + "detail": "eyes_ink", + "why": "material names itself an eye card", + "verts": 140, + "textured": false, + "chain_share": {} + }, + { + "mesh": "Object_11", + "surface": 0, + "material": "EyesHL", + "class": "body", + "detail": "eyes_highlight", + "why": "material names itself an eye card", + "verts": 92, + "textured": false, + "chain_share": {} + }, + { + "mesh": "Object_12", + "surface": 0, + "material": "EyesFullBlack", + "class": "body", + "detail": "eyes_ink", + "why": "material names itself an eye card", + "verts": 269, + "textured": false, + "chain_share": {} + }, + { + "mesh": "Object_14", + "surface": 0, + "material": "ClothB", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 1658, + "textured": true, + "chain_share": { + "skirt": 0.688 + } + }, + { + "mesh": "Object_15", + "surface": 0, + "material": "FullBlack", + "class": "linework", + "detail": "outline_hull", + "why": "material is untextured ink or inside-out", + "verts": 2052, + "textured": false, + "chain_share": { + "skirt": 0.689 + } + }, + { + "mesh": "Object_17", + "surface": 0, + "material": "ClothA", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 706, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_18", + "surface": 0, + "material": "FullBlack", + "class": "linework", + "detail": "outline_hull", + "why": "material is untextured ink or inside-out", + "verts": 786, + "textured": false, + "chain_share": {} + }, + { + "mesh": "Object_20", + "surface": 0, + "material": "ClothA", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 124, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_22", + "surface": 0, + "material": "ClothA", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 124, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_24", + "surface": 0, + "material": "ClothCAndW", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 78, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_26", + "surface": 0, + "material": "ClothCAndW", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 312, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_27", + "surface": 0, + "material": "FullBlack", + "class": "linework", + "detail": "outline_hull", + "why": "material is untextured ink or inside-out", + "verts": 312, + "textured": false, + "chain_share": {} + }, + { + "mesh": "Object_29", + "surface": 0, + "material": "ClothCAndW", + "class": "cloth", + "detail": "garment", + "why": "material name says 'garment'", + "verts": 54, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_31", + "surface": 0, + "material": "Hair", + "class": "hair", + "detail": "hair", + "why": "material name says 'hair'", + "verts": 3479, + "textured": true, + "chain_share": { + "hair": 0.51, + "skirt": 0.002 + } + }, + { + "mesh": "Object_32", + "surface": 0, + "material": "material", + "class": "linework", + "detail": "outline_hull", + "why": "material is untextured ink or inside-out", + "verts": 3814, + "textured": false, + "chain_share": { + "hair": 0.474, + "skirt": 0.002 + } + }, + { + "mesh": "Object_7", + "surface": 0, + "material": "Face", + "class": "body", + "detail": "face", + "why": "material name says 'face'", + "verts": 1298, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_8", + "surface": 0, + "material": "Body", + "class": "body", + "detail": "skin", + "why": "material name says 'skin'", + "verts": 1980, + "textured": true, + "chain_share": {} + }, + { + "mesh": "Object_9", + "surface": 0, + "material": "Eyes", + "class": "body", + "detail": "eyes", + "why": "material name says 'eyes'", + "verts": 112, + "textured": true, + "chain_share": {} + } ], - "middle.L": [ - "DEF-f_middle.01.L", - "DEF-f_middle.02.L", - "DEF-f_middle.03.L" + "colliders": [ + { + "bone": "DEF-spine", + "child": "DEF-spine.001", + "from": 0.0, + "radius_head": 0.1859, + "radius_tail": 0.1859, + "radius": 0.1859, + "lid": true + }, + { + "bone": "DEF-thigh.L", + "child": "DEF-shin.L", + "from": 0.1, + "radius_head": 0.1121, + "radius_tail": 0.0619, + "radius": 0.0619 + }, + { + "bone": "DEF-thigh.R", + "child": "DEF-shin.R", + "from": 0.1, + "radius_head": 0.1107, + "radius_tail": 0.062, + "radius": 0.062 + }, + { + "bone": "DEF-shin.L", + "child": "DEF-foot.L", + "from": 0.1, + "radius_head": 0.0953, + "radius_tail": 0.082, + "radius": 0.082 + }, + { + "bone": "DEF-shin.R", + "child": "DEF-foot.R", + "from": 0.1, + "radius_head": 0.0955, + "radius_tail": 0.0815, + "radius": 0.0815 + } ], - "ring.L": [ - "DEF-f_ring.01.L", - "DEF-f_ring.02.L", - "DEF-f_ring.03.L" - ], - "pinky.L": [ - "DEF-f_pinky.01.L", - "DEF-f_pinky.02.L", - "DEF-f_pinky.03.L" - ], - "thumb.R": [ - "DEF-thumb.01.R", - "DEF-thumb.02.R", - "DEF-thumb.03.R" - ], - "index.R": [ - "DEF-f_index.01.R", - "DEF-f_index.02.R", - "DEF-f_index.03.R" - ], - "middle.R": [ - "DEF-f_middle.01.R", - "DEF-f_middle.02.R", - "DEF-f_middle.03.R" - ], - "ring.R": [ - "DEF-f_ring.01.R", - "DEF-f_ring.02.R", - "DEF-f_ring.03.R" - ], - "pinky.R": [ - "DEF-f_pinky.01.R", - "DEF-f_pinky.02.R", - "DEF-f_pinky.03.R" + "weights_authored": true, + "driven_bones": [ + "DEF-f_index.01.L", + "DEF-f_index.01.R", + "DEF-f_index.02.L", + "DEF-f_index.02.R", + "DEF-f_index.03.L", + "DEF-f_index.03.R", + "DEF-f_middle.01.L", + "DEF-f_middle.01.R", + "DEF-f_middle.02.L", + "DEF-f_middle.02.R", + "DEF-f_middle.03.L", + "DEF-f_middle.03.R", + "DEF-f_pinky.01.L", + "DEF-f_pinky.01.R", + "DEF-f_pinky.02.L", + "DEF-f_pinky.02.R", + "DEF-f_pinky.03.L", + "DEF-f_pinky.03.R", + "DEF-f_ring.01.L", + "DEF-f_ring.01.R", + "DEF-f_ring.02.L", + "DEF-f_ring.02.R", + "DEF-f_ring.03.L", + "DEF-f_ring.03.R", + "DEF-foot.L", + "DEF-foot.R", + "DEF-forearm.L", + "DEF-forearm.R", + "DEF-hand.L", + "DEF-hand.R", + "DEF-shin.L", + "DEF-shin.R", + "DEF-shoulder.L", + "DEF-shoulder.R", + "DEF-spine", + "DEF-spine.001", + "DEF-spine.002", + "DEF-spine.003", + "DEF-spine.004", + "DEF-spine.005", + "DEF-spine.006", + "DEF-thigh.L", + "DEF-thigh.R", + "DEF-thumb.01.L", + "DEF-thumb.01.R", + "DEF-thumb.02.L", + "DEF-thumb.02.R", + "DEF-thumb.03.L", + "DEF-thumb.03.R", + "DEF-toe.L", + "DEF-toe.R", + "DEF-upper_arm.L", + "DEF-upper_arm.R" ] - }, - "chains": [ - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair", - "DEF-hair.001", - "DEF-hair.002" - ], - "tips": [ - [ - -0.0, - 0.06838, - 0.0 - ], - [ - -0.0, - 0.04506, - 0.0 - ], - [ - -0.01032, - 0.00854, - 0.01182 - ] - ], - "hulls": [ - [ - [ - 0.01674, - 0.10291, - 0.02973 - ], - [ - 0.03451, - 0.03231, - 0.00016 - ], - [ - -0.01716, - 0.05337, - -0.00299 - ], - [ - 0.03264, - 0.08202, - 0.01591 - ], - [ - 0.00803, - 0.0577, - 0.0108 - ], - [ - 0.03992, - 0.05758, - 0.00882 - ], - [ - 0.02314, - 0.05921, - 0.0009 - ], - [ - -0.00304, - 0.05285, - 0.00139 - ], - [ - 0.01035, - 0.06042, - -0.00529 - ], - [ - 0.01928, - 0.07988, - 0.01229 - ] - ], - [ - [ - -0.00899, - 0.04442, - -0.00085 - ], - [ - -0.02404, - 0.01036, - 0.01139 - ], - [ - 0.01712, - 0.01993, - 0.005 - ], - [ - -0.00513, - 0.01451, - -0.00241 - ], - [ - 0.00984, - 0.03774, - -0.00153 - ], - [ - 0.00415, - 0.01792, - 0.0089 - ], - [ - -0.01645, - 0.0083, - -0.00014 - ], - [ - 0.00046, - 0.03505, - 0.00649 - ], - [ - -0.01208, - 0.00937, - 0.01203 - ], - [ - -0.00743, - 0.03312, - -0.00319 - ] - ], - [ - [ - -0.01394, - 0.02572, - 0.00011 - ], - [ - -0.0018, - -0.01608, - 0.01364 - ], - [ - 0.00391, - 0.0038, - -0.00163 - ], - [ - -0.01374, - 0.00288, - 0.0104 - ], - [ - 0.00701, - 0.02313, - 0.00607 - ], - [ - -0.02015, - -0.01109, - 0.01284 - ], - [ - -0.00695, - 0.01233, - 0.00213 - ], - [ - -0.00142, - -0.00478, - 0.00609 - ], - [ - -0.00975, - -0.01068, - 0.01879 - ], - [ - 5e-05, - 0.01019, - 0.00929 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.001": 6.444893450622218, - "DEF-hair.007": 2.1405753868633206, - "DEF-hair.004": 4.305379068442997, - "DEF-hair.003": 3.737794440612138, - "DEF-hair.002": 2.852879727367223 - }, - { - "DEF-hair.002": 10.932883673705392, - "DEF-hair": 6.444893450622218, - "DEF-hair.004": 0.5635176735066056, - "DEF-hair.003": 0.15824819220502323, - "DEF-hair.007": 0.3355432027270542 - }, - { - "DEF-hair.001": 10.932883673705392, - "DEF-hair": 2.852879727367223, - "DEF-hair.007": 0.03886162555068284 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.003", - "DEF-hair.004" - ], - "tips": [ - [ - 0.0, - 0.06123, - -0.0 - ], - [ - -0.00017, - 0.04439, - -0.00029 - ] - ], - "hulls": [ - [ - [ - 0.00113, - 0.06236, - -0.00227 - ], - [ - 0.00701, - 0.02584, - -0.00787 - ], - [ - -0.00614, - 0.04412, - -0.00738 - ], - [ - 0.00784, - 0.04471, - 0.00317 - ], - [ - 0.00988, - 0.02683, - 0.003 - ], - [ - 0.00477, - 0.04586, - -0.00738 - ], - [ - 0.00445, - 0.05938, - 0.00744 - ], - [ - -0.00921, - 0.05971, - -0.00256 - ], - [ - 0.00043, - 0.02596, - 0.0005 - ], - [ - -0.00134, - 0.04362, - 0.00076 - ] - ], - [ - [ - 0.00022, - 0.03937, - -0.00024 - ], - [ - 0.00511, - 0.01787, - 0.00141 - ], - [ - -0.00567, - 0.01832, - -0.00298 - ], - [ - -0.00236, - 0.01841, - 0.00283 - ], - [ - 0.00218, - 0.0177, - -0.00326 - ], - [ - -0.00314, - 0.01799, - -0.00474 - ], - [ - 0.00415, - 0.01817, - 0.00434 - ], - [ - -0.00465, - 0.01808, - -0.00268 - ], - [ - 0.00404, - 0.01779, - 0.0016 - ], - [ - -0.00301, - 0.0179, - -0.00366 - ] - ] - ], - "neighbours": [ - { - "DEF-hair": 3.737794440612138, - "DEF-hair.004": 0.48107030537866957, - "DEF-hair.001": 0.15824819220502323 - }, - { - "DEF-hair": 4.305379068442997, - "DEF-hair.001": 0.5635176735066056, - "DEF-hair.003": 0.48107030537866957 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.007", - "DEF-hair.005" - ], - "tips": [ - [ - 0.0, - 0.04186, - -0.0 - ], - [ - 0.00621, - 0.02144, - 0.00318 - ] - ], - "hulls": [ - [ - [ - -0.02112, - 0.04918, - 0.02422 - ], - [ - 0.00107, - -0.01033, - 0.00095 - ], - [ - -0.00347, - 0.03222, - -0.00397 - ], - [ - -0.00948, - 0.01364, - 0.01086 - ], - [ - 0.01133, - 0.01422, - -0.00041 - ], - [ - -0.00915, - 0.03367, - 0.01549 - ], - [ - -0.0059, - -0.00788, - 0.01565 - ], - [ - -0.00287, - 0.01257, - -0.00376 - ], - [ - 0.00363, - 0.03034, - 0.00838 - ], - [ - 0.00801, - -0.00529, - 0.01351 - ] - ], - [ - [ - -0.00018, - 0.03655, - -0.00043 - ], - [ - 0.01031, - -0.00915, - -4e-05 - ], - [ - -0.00729, - 0.01104, - 0.00395 - ], - [ - 0.0085, - 0.01128, - -0.00185 - ], - [ - 0.0031, - 0.01248, - 0.00607 - ], - [ - -0.00257, - 0.00994, - -0.00246 - ], - [ - 0.01434, - -0.00662, - 0.00254 - ], - [ - -0.00639, - 0.01211, - 0.00782 - ], - [ - 0.00527, - 0.01035, - -0.00419 - ], - [ - -0.00016, - 0.03547, - -0.00033 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.005": 3.2375640964822265, - "DEF-hair": 2.1405753868633206, - "DEF-hair.001": 0.3355432027270542, - "DEF-hair.002": 0.03886162555068284 - }, - { - "DEF-hair.007": 3.2375640964822265 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.008", - "DEF-hair.009", - "DEF-hair.010", - "DEF-hair.011" - ], - "tips": [ - [ - -0.0, - 0.08774, - -0.0 - ], - [ - 0.0, - 0.05443, - -0.0 - ], - [ - -0.0, - 0.03074, - 0.0 - ], - [ - -0.00258, - 0.02087, - 0.00039 - ] - ], - "hulls": [ - [ - [ - 0.02765, - 0.09489, - -0.00831 - ], - [ - 0.01218, - -0.00528, - -0.01352 - ], - [ - -0.00966, - 0.04947, - 0.0296 - ], - [ - 0.03232, - 0.03961, - -0.02464 - ], - [ - -0.00344, - 0.06915, - -0.00733 - ], - [ - 0.01392, - 0.01818, - 0.01576 - ], - [ - -0.01562, - 0.08459, - 0.02502 - ], - [ - 0.01659, - 0.07392, - 0.02003 - ], - [ - 0.02263, - 0.06867, - -0.02538 - ], - [ - -0.00898, - 0.01997, - -0.0017 - ] - ], - [ - [ - 0.03852, - 0.0474, - -0.02382 - ], - [ - -0.01561, - 0.01392, - 0.02178 - ], - [ - -0.01735, - 0.05484, - -0.01292 - ], - [ - 0.01606, - 0.01503, - -0.0077 - ], - [ - 0.00483, - 0.03686, - 0.01325 - ], - [ - 0.01213, - 0.05777, - -0.01159 - ], - [ - -0.00464, - 0.03242, - -0.01137 - ], - [ - -0.01895, - 0.0337, - 0.00761 - ], - [ - -0.01409, - 0.06012, - 0.00746 - ], - [ - 0.01101, - 0.01666, - 0.01597 - ] - ], - [ - [ - -0.01869, - 0.01611, - 0.00917 - ], - [ - -0.00763, - 0.01686, - -0.01105 - ], - [ - -0.0002, - 0.01687, - 0.01034 - ], - [ - -0.01846, - 0.0161, - 0.00208 - ], - [ - -0.00718, - 0.01676, - -0.01006 - ], - [ - -0.01785, - 0.01605, - 0.00846 - ], - [ - -0.01743, - 0.01605, - 0.00244 - ], - [ - -0.0005, - 0.0168, - 0.00929 - ] - ], - [ - [ - 0.00088, - 0.03163, - -0.00099 - ], - [ - -0.01318, - 0.00392, - 0.00628 - ], - [ - 0.01083, - 0.00678, - -0.00118 - ], - [ - -0.00504, - 0.00595, - -0.00867 - ], - [ - 0.00037, - 0.00517, - 0.00747 - ], - [ - 0.00756, - 0.0066, - -0.00563 - ], - [ - -0.01301, - 0.00416, - 0.00091 - ], - [ - -0.01235, - 0.00379, - 0.00558 - ], - [ - 0.00012, - 0.00504, - 0.00641 - ], - [ - 0.00708, - 0.00616, - -0.00476 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.009": 5.0612108830893465, - "DEF-hair.L.008": 0.10078104744782834, - "DEF-hair.R.012": 9.298894414270677 - }, - { - "DEF-hair.010": 2.072245581610017, - "DEF-hair.008": 5.0612108830893465 - }, - { - "DEF-hair.009": 2.072245581610017 - }, - {} - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.L.008", - "DEF-hair.L.009", - "DEF-hair.L.010", - "DEF-hair.L.011" - ], - "tips": [ - [ - -0.0, - 0.15518, - 0.0 - ], - [ - 0.0, - 0.1139, - 0.0 - ], - [ - -0.0, - 0.0683, - -0.0 - ], - [ - -0.00184, - 0.09275, - -0.00073 - ] - ], - "hulls": [ - [ - [ - -0.0201, - 0.12899, - -0.03061 - ], - [ - -0.02432, - 0.079, - 0.02303 - ], - [ - -0.02055, - 0.07496, - -0.03044 - ], - [ - -0.01427, - 0.12502, - 0.01645 - ], - [ - -0.00353, - 0.07731, - -0.0035 - ], - [ - -0.03306, - 0.07625, - -0.00346 - ], - [ - -0.03185, - 0.12798, - -0.00534 - ], - [ - -0.00435, - 0.12595, - -0.00541 - ], - [ - -0.01423, - 0.07892, - 0.01983 - ], - [ - -0.01092, - 0.07553, - -0.02619 - ] - ], - [ - [ - -0.01584, - 0.11898, - 0.02156 - ], - [ - -0.01937, - 0.01511, - -0.02701 - ], - [ - -0.01421, - 0.07061, - -0.01786 - ], - [ - -0.02173, - 0.01636, - 0.01885 - ], - [ - -0.01499, - 0.11514, - -0.01949 - ], - [ - -0.02382, - 0.06925, - 0.0219 - ], - [ - -0.00429, - 0.01801, - -0.00334 - ], - [ - -0.02958, - 0.01445, - -0.00452 - ], - [ - -0.03155, - 0.07078, - -0.00053 - ], - [ - -0.02325, - 0.1195, - 0.00065 - ] - ], - [ - [ - -0.00944, - 0.07223, - 0.01619 - ], - [ - -0.00096, - -0.00272, - 0.00095 - ], - [ - -0.02097, - 0.02867, - 0.02652 - ], - [ - -0.02263, - 0.03074, - -0.00958 - ], - [ - -0.0118, - 0.06831, - -0.00751 - ], - [ - -0.00862, - 0.02933, - 0.0079 - ], - [ - -0.029, - 0.02986, - 0.00881 - ], - [ - -0.01507, - 0.07231, - 0.00445 - ], - [ - -0.01532, - 0.03041, - -0.00731 - ], - [ - -0.01399, - 0.02864, - 0.02361 - ] - ], - [ - [ - -0.00073, - 0.05526, - -0.00107 - ], - [ - -0.00219, - -0.00378, - 0.00364 - ], - [ - -0.00111, - -0.00363, - 0.00361 - ], - [ - -0.00086, - 0.05418, - -0.00098 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.008": 0.10078104744782834, - "DEF-hair.L": 0.07930559596010497, - "DEF-hair.L.009": 1.529110640133334, - "DEF-hair.L.012": 3.1007271781263652, - "DEF-hair.L.013": 0.021517533900501484 - }, - { - "DEF-hair.L.010": 2.697011560446442, - "DEF-hair.L.008": 1.529110640133334 - }, - { - "DEF-hair.L.011": 3.3977685462242633, - "DEF-hair.L.009": 2.697011560446442 - }, - { - "DEF-hair.L.010": 3.3977685462242633 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.L.012", - "DEF-hair.L.013", - "DEF-hair.L.014" - ], - "tips": [ - [ - 0.0, - 0.105, - -0.0 - ], - [ - 0.0, - 0.05073, - 0.0 - ], - [ - -0.0067, - 0.04095, - 0.00485 - ] - ], - "hulls": [ - [ - [ - -0.02527, - 0.09, - 0.0122 - ], - [ - -0.01623, - 0.01641, - -0.012 - ], - [ - -0.00057, - 0.05122, - 0.0168 - ], - [ - -0.02063, - 0.05217, - -0.01658 - ], - [ - -0.01205, - 0.0868, - -0.0162 - ], - [ - -0.00573, - 0.08579, - 0.01506 - ], - [ - -0.00293, - 0.05454, - -0.00049 - ], - [ - -0.00287, - 0.0856, - -0.00186 - ], - [ - -0.01003, - 0.06304, - 0.01981 - ], - [ - -0.00815, - 0.01761, - -0.01073 - ] - ], - [ - [ - -0.01738, - 0.05399, - 0.00728 - ], - [ - -0.01851, - 0.00751, - -0.01353 - ], - [ - -0.01059, - 0.00565, - 0.02048 - ], - [ - -0.02089, - 0.03301, - -0.00948 - ], - [ - -0.01369, - 0.03096, - 0.02149 - ], - [ - -0.00248, - 0.00933, - 0.00082 - ], - [ - -0.00592, - 0.03143, - 0.00333 - ], - [ - -0.02149, - 0.00584, - 0.00598 - ], - [ - -0.02351, - 0.03225, - 0.00747 - ], - [ - -0.01105, - 0.04818, - -0.00687 - ] - ], - [ - [ - 0.00167, - 0.03852, - -0.0 - ], - [ - -0.00624, - 0.00804, - 0.01374 - ], - [ - -0.01075, - 0.00706, - -0.00464 - ], - [ - -0.00133, - 0.00698, - 0.00283 - ], - [ - -0.01224, - 0.00774, - 0.00546 - ], - [ - -0.00636, - 0.00686, - -0.00431 - ], - [ - -0.0025, - 0.0077, - 0.01144 - ], - [ - -0.00238, - 0.00686, - 0.00309 - ], - [ - -0.01002, - 0.00687, - -0.00385 - ], - [ - -0.00595, - 0.00775, - 0.01272 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.L.008": 3.1007271781263652, - "DEF-hair.L.013": 2.0607935679774254, - "DEF-hair.L.018": 5.976813796675678, - "DEF-hair.L.025": 0.040129953350502845 - }, - { - "DEF-hair.L.014": 3.4327523504035433, - "DEF-hair.L.012": 2.0607935679774254, - "DEF-hair.L.008": 0.021517533900501484 - }, - { - "DEF-hair.L.013": 3.4327523504035433 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.L.018", - "DEF-hair.L.019", - "DEF-hair.L.020", - "DEF-hair.L.021" - ], - "tips": [ - [ - 0.0, - 0.06312, - 0.0 - ], - [ - -0.0, - 0.05056, - 0.0 - ], - [ - 0.0, - 0.02034, - -0.0 - ], - [ - 0.0048, - 0.02461, - -0.00168 - ] - ], - "hulls": [ - [ - [ - -0.02612, - 0.14192, - -0.003 - ], - [ - -0.02929, - -0.03494, - 7e-05 - ], - [ - 0.00844, - 0.05412, - 0.0319 - ], - [ - -0.01794, - 0.08142, - -0.01905 - ], - [ - -0.01421, - 0.01826, - -0.00923 - ], - [ - -0.00185, - 0.10668, - 0.02716 - ], - [ - 0.01647, - 0.12274, - -0.02448 - ], - [ - -0.00038, - -0.01378, - 0.03151 - ], - [ - 0.01097, - 0.01946, - 0.02163 - ], - [ - 0.0037, - 0.04788, - -0.003 - ] - ], - [ - [ - -0.02288, - 0.05079, - -0.0256 - ], - [ - -0.00914, - -0.01232, - 0.01282 - ], - [ - 0.01774, - 0.04282, - 0.01727 - ], - [ - -0.00945, - 0.0198, - -0.00823 - ], - [ - 0.01435, - 0.00964, - 0.01206 - ], - [ - -0.00794, - 0.04511, - -0.00021 - ], - [ - -0.00348, - 0.01658, - 0.01378 - ], - [ - 0.00623, - 0.05728, - 0.00083 - ], - [ - -0.01301, - 0.04025, - -0.01727 - ], - [ - 0.01291, - 0.04211, - -0.00045 - ] - ], - [ - [ - 0.01476, - 0.03544, - 0.01055 - ], - [ - 0.00952, - -0.00352, - -0.00197 - ], - [ - 0.0258, - 0.01218, - 0.01849 - ], - [ - 0.00063, - 0.01187, - 0.00239 - ], - [ - 0.02179, - 0.00272, - 0.0051 - ], - [ - 0.0095, - 0.02439, - 0.00123 - ], - [ - 0.02216, - 0.02327, - 0.01003 - ], - [ - 0.02259, - -0.0023, - 0.01636 - ], - [ - 0.0127, - 0.01015, - 0.00256 - ], - [ - 0.01403, - 0.00342, - 0.01365 - ] - ], - [ - [ - -0.0004, - 0.02115, - -0.00031 - ], - [ - 0.00701, - 0.01093, - 0.00114 - ], - [ - -0.00091, - 0.00992, - -0.00415 - ], - [ - 0.00074, - 0.00975, - 0.00111 - ], - [ - 0.00552, - 0.01082, - -0.00311 - ], - [ - 0.00155, - 0.01032, - -0.00522 - ], - [ - 0.00567, - 0.01064, - 0.0034 - ], - [ - 0.00474, - 0.01042, - -0.00245 - ], - [ - 6e-05, - 0.00969, - -0.00372 - ], - [ - 0.00154, - 0.00977, - 0.00037 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.L.019": 30.077657354322675, - "DEF-hair.L.012": 5.976813796675678 - }, - { - "DEF-hair.L.018": 30.077657354322675, - "DEF-hair.L.020": 8.790750150303484 - }, - { - "DEF-hair.L.019": 8.790750150303484, - "DEF-hair.L.021": 1.3119401760714409 - }, - { - "DEF-hair.L.020": 1.3119401760714409 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.L.022", - "DEF-hair.L.023", - "DEF-hair.L.024", - "DEF-hair.L.025" - ], - "tips": [ - [ - 0.0, - 0.06757, - -0.0 - ], - [ - 0.0, - 0.05187, - 0.0 - ], - [ - 0.0, - 0.02554, - 0.0 - ], - [ - -0.00718, - 0.05819, - -0.00047 - ] - ], - "hulls": [ - [ - [ - -0.0171, - 0.05533, - -0.00825 - ], - [ - 0.00085, - 0.05042, - 0.01568 - ], - [ - -0.01647, - 0.05217, - 0.00964 - ], - [ - 0.00017, - 0.05294, - -0.00219 - ], - [ - 0.0041, - 0.05091, - 0.01062 - ], - [ - -0.01131, - 0.05508, - -0.00972 - ], - [ - 0.00302, - 0.05085, - 0.01045 - ], - [ - -0.00071, - 0.0528, - -0.00156 - ], - [ - -0.01627, - 0.05521, - -0.00756 - ], - [ - 0.00057, - 0.05054, - 0.01463 - ] - ], - [ - [ - -0.00961, - 0.04836, - -0.01404 - ], - [ - -0.01303, - 0.00389, - -0.01295 - ], - [ - -0.00493, - 0.01951, - 0.00843 - ], - [ - -0.00136, - 0.02665, - -0.01764 - ], - [ - -0.0161, - 0.03114, - -0.01071 - ], - [ - -0.01843, - 0.01304, - 0.00013 - ], - [ - 0.00438, - 0.04454, - -0.00581 - ], - [ - 0.00052, - 0.0112, - -0.00442 - ], - [ - -0.00752, - 0.03146, - 0.00147 - ], - [ - 0.00152, - 0.04372, - -0.02001 - ] - ], - [ - [ - -0.00382, - 0.02522, - -0.00718 - ], - [ - -0.00559, - 0.0012, - -0.00894 - ], - [ - 0.00224, - 0.00661, - -0.00227 - ], - [ - -0.00973, - 0.02269, - 0.00034 - ], - [ - 0.00214, - 0.00447, - -0.01281 - ], - [ - -0.00643, - 0.00293, - 3e-05 - ], - [ - -0.00344, - 0.0247, - 3e-05 - ], - [ - -0.00842, - 0.02339, - -0.00509 - ], - [ - 0.00399, - 0.00592, - -0.00951 - ], - [ - -0.00327, - 0.00453, - 0.00141 - ] - ], - [ - [ - -0.00355, - 0.02964, - -0.00022 - ], - [ - -0.00363, - 0.02855, - -0.00025 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.L.023": 1.3606538529242842 - }, - { - "DEF-hair.L.022": 1.3606538529242842, - "DEF-hair.L.024": 1.7455139142072973 - }, - { - "DEF-hair.L.025": 1.9640462828626353, - "DEF-hair.L.023": 1.7455139142072973 - }, - { - "DEF-hair.L.012": 0.040129953350502845, - "DEF-hair.L.024": 1.9640462828626353 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.R.008", - "DEF-hair.R.009", - "DEF-hair.R.010", - "DEF-hair.R.011" - ], - "tips": [ - [ - 0.0, - 0.15518, - 0.0 - ], - [ - -0.0, - 0.1139, - 0.0 - ], - [ - 0.0, - 0.0683, - 0.0 - ], - [ - 0.00252, - -0.00932, - 0.00663 - ] - ], - "hulls": [ - [ - [ - 0.02108, - 0.12855, - -0.03051 - ], - [ - 0.05777, - 0.0078, - -0.02551 - ], - [ - 0.0253, - 0.07855, - 0.02313 - ], - [ - 0.10638, - 0.04973, - -0.03343 - ], - [ - 0.02152, - 0.07451, - -0.03034 - ], - [ - 0.01525, - 0.12457, - 0.01655 - ], - [ - 0.07178, - 0.03724, - -0.00026 - ], - [ - 0.02707, - 0.01361, - -0.01575 - ], - [ - 0.00451, - 0.07687, - -0.00339 - ], - [ - 0.08192, - 0.03253, - -0.03457 - ] - ], - [ - [ - 0.01656, - 0.11824, - 0.02184 - ], - [ - 0.02009, - 0.01436, - -0.02673 - ], - [ - 0.01493, - 0.06986, - -0.01758 - ], - [ - 0.02245, - 0.01562, - 0.01914 - ], - [ - 0.01571, - 0.11439, - -0.01921 - ], - [ - 0.02454, - 0.0685, - 0.02218 - ], - [ - 0.00501, - 0.01726, - -0.00305 - ], - [ - 0.0303, - 0.01371, - -0.00423 - ], - [ - 0.03227, - 0.07003, - -0.00025 - ], - [ - 0.02397, - 0.11875, - 0.00093 - ] - ], - [ - [ - -0.02348, - 0.11573, - -0.01225 - ], - [ - 0.00105, - -0.00379, - 0.00086 - ], - [ - 0.00953, - 0.07116, - 0.01609 - ], - [ - 0.02106, - 0.0276, - 0.02643 - ], - [ - 0.02271, - 0.02967, - -0.00967 - ], - [ - 0.01189, - 0.06724, - -0.00761 - ], - [ - 0.00871, - 0.02826, - 0.0078 - ], - [ - 0.02909, - 0.02879, - 0.00872 - ], - [ - 0.01515, - 0.07124, - 0.00436 - ], - [ - 0.0154, - 0.02934, - -0.0074 - ] - ], - [] - ], - "neighbours": [ - { - "DEF-hair.R.005": 1.3949822409393602, - "DEF-hair.R": 2.6819264528755653, - "DEF-hair.R.006": 0.08180381100485945, - "DEF-hair.R.009": 5.783332277237857, - "DEF-hair.R.012": 9.98357400601948, - "DEF-hair.R.013": 0.03451450992651994 - }, - { - "DEF-hair.R.010": 2.9875086680951517, - "DEF-hair.R.008": 5.783332277237857 - }, - { - "DEF-hair.R.011": 3.9748911536524965, - "DEF-hair.R.009": 2.9875086680951517 - }, - { - "DEF-hair.R.010": 3.9748911536524965 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.006", - "bones": [ - "DEF-hair.R.012", - "DEF-hair.R.013", - "DEF-hair.R.014" - ], - "tips": [ - [ - 0.0, - 0.105, - 0.0 - ], - [ - -0.0, - 0.05073, - 0.0 - ], - [ - 0.0059, - 0.03897, - 0.00457 - ] - ], - "hulls": [ - [ - [ - 0.02629, - 0.08967, - 0.01231 - ], - [ - 0.02854, - 0.01432, - -0.02129 - ], - [ - 0.00159, - 0.05089, - 0.0169 - ], - [ - 0.02154, - 0.04587, - -0.04861 - ], - [ - 0.01367, - 0.05223, - -0.01523 - ], - [ - 0.01307, - 0.08647, - -0.0161 - ], - [ - 0.03538, - 0.029, - -0.00254 - ], - [ - 0.00917, - 0.01728, - -0.01063 - ], - [ - 0.03284, - 0.03419, - -0.02689 - ], - [ - 0.00675, - 0.08546, - 0.01516 - ] - ], - [ - [ - 0.0181, - 0.0532, - 0.00736 - ], - [ - 0.01924, - 0.00672, - -0.01345 - ], - [ - 0.01131, - 0.00486, - 0.02056 - ], - [ - 0.02162, - 0.03222, - -0.0094 - ], - [ - 0.01442, - 0.03017, - 0.02157 - ], - [ - 0.00321, - 0.00854, - 0.0009 - ], - [ - 0.00665, - 0.03064, - 0.00341 - ], - [ - 0.02221, - 0.00505, - 0.00606 - ], - [ - 0.02424, - 0.03146, - 0.00755 - ], - [ - 0.01178, - 0.04739, - -0.00679 - ] - ], - [ - [ - -0.00207, - 0.03753, - -0.00014 - ], - [ - 0.00584, - 0.00705, - 0.0136 - ], - [ - 0.01035, - 0.00607, - -0.00478 - ], - [ - 0.00093, - 0.00599, - 0.00268 - ], - [ - 0.01184, - 0.00675, - 0.00532 - ], - [ - 0.00596, - 0.00587, - -0.00445 - ], - [ - 0.0021, - 0.00671, - 0.0113 - ], - [ - 0.00198, - 0.00587, - 0.00294 - ], - [ - 0.00555, - 0.00676, - 0.01258 - ], - [ - 0.00962, - 0.00588, - -0.00399 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.R.013": 2.1961626040734146, - "DEF-hair.R.008": 9.98357400601948, - "DEF-hair.008": 9.298894414270677 - }, - { - "DEF-hair.R.014": 4.241960469519606, - "DEF-hair.R.012": 2.1961626040734146, - "DEF-hair.R.008": 0.03451450992651994 - }, - { - "DEF-hair.R.013": 4.241960469519606 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.004", - "bones": [ - "DEF-hair.L", - "DEF-hair.L.001", - "DEF-hair.L.002", - "DEF-hair.L.003" - ], - "tips": [ - [ - -0.0, - 0.11762, - 0.0 - ], - [ - 0.0, - 0.18049, - 0.0 - ], - [ - 0.0, - 0.10823, - -0.0 - ], - [ - -0.04057, - -0.18138, - -0.1173 - ] - ], - "hulls": [ - [ - [ - -0.15137, - 0.17534, - -0.07598 - ], - [ - -0.00863, - -0.01403, - -0.03231 - ], - [ - -0.21243, - 0.04236, - -0.06494 - ], - [ - -0.05177, - 0.08996, - 0.01251 - ], - [ - -0.13274, - 0.06548, - -0.05554 - ], - [ - -0.00623, - 0.09071, - -0.05371 - ], - [ - -0.07673, - -0.03425, - -0.01586 - ], - [ - -0.04578, - 0.02054, - 0.01325 - ], - [ - -0.06815, - 0.05124, - -0.0536 - ], - [ - -0.11547, - 0.09188, - -0.01059 - ] - ], - [ - [ - -0.04022, - 0.19274, - 0.02268 - ], - [ - -0.18808, - -0.01116, - -0.04065 - ], - [ - -0.00948, - 0.03806, - -0.05252 - ], - [ - -0.00011, - 0.12049, - -0.03261 - ], - [ - -0.05524, - 0.03318, - 0.01582 - ], - [ - -0.0526, - 0.12081, - 0.02013 - ], - [ - -0.16581, - 0.05417, - -0.05721 - ], - [ - -0.00179, - 0.18739, - -0.03687 - ], - [ - -0.0086, - 0.0377, - -0.00229 - ], - [ - -0.04574, - 0.03444, - -0.02739 - ] - ], - [ - [ - -0.13171, - -0.13625, - -0.07775 - ], - [ - -0.01593, - 0.10242, - 0.01499 - ], - [ - -0.03483, - 0.00179, - -0.01444 - ], - [ - -0.12119, - -0.04466, - -0.10461 - ], - [ - -0.03919, - 0.04719, - 0.02226 - ], - [ - -0.00011, - 0.04727, - -0.01891 - ], - [ - -0.14944, - -0.07688, - -0.1013 - ], - [ - 0.00414, - 0.00687, - -0.02526 - ], - [ - -0.11494, - -0.1096, - -0.09973 - ], - [ - -0.00516, - 0.09746, - -0.01752 - ] - ], - [ - [ - -0.00177, - -0.27194, - -0.12983 - ], - [ - 0.00148, - 0.04408, - -0.00096 - ], - [ - -0.09496, - -0.20564, - -0.13214 - ], - [ - -0.04621, - -0.22711, - -0.13847 - ], - [ - -0.01361, - 0.01332, - 0.0075 - ], - [ - -0.03276, - -0.24628, - -0.12281 - ], - [ - -0.00278, - 0.01174, - -0.01145 - ], - [ - -0.07862, - -0.20394, - -0.14078 - ], - [ - -0.00561, - -0.27193, - -0.11553 - ], - [ - -0.02874, - -0.24582, - -0.13598 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.L.005": 11.524415420547314, - "DEF-hair.L.006": 0.95175079508802, - "DEF-hair.L.007": 0.02390360394735, - "DEF-hair.L.008": 0.07930559596010497, - "DEF-hair.L.004": 3.6971060377822207, - "DEF-hair.L.001": 1.7201846105818497 - }, - { - "DEF-hair.L.002": 4.3335288549483275, - "DEF-hair.L": 1.7201846105818497, - "DEF-hair.L.005": 1.389903376048438, - "DEF-hair.L.004": 0.12349060048937432, - "DEF-hair.L.003": 2.1324736022343336 - }, - { - "DEF-hair.L.003": 9.736097935389328, - "DEF-hair.L.001": 4.3335288549483275, - "DEF-hair.L.005": 3.6150717720990997 - }, - { - "DEF-hair.L.002": 9.736097935389328, - "DEF-hair.L.005": 4.400683455708296, - "DEF-hair.L.001": 2.1324736022343336 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.004", - "bones": [ - "DEF-hair.R", - "DEF-hair.R.001", - "DEF-hair.R.002", - "DEF-hair.R.003" - ], - "tips": [ - [ - 0.0, - 0.11762, - -0.0 - ], - [ - 0.0, - 0.18049, - 0.0 - ], - [ - 0.0, - 0.10823, - -0.0 - ], - [ - 0.00673, - 0.04894, - -0.00199 - ] - ], - "hulls": [ - [ - [ - 0.10392, - 0.04499, - -0.03143 - ], - [ - -0.00179, - 0.08913, - -0.04032 - ], - [ - 0.01042, - -0.01469, - -0.03154 - ], - [ - 0.05269, - 0.08941, - 0.01263 - ], - [ - 0.0467, - 0.01999, - 0.01337 - ], - [ - 0.00289, - 0.0244, - -0.00341 - ], - [ - 0.03259, - 0.06068, - -0.03311 - ], - [ - 0.00187, - 0.02225, - -0.05257 - ], - [ - 0.06928, - 0.05051, - -0.05454 - ], - [ - 0.03548, - 0.01714, - -0.02935 - ] - ], - [ - [ - 0.04087, - 0.19192, - 0.02292 - ], - [ - 0.01014, - 0.03724, - -0.05229 - ], - [ - 0.00077, - 0.11967, - -0.03237 - ], - [ - 0.05589, - 0.03236, - 0.01606 - ], - [ - 0.05326, - 0.11999, - 0.02036 - ], - [ - 0.00244, - 0.18657, - -0.03663 - ], - [ - 0.00925, - 0.03688, - -0.00205 - ], - [ - 0.0464, - 0.03362, - -0.02716 - ], - [ - 0.04423, - 0.12228, - -0.02088 - ], - [ - 0.033, - 0.19244, - -0.01446 - ] - ], - [ - [ - 0.02337, - 0.10374, - 0.0137 - ], - [ - 0.02848, - -0.00246, - 0.02582 - ], - [ - 0.0076, - 0.04735, - -0.02831 - ], - [ - 0.03954, - 0.04619, - 0.02246 - ], - [ - 0.00572, - 0.04488, - 0.00827 - ], - [ - 0.00551, - 0.09646, - -0.01731 - ], - [ - 0.00242, - 0.00207, - 0.00675 - ], - [ - 0.0336, - 0.04774, - -0.00921 - ], - [ - 0.0059, - 0.07567, - 0.00688 - ], - [ - 0.0273, - 0.0798, - -0.0066 - ] - ], - [ - [ - -0.00144, - 0.04301, - -0.00088 - ], - [ - 0.01365, - 0.01225, - 0.00757 - ], - [ - 0.00282, - 0.01066, - -0.01138 - ], - [ - 0.00127, - 0.00974, - 0.00225 - ], - [ - 0.01206, - 0.0122, - -0.00415 - ], - [ - -0.0003, - 0.00984, - -0.00764 - ], - [ - 0.00895, - 0.0112, - 0.00848 - ], - [ - 0.00295, - 0.01034, - -0.01034 - ], - [ - 0.01117, - 0.01186, - -0.00363 - ], - [ - -0.00118, - 0.04195, - -0.00089 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.R.005": 8.895136039757556, - "DEF-hair.R.006": 0.5874295932403306, - "DEF-hair.R.008": 2.6819264528755653, - "DEF-hair.R.004": 3.0752010115277004, - "DEF-hair.R.001": 1.8037417183725024 - }, - { - "DEF-hair.R.002": 3.682230264940157, - "DEF-hair.R": 1.8037417183725024 - }, - { - "DEF-hair.R.003": 3.8194620543874045, - "DEF-hair.R.001": 3.682230264940157 - }, - { - "DEF-hair.R.002": 3.8194620543874045 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.003", - "bones": [ - "DEF-hair.L.004", - "DEF-hair.L.005", - "DEF-hair.L.006", - "DEF-hair.L.007" - ], - "tips": [ - [ - 0.0, - 0.13941, - -0.0 - ], - [ - -0.0, - 0.20008, - -0.0 - ], - [ - -0.0, - 0.10005, - -0.0 - ], - [ - -0.05539, - -0.26952, - -0.08576 - ] - ], - "hulls": [ - [ - [ - -0.12642, - 0.30292, - 0.00405 - ], - [ - 0.05763, - -0.01675, - -0.04559 - ], - [ - -0.06843, - 0.11737, - -0.0105 - ], - [ - -0.01425, - 0.23819, - 0.01163 - ], - [ - -0.04616, - -0.00216, - -0.04609 - ], - [ - 0.02452, - 0.11463, - -0.05849 - ], - [ - -0.08785, - 0.22711, - -0.05288 - ], - [ - -0.04701, - 0.30847, - -0.0095 - ], - [ - 0.06689, - 0.05756, - -0.04087 - ], - [ - 0.01631, - 0.00124, - -0.00275 - ] - ], - [ - [ - 0.22683, - 0.29103, - 0.02328 - ], - [ - -0.08923, - 0.10069, - -0.15483 - ], - [ - 0.10543, - -0.04097, - -0.04242 - ], - [ - -0.05913, - 0.1578, - 0.02553 - ], - [ - 0.13118, - 0.13005, - 0.05198 - ], - [ - 0.01882, - 0.07286, - -0.05548 - ], - [ - 0.12421, - 0.23997, - 0.00629 - ], - [ - 0.04625, - 0.10805, - -0.14725 - ], - [ - 0.10061, - 0.13139, - -0.04347 - ], - [ - 0.01364, - 0.17206, - -0.05526 - ] - ], - [ - [ - -0.03371, - 0.08882, - 0.00318 - ], - [ - 0.01108, - 0.04109, - -0.04371 - ], - [ - -0.02901, - 0.03766, - 0.01054 - ], - [ - 0.01259, - 0.08166, - -0.02576 - ], - [ - -0.02479, - 0.03974, - -0.03012 - ], - [ - 0.00154, - 0.03876, - -0.00124 - ], - [ - -0.02018, - 0.0886, - -0.02503 - ], - [ - 0.00084, - 0.08176, - -0.00177 - ], - [ - 0.01614, - 0.04051, - -0.03052 - ], - [ - -0.04167, - 0.03777, - 0.00429 - ] - ], - [ - [ - 0.09474, - -0.32132, - -0.06828 - ], - [ - -0.00094, - 0.06392, - 0.00035 - ], - [ - -0.10686, - -0.18513, - -0.02452 - ], - [ - 0.03872, - -0.24274, - -0.13557 - ], - [ - -0.11326, - -0.09615, - 0.0002 - ], - [ - -0.03432, - -0.169, - -0.05155 - ], - [ - 0.09388, - -0.25139, - -0.1342 - ], - [ - -0.02109, - 0.01802, - 0.00233 - ], - [ - -0.07019, - -0.21769, - -0.02149 - ], - [ - -0.09006, - -0.1274, - -0.02563 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.R.004": 4.686255793960349, - "DEF-hair.L.005": 40.2787691692223, - "DEF-hair.R.005": 0.15631888339514832, - "DEF-hair.L": 3.6971060377822207, - "DEF-hair.L.007": 2.0160448574603813, - "DEF-hair.L.006": 1.8851661673028555, - "DEF-hair.L.001": 0.12349060048937432 - }, - { - "DEF-hair.R.004": 0.45132666236203267, - "DEF-hair.L.004": 40.2787691692223, - "DEF-hair.L.006": 18.005348521988193, - "DEF-hair.L.007": 11.200201317515079, - "DEF-hair.L": 11.524415420547314, - "DEF-hair.L.001": 1.389903376048438, - "DEF-hair.L.002": 3.6150717720990997, - "DEF-hair.L.003": 4.400683455708296 - }, - { - "DEF-hair.L.005": 18.005348521988193, - "DEF-hair.L.007": 11.514555192106366, - "DEF-hair.L": 0.95175079508802, - "DEF-hair.L.004": 1.8851661673028555 - }, - { - "DEF-hair.L.005": 11.200201317515079, - "DEF-hair.L.006": 11.514555192106366, - "DEF-hair.L": 0.02390360394735, - "DEF-hair.L.004": 2.0160448574603813 - } - ] - }, - { - "class": "hair", - "root_parent": "DEF-spine.003", - "bones": [ - "DEF-hair.R.004", - "DEF-hair.R.005", - "DEF-hair.R.006", - "DEF-hair.R.007" - ], - "tips": [ - [ - -0.0, - 0.13941, - -0.0 - ], - [ - 0.0, - 0.20008, - -0.0 - ], - [ - 0.0, - 0.10005, - -0.0 - ], - [ - 0.108, - -0.20336, - -0.03794 - ] - ], - "hulls": [ - [ - [ - 0.12748, - 0.30272, - 0.00416 - ], - [ - -0.05658, - -0.01695, - -0.04548 - ], - [ - 0.10849, - 0.10936, - -0.0433 - ], - [ - 0.01637, - 0.2129, - -0.00652 - ], - [ - -0.02347, - 0.11443, - -0.05839 - ], - [ - 0.04721, - -0.00236, - -0.04598 - ], - [ - 0.12972, - 0.2226, - -0.03887 - ], - [ - 0.04806, - 0.30826, - -0.0094 - ], - [ - 0.03544, - 0.11472, - -0.00615 - ], - [ - -0.06583, - 0.05736, - -0.04077 - ] - ], - [ - [ - -0.22587, - 0.29058, - 0.02349 - ], - [ - 0.03823, - 0.00201, - -0.07772 - ], - [ - -0.0914, - 0.12009, - 0.04015 - ], - [ - 0.07244, - 0.14233, - -0.1518 - ], - [ - 0.06008, - 0.15735, - 0.02574 - ], - [ - -0.10791, - -0.04429, - -0.05776 - ], - [ - -0.02672, - 0.08048, - -0.13759 - ], - [ - -0.12325, - 0.23952, - 0.0065 - ], - [ - -0.01313, - 0.17179, - -0.05603 - ], - [ - -0.03432, - 0.04033, - 0.0005 - ] - ], - [ - [ - 0.03454, - 0.08821, - 0.00351 - ], - [ - -0.01025, - 0.04049, - -0.04339 - ], - [ - 0.02984, - 0.03706, - 0.01086 - ], - [ - -0.01176, - 0.08105, - -0.02544 - ], - [ - 0.02562, - 0.03913, - -0.0298 - ], - [ - -0.00071, - 0.03816, - -0.00091 - ], - [ - 0.02101, - 0.08799, - -0.02471 - ], - [ - -1e-05, - 0.08116, - -0.00145 - ], - [ - -0.01531, - 0.03991, - -0.03019 - ], - [ - 0.0425, - 0.03716, - 0.00461 - ] - ], - [ - [ - -0.09428, - -0.32225, - -0.06801 - ], - [ - 0.0014, - 0.06298, - 0.00062 - ], - [ - 0.10733, - -0.18606, - -0.02425 - ], - [ - 0.11373, - -0.09708, - 0.00047 - ], - [ - 0.03479, - -0.16994, - -0.05128 - ], - [ - 0.02156, - 0.01709, - 0.0026 - ], - [ - 0.07066, - -0.21863, - -0.02122 - ], - [ - 0.09053, - -0.12833, - -0.02536 - ], - [ - -0.11638, - -0.28965, - -0.06878 - ], - [ - 0.06006, - -0.19855, - -0.05526 - ] - ] - ], - "neighbours": [ - { - "DEF-hair.R.005": 39.8027606329575, - "DEF-hair.R": 3.0752010115277004, - "DEF-hair.R.007": 2.4225536161899783, - "DEF-hair.R.006": 1.7560717991488746, - "DEF-hair.L.004": 4.686255793960349, - "DEF-hair.L.005": 0.45132666236203267 - }, - { - "DEF-hair.R.006": 16.949170511515604, - "DEF-hair.R.007": 4.844983954094674, - "DEF-hair.R": 8.895136039757556, - "DEF-hair.R.008": 1.3949822409393602, - "DEF-hair.R.004": 39.8027606329575, - "DEF-hair.L.004": 0.15631888339514832, - "DEF-skirt.L.003.seg3": 2.6234847961983174 - }, - { - "DEF-hair.R.005": 16.949170511515604, - "DEF-hair.R.007": 7.382812583882695, - "DEF-hair.R": 0.5874295932403306, - "DEF-hair.R.008": 0.08180381100485945, - "DEF-hair.R.004": 1.7560717991488746 - }, - { - "DEF-hair.R.005": 4.844983954094674, - "DEF-hair.R.006": 7.382812583882695, - "DEF-hair.R.004": 2.4225536161899783 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine.001", - "bones": [ - "DEF-skirt", - "DEF-skirt.seg1", - "DEF-skirt.seg2", - "DEF-skirt.seg3" - ], - "tips": [ - [ - -0.0, - 0.04862, - 0.0 - ], - [ - 0.0, - 0.04862, - 0.0 - ], - [ - 0.0, - 0.04862, - 0.0 - ], - [ - -0.0511, - 0.12938, - -0.02471 - ] - ], - "hulls": [ - [ - [ - -0.01048, - 0.00471, - -0.09838 - ], - [ - 0.00854, - -0.00859, - 0.01809 - ], - [ - -0.0271, - 0.03158, - -0.01766 - ], - [ - -0.0049, - 0.0074, - -0.04266 - ], - [ - -0.00582, - -0.0011, - -0.0058 - ], - [ - -0.00356, - 0.02438, - -0.02161 - ], - [ - -0.02431, - 0.0063, - -0.04071 - ], - [ - 0.00058, - -0.00428, - 0.00657 - ], - [ - -0.01003, - 0.00121, - -0.01887 - ], - [ - -0.0145, - 0.00333, - -0.03183 - ] - ], - [ - [ - 0.03347, - 0.04167, - 0.04521 - ], - [ - 0.00529, - 0.02842, - -0.04709 - ], - [ - -0.00166, - 0.02549, - 0.00886 - ], - [ - 0.03107, - 0.00122, - 0.0163 - ], - [ - 0.02719, - 0.03735, - -0.013 - ], - [ - -0.01386, - 0.01375, - -0.02703 - ], - [ - 0.0171, - 0.01031, - -0.01507 - ], - [ - 0.04061, - 0.03722, - 0.02051 - ], - [ - 0.00867, - 0.01004, - 0.02387 - ], - [ - 0.02478, - 0.02369, - 0.00318 - ] - ], - [ - [ - -0.03953, - 0.03328, - -0.05187 - ], - [ - 0.04098, - 0.02712, - 0.02868 - ], - [ - -0.03136, - 0.01869, - 0.01049 - ], - [ - 0.00783, - 0.03339, - -0.01775 - ], - [ - 0.02203, - 0.01142, - 0.00456 - ], - [ - -0.04381, - 0.01432, - -0.02093 - ], - [ - -0.01168, - 0.02406, - 0.03633 - ], - [ - -0.0128, - 0.04482, - 0.00027 - ], - [ - 0.01566, - 0.0355, - 0.04064 - ], - [ - -0.01628, - 0.04258, - -0.02244 - ] - ], - [ - [ - -0.06106, - 0.11464, - -0.09045 - ], - [ - -0.01295, - 0.03749, - 0.09158 - ], - [ - -0.04234, - 0.03208, - -0.02001 - ], - [ - -0.02317, - 0.11069, - -0.0164 - ], - [ - 0.01719, - 0.00293, - 0.03068 - ], - [ - -0.07535, - 0.03421, - -0.07642 - ], - [ - -0.02094, - 0.09802, - 0.0734 - ], - [ - 0.01737, - 0.0661, - 0.02554 - ], - [ - -0.0279, - 0.0519, - -0.06022 - ], - [ - -0.02057, - 0.03865, - 0.01785 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.L": 10.771234691287294, - "DEF-skirt.L.003": 1.4136845819587607, - "DEF-skirt.003": 10.346675467459745, - "DEF-skirt.007": 0.09543175411841748, - "DEF-skirt.003.seg2": 2.6303551266513883, - "DEF-skirt.003.seg3": 1.595806262183024, - "DEF-skirt.seg1": 1.8893422721806523, - "DEF-skirt.003.seg1": 2.168171665547564, - "DEF-skirt.L.004": 0.19603096959511881, - "DEF-skirt.L.seg1": 0.2516482614836598 - }, - { - "DEF-skirt": 1.8893422721806523, - "DEF-skirt.003.seg2": 3.097181898448024, - "DEF-skirt.003.seg1": 1.512647745279899, - "DEF-skirt.003.seg3": 5.169104631170351, - "DEF-skirt.007.seg3": 0.12245594475491542, - "DEF-skirt.005": 0.16033306364800098, - "DEF-skirt.seg2": 2.786772327315499, - "DEF-skirt.009": 0.09372445010591768, - "DEF-skirt.L.seg1": 2.1407108254297182, - "DEF-skirt.007": 0.04866960387074015, - "DEF-skirt.L.seg2": 1.587180682104046, - "DEF-skirt.L.004": 0.20676807425859, - "DEF-skirt.007.seg2": 0.01650988077651644, - "DEF-skirt.L.003": 0.31053856632945737, - "DEF-skirt.003": 0.0982967789470155, - "DEF-skirt.L": 0.12044016764635926 - }, - { - "DEF-skirt.003.seg3": 5.207492995592394, - "DEF-skirt.seg3": 2.161155452056163, - "DEF-skirt.007.seg2": 0.05601081196344071, - "DEF-skirt.L.seg3": 2.7192597439563233, - "DEF-skirt.007.seg1": 0.09495247018795672, - "DEF-skirt.L.001": 0.07432020535983441, - "DEF-skirt.005": 0.4334825864653958, - "DEF-skirt.L.004": 0.14029811314042995, - "DEF-skirt.009": 0.16869629844680606, - "DEF-skirt.seg1": 2.786772327315499, - "DEF-skirt.011": 0.15320185432908007, - "DEF-skirt.007.seg3": 0.1889945161820596, - "DEF-skirt.009.seg1": 0.0714384640111776, - "DEF-skirt.009.seg2": 0.01027086487236381, - "DEF-skirt.005.seg1": 0.007790076778398158, - "DEF-skirt.L.seg2": 3.2111160715999776, - "DEF-skirt.L.003": 0.7391116369404801, - "DEF-skirt.003.seg1": 0.12276459342907578, - "DEF-skirt.L.seg1": 0.3928760868859329, - "DEF-skirt.003.seg2": 0.2754052277329222 - }, - { - "DEF-skirt.002": 0.25142821495506595, - "DEF-skirt.011.seg1": 5.570096570820647, - "DEF-skirt.L.001": 0.4132729913690881, - "DEF-skirt.011": 4.6961528319649295, - "DEF-skirt.003.seg3": 8.365425825133514, - "DEF-skirt.005.seg2": 4.61498402848288, - "DEF-skirt.009": 0.7582600522037454, - "DEF-skirt.009.seg1": 0.9144382488613035, - "DEF-skirt.seg2": 2.161155452056163, - "DEF-skirt.L.seg3": 4.3393369779026685, - "DEF-skirt.011.seg2": 2.8357110381653126, - "DEF-skirt.006": 0.1217170026805725, - "DEF-skirt.005.seg1": 3.920393819893497, - "DEF-skirt.005": 1.3755075436915636, - "DEF-skirt.L.004": 0.8155590405608446, - "DEF-skirt.007.seg3": 0.42513202420982066, - "DEF-skirt.009.seg2": 0.16737698817810986, - "DEF-skirt.007.seg2": 0.04747602245800686, - "DEF-skirt.011.seg3": 0.14448731559887507, - "DEF-skirt.005.seg3": 0.040614199510271565, - "DEF-skirt.L.004.seg1": 0.029836616482513456, - "DEF-skirt.L.seg2": 0.0966744171849947, - "DEF-skirt.L.003": 0.04019258132871978 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine.001", - "bones": [ - "DEF-skirt.003", - "DEF-skirt.003.seg1", - "DEF-skirt.003.seg2", - "DEF-skirt.003.seg3" - ], - "tips": [ - [ - 0.0, - 0.01789, - -0.0 - ], - [ - 0.0, - 0.01789, - -0.0 - ], - [ - -0.0, - 0.01789, - -0.0 - ], - [ - -0.01933, - 0.17699, - 0.01439 - ] - ], - "hulls": [ - [ - [ - -0.05008, - -0.05696, - -0.04666 - ], - [ - 0.03924, - 0.00423, - 0.05153 - ], - [ - -0.02261, - 0.01376, - -0.00513 - ], - [ - -0.01061, - -0.04128, - -0.00124 - ], - [ - -0.04675, - -0.01389, - -0.0306 - ], - [ - 0.01629, - 0.00121, - 0.01377 - ], - [ - 0.00116, - -0.03008, - 0.03884 - ], - [ - 0.04402, - -0.0359, - 0.03842 - ], - [ - -0.03107, - -0.02404, - -0.06145 - ], - [ - -0.03916, - -0.04651, - -0.02056 - ] - ], - [ - [ - 0.05649, - 0.01409, - -0.02176 - ], - [ - -0.02953, - 0.00775, - -0.02538 - ], - [ - 0.01593, - 0.01651, - -0.02885 - ], - [ - 0.03006, - 0.00965, - 0.00505 - ], - [ - -0.00527, - 0.00065, - -0.02313 - ], - [ - 0.03474, - 0.00541, - -0.02441 - ], - [ - 0.03365, - 0.00732, - -0.01286 - ], - [ - 0.05361, - 0.01518, - -0.01043 - ], - [ - 0.0404, - 0.01194, - -0.01841 - ], - [ - -0.02809, - 0.0097, - -0.01762 - ] - ], - [ - [ - 0.05188, - 0.00935, - -0.01886 - ], - [ - -0.00277, - 0.01551, - 0.0356 - ], - [ - -0.00301, - 0.00995, - -0.03276 - ], - [ - 0.0326, - 0.00707, - 0.03816 - ], - [ - -0.01411, - 0.01538, - -0.00122 - ], - [ - 0.02057, - 0.01059, - 0.03576 - ], - [ - 0.04274, - 0.01002, - -0.02639 - ], - [ - 0.00871, - 0.01341, - 0.03461 - ], - [ - -0.00379, - 0.0099, - -0.02409 - ], - [ - -0.00263, - 0.00945, - -0.03186 - ] - ], - [ - [ - 0.00158, - 0.24736, - -0.0107 - ], - [ - -0.06139, - 0.00862, - 0.01247 - ], - [ - 0.05627, - 0.11884, - 0.02984 - ], - [ - 0.02881, - 0.00607, - -0.03046 - ], - [ - -0.04123, - 0.12774, - 0.04129 - ], - [ - 0.00339, - 0.07794, - -0.01952 - ], - [ - -0.01982, - 0.19296, - 0.03012 - ], - [ - 0.03832, - 0.05577, - 0.03226 - ], - [ - -0.03876, - 0.06343, - 0.04015 - ], - [ - 0.01598, - 0.1495, - 0.00201 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt": 10.346675467459745, - "DEF-skirt.007": 17.306699839384407, - "DEF-skirt.003.seg1": 2.1973733196999894, - "DEF-skirt.L": 7.836036998479067, - "DEF-skirt.seg1": 0.0982967789470155, - "DEF-skirt.L.003": 0.05178392130275811, - "DEF-skirt.R": 1.8390214778296556, - "DEF-skirt.007.seg1": 0.03645670787178257 - }, - { - "DEF-skirt.seg1": 1.512647745279899, - "DEF-skirt": 2.168171665547564, - "DEF-skirt.003.seg2": 1.2503227871553237, - "DEF-skirt.003": 2.1973733196999894, - "DEF-skirt.L.seg2": 0.33299535676595077, - "DEF-skirt.seg2": 0.12276459342907578, - "DEF-skirt.L.seg1": 0.2760094692193936, - "DEF-skirt.L.003": 0.049952687783858335 - }, - { - "DEF-skirt": 2.6303551266513883, - "DEF-skirt.003.seg3": 2.4862878647459423, - "DEF-skirt.seg1": 3.097181898448024, - "DEF-skirt.003.seg1": 1.2503227871553237, - "DEF-skirt.L.seg1": 0.4596588821250581, - "DEF-skirt.007": 0.1699214252576411, - "DEF-skirt.L.seg2": 0.29650027410600566, - "DEF-skirt.seg2": 0.2754052277329222, - "DEF-skirt.L.seg3": 0.024977540935678944, - "DEF-skirt.L.003": 0.03862589214435566 - }, - { - "DEF-skirt.006": 0.32341552001905205, - "DEF-skirt.005.seg1": 4.093617441427922, - "DEF-skirt.005": 2.6745725910706306, - "DEF-skirt.seg3": 8.365425825133514, - "DEF-skirt.005.seg2": 4.8612654356533715, - "DEF-skirt.009": 1.1286794898007495, - "DEF-skirt.009.seg1": 0.8940016644600399, - "DEF-skirt.011.seg1": 1.3719167847603355, - "DEF-skirt.seg2": 5.207492995592394, - "DEF-skirt.007.seg2": 1.980115868442557, - "DEF-skirt.011": 1.6883411692987111, - "DEF-skirt.007.seg1": 2.9822736639827294, - "DEF-skirt.L.seg3": 0.5467618653323603, - "DEF-skirt": 1.595806262183024, - "DEF-skirt.003.seg2": 2.4862878647459423, - "DEF-skirt.seg1": 5.169104631170351, - "DEF-skirt.007.seg3": 2.7651239767046945, - "DEF-skirt.009.seg2": 0.18218562382336984, - "DEF-skirt.R.seg2": 0.02413577489838903, - "DEF-skirt.R.seg1": 0.07982489265778714, - "DEF-skirt.007": 1.5140279785753088, - "DEF-skirt.011.seg2": 0.25246157887255904, - "DEF-skirt.005.seg3": 0.04013802336446326, - "DEF-skirt.R.seg3": 0.018324890406280048, - "DEF-skirt.L.seg2": 0.5537837520574962, - "DEF-skirt.L.003": 0.1310899159343887, - "DEF-skirt.R": 0.058546876452437446, - "DEF-skirt.L.seg1": 0.07840127522781742 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine.001", - "bones": [ - "DEF-skirt.L", - "DEF-skirt.L.seg1", - "DEF-skirt.L.seg2", - "DEF-skirt.L.seg3" - ], - "tips": [ - [ - -0.0, - 0.03781, - -0.0 - ], - [ - -0.0, - 0.03781, - -0.0 - ], - [ - 0.0, - 0.03781, - -0.0 - ], - [ - -0.01353, - 0.14671, - 0.03405 - ] - ], - "hulls": [ - [ - [ - -0.01718, - -0.03886, - 0.07057 - ], - [ - 0.03955, - 0.02731, - -0.04543 - ], - [ - 0.01011, - -0.04105, - -0.01324 - ], - [ - -0.01167, - 0.00334, - 0.01467 - ], - [ - -0.01033, - 0.03067, - -0.02984 - ], - [ - 0.00037, - -0.0048, - 0.0536 - ], - [ - 0.034, - -0.00826, - -0.03108 - ], - [ - 0.01864, - -0.03554, - -0.04963 - ], - [ - -0.01519, - -0.03227, - 0.00973 - ], - [ - -0.00034, - 0.00351, - -0.01547 - ] - ], - [ - [ - -0.01733, - 0.03016, - 0.03129 - ], - [ - 0.00029, - 0.03363, - -0.01135 - ], - [ - 0.01304, - 0.01139, - 0.02869 - ], - [ - -0.0117, - 0.00289, - 0.01837 - ], - [ - -0.00597, - 0.03249, - 0.01887 - ], - [ - 0.002, - 0.03388, - 0.00543 - ], - [ - -0.0113, - 0.00187, - 0.01831 - ], - [ - 0.00094, - 0.03396, - 0.00519 - ], - [ - -0.01822, - 0.03022, - 0.03066 - ], - [ - 0.01244, - 0.01059, - 0.02825 - ] - ], - [ - [ - 0.02797, - 0.02078, - -0.06929 - ], - [ - -0.00319, - 0.02407, - 0.05732 - ], - [ - 0.03008, - 0.0173, - -0.01102 - ], - [ - 0.00404, - 0.00325, - 0.00995 - ], - [ - 0.02352, - 0.03451, - 0.01921 - ], - [ - 0.01962, - 0.01154, - -0.03737 - ], - [ - -0.01275, - 0.00262, - 0.04527 - ], - [ - 0.03356, - 0.02949, - -0.04329 - ], - [ - 0.0172, - 0.00476, - -0.0589 - ], - [ - 0.01944, - 0.01544, - 0.01925 - ] - ], - [ - [ - -0.07888, - 0.14866, - 0.10517 - ], - [ - 0.02098, - 0.00616, - -0.07077 - ], - [ - 0.00728, - 0.17813, - -0.01431 - ], - [ - -0.04021, - 0.01427, - 0.07286 - ], - [ - -0.02487, - 0.08854, - -0.00247 - ], - [ - 0.02708, - 0.01623, - 0.01893 - ], - [ - 0.00583, - 0.14213, - -0.07387 - ], - [ - -0.02766, - 0.13164, - 0.04888 - ], - [ - -0.0686, - 0.06595, - 0.08917 - ], - [ - -0.00615, - 0.02573, - -0.03441 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt": 10.771234691287294, - "DEF-skirt.L.003": 16.419495031726157, - "DEF-skirt.L.003.seg1": 1.5219704851834268, - "DEF-skirt.L.003.seg3": 1.5856033828218843, - "DEF-skirt.R.003": 3.3102703706591687, - "DEF-skirt.003": 7.836036998479067, - "DEF-skirt.L.seg1": 0.8892123245093044, - "DEF-skirt.seg1": 0.12044016764635926, - "DEF-skirt.L.003.seg2": 0.9837299947917074, - "DEF-skirt.R": 0.7833441613586325 - }, - { - "DEF-skirt.L.003": 4.760126868318332, - "DEF-skirt.seg1": 2.1407108254297182, - "DEF-skirt.L.seg2": 4.246796835031998, - "DEF-skirt.003.seg2": 0.4596588821250581, - "DEF-skirt.L.003.seg1": 0.6595836843532097, - "DEF-skirt.seg2": 0.3928760868859329, - "DEF-skirt.003.seg1": 0.2760094692193936, - "DEF-skirt": 0.2516482614836598, - "DEF-skirt.L": 0.8892123245093044, - "DEF-skirt.L.003.seg2": 0.322305533592365, - "DEF-skirt.003.seg3": 0.07840127522781742 - }, - { - "DEF-skirt.L.004": 0.06935733543747569, - "DEF-skirt.L.003": 4.430261283824864, - "DEF-skirt.L.seg3": 3.686576538966275, - "DEF-skirt.L.003.seg3": 0.009721631050302243, - "DEF-skirt.L.003.seg2": 0.19439024437881924, - "DEF-skirt.L.seg1": 4.246796835031998, - "DEF-skirt.seg1": 1.587180682104046, - "DEF-skirt.L.003.seg1": 0.1957218245564052, - "DEF-skirt.seg2": 3.2111160715999776, - "DEF-skirt.003.seg3": 0.5537837520574962, - "DEF-skirt.seg3": 0.0966744171849947, - "DEF-skirt.003.seg1": 0.33299535676595077, - "DEF-skirt.003.seg2": 0.29650027410600566, - "DEF-skirt.L.001": 0.10371684437270923, - "DEF-skirt.011": 0.01208546172060343 - }, - { - "DEF-skirt.L.001.seg2": 0.1699154302186351, - "DEF-skirt.L.002": 0.10102962666415694, - "DEF-skirt.L.001.seg1": 0.19093190472623134, - "DEF-skirt.L.005": 0.04318668795316161, - "DEF-skirt.L.004": 3.8285142497834164, - "DEF-skirt.011.seg3": 4.602221938080493, - "DEF-skirt.011.seg2": 3.722823119417367, - "DEF-skirt.L.004.seg1": 0.9764221426855458, - "DEF-skirt.L.003": 3.6402760699503562, - "DEF-skirt.L.seg2": 3.686576538966275, - "DEF-skirt.L.001": 1.7430155018692777, - "DEF-skirt.seg3": 4.3393369779026685, - "DEF-skirt.seg2": 2.7192597439563233, - "DEF-skirt.003.seg3": 0.5467618653323603, - "DEF-skirt.L.003.seg3": 1.533915214427354, - "DEF-skirt.L.003.seg2": 0.7716510583968516, - "DEF-skirt.005.seg2": 0.03759796665737425, - "DEF-skirt.011.seg1": 0.6236902469398551, - "DEF-skirt.L.003.seg1": 0.7488992127140781, - "DEF-skirt.011": 0.13239584627096912, - "DEF-skirt.003.seg2": 0.024977540935678944 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine.001", - "bones": [ - "DEF-skirt.L.003", - "DEF-skirt.L.003.seg1", - "DEF-skirt.L.003.seg2", - "DEF-skirt.L.003.seg3" - ], - "tips": [ - [ - -0.0, - 0.01473, - -0.0 - ], - [ - 0.0, - 0.01473, - 0.0 - ], - [ - 0.0, - 0.01473, - 0.0 - ], - [ - 0.0279, - 0.14363, - -0.02295 - ] - ], - "hulls": [ - [ - [ - -0.11938, - 0.01472, - 0.08554 - ], - [ - 0.03737, - -0.00762, - -0.02167 - ], - [ - -0.04228, - -0.06138, - 0.03614 - ], - [ - -0.06996, - 0.01341, - 0.01895 - ], - [ - -0.01719, - 0.00218, - -0.0034 - ], - [ - -0.0946, - -0.01653, - 0.05222 - ], - [ - -0.02027, - -0.04183, - -0.005 - ], - [ - -0.11124, - -0.02195, - 0.08811 - ], - [ - -0.00607, - -0.04564, - 0.02964 - ], - [ - 0.02042, - 0.01328, - 0.00447 - ] - ], - [ - [ - 0.09911, - 0.00038, - -0.10791 - ], - [ - -0.02717, - 0.01394, - 0.0105 - ], - [ - 0.04307, - 0.00641, - -0.04658 - ], - [ - 0.03046, - 0.00077, - -0.00282 - ], - [ - 0.07423, - 0.01319, - -0.03945 - ], - [ - -0.00096, - 0.0041, - -0.00347 - ], - [ - -0.01848, - 0.0026, - 0.00091 - ], - [ - 0.00014, - 0.00336, - 0.01308 - ], - [ - 0.02267, - 0.00104, - -0.0143 - ], - [ - -0.02332, - 0.00279, - 0.01466 - ] - ], - [ - [ - 0.08569, - 0.00881, - -0.09381 - ], - [ - -0.04002, - 0.00864, - 0.02482 - ], - [ - 0.02842, - 0.00921, - -0.02168 - ], - [ - 0.06602, - 0.00044, - -0.04476 - ], - [ - -0.00207, - 0.01085, - 0.01334 - ], - [ - 0.03399, - 0.0046, - -0.00424 - ], - [ - 0.03981, - 0.00639, - -0.03647 - ], - [ - -0.00247, - 0.01096, - 0.01233 - ], - [ - -0.04035, - 0.00881, - 0.02379 - ], - [ - 0.08488, - 0.00908, - -0.0945 - ] - ], - [ - [ - -0.1601, - 0.2389, - 0.17881 - ], - [ - 0.18049, - 0.03004, - -0.17856 - ], - [ - -0.044, - 0.01497, - 0.02135 - ], - [ - 0.06877, - 0.11585, - -0.07637 - ], - [ - 0.08415, - 0.01272, - -0.07912 - ], - [ - 0.03725, - 0.00859, - -0.00617 - ], - [ - 0.15246, - 0.07652, - -0.13417 - ], - [ - -0.11244, - 0.19171, - 0.16551 - ], - [ - 0.05084, - 0.0605, - -0.06074 - ], - [ - 0.12019, - 0.02429, - -0.12566 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt": 1.4136845819587607, - "DEF-skirt.L": 16.419495031726157, - "DEF-skirt.L.004": 0.21646333191305667, - "DEF-skirt.L.seg3": 3.6402760699503562, - "DEF-skirt.L.seg2": 4.430261283824864, - "DEF-skirt.L.001": 0.23974937651991812, - "DEF-skirt.L.003.seg1": 6.056427661191389, - "DEF-skirt.R.003": 3.8759857822260786, - "DEF-skirt.L.seg1": 4.760126868318332, - "DEF-skirt.seg2": 0.7391116369404801, - "DEF-skirt.003.seg3": 0.1310899159343887, - "DEF-skirt.seg3": 0.04019258132871978, - "DEF-skirt.seg1": 0.31053856632945737, - "DEF-skirt.003.seg1": 0.049952687783858335, - "DEF-skirt.003.seg2": 0.03862589214435566, - "DEF-skirt.R": 0.7233312277085479, - "DEF-skirt.003": 0.05178392130275811 - }, - { - "DEF-skirt.L.003": 6.056427661191389, - "DEF-skirt.R.003": 3.5362091427452844, - "DEF-skirt.L.003.seg2": 1.4465578586045362, - "DEF-skirt.R.003.seg1": 0.0075091120423149416, - "DEF-skirt.L.004": 0.3003293425343978, - "DEF-skirt.L.seg3": 0.7488992127140781, - "DEF-skirt.L.seg2": 0.1957218245564052, - "DEF-skirt.L.seg1": 0.6595836843532097, - "DEF-skirt.L": 1.5219704851834268, - "DEF-skirt.R": 0.6615938216234099 - }, - { - "DEF-skirt.L.003.seg3": 1.7624733424013248, - "DEF-skirt.R.003": 5.005937962569201, - "DEF-skirt.L.004": 0.677365454643255, - "DEF-skirt.L.seg3": 0.7716510583968516, - "DEF-skirt.L.seg2": 0.19439024437881924, - "DEF-skirt.L.003.seg1": 1.4465578586045362, - "DEF-skirt.R.003.seg1": 0.1158529500045482, - "DEF-skirt.L": 0.9837299947917074, - "DEF-skirt.L.seg1": 0.322305533592365, - "DEF-skirt.R": 0.7641415590885685 - }, - { - "DEF-skirt.L.003.seg2": 1.7624733424013248, - "DEF-skirt.R.003": 4.763912732133256, - "DEF-skirt.L.005.seg2": 0.07865966284521508, - "DEF-skirt.L.004.seg3": 0.5971418419198388, - "DEF-skirt.L.005.seg1": 0.09606776911003378, - "DEF-skirt.R.005.seg3": 0.5377347293486077, - "DEF-skirt.L.005": 1.1878016405318974, - "DEF-skirt.L.004.seg2": 5.510255386232652, - "DEF-skirt.R.003.seg3": 5.539891594630126, - "DEF-skirt.L.004.seg1": 7.552187895272386, - "DEF-skirt.R.004": 1.6662561376102858, - "DEF-skirt.L.004": 5.274030080809885, - "DEF-skirt.L.seg3": 1.533915214427354, - "DEF-skirt.L.seg2": 0.009721631050302243, - "DEF-skirt.R.004.seg1": 0.2699483450459348, - "DEF-skirt.R.003.seg2": 1.2716413510540925, - "DEF-skirt.R.003.seg1": 1.7362462282224314, - "DEF-skirt.L": 1.5856033828218843, - "DEF-skirt.R": 0.9611470727391278, - "DEF-skirt.R.005": 0.04733610412909939, - "DEF-hair.R.005": 2.6234847961983174 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine.001", - "bones": [ - "DEF-skirt.R", - "DEF-skirt.R.seg1", - "DEF-skirt.R.seg2", - "DEF-skirt.R.seg3" - ], - "tips": [ - [ - -0.0, - 0.05443, - -0.0 - ], - [ - -0.0, - 0.05443, - 0.0 - ], - [ - -0.0, - 0.05443, - 0.0 - ], - [ - -0.01241, - 0.17736, - -0.03549 - ] - ], - "hulls": [ - [ - [ - -0.02114, - 0.03224, - -0.06298 - ], - [ - 0.01753, - -0.04064, - 0.03519 - ], - [ - -0.03099, - -0.03708, - -0.04283 - ], - [ - -0.01695, - 0.00322, - 0.00621 - ], - [ - -0.04181, - 0.00588, - -0.02927 - ], - [ - -0.01038, - 0.04966, - -0.02862 - ], - [ - 0.0017, - -0.02936, - -0.02451 - ], - [ - 0.0158, - -0.00613, - 0.04503 - ], - [ - -0.00272, - -0.04123, - 0.00572 - ], - [ - 0.01175, - 0.0006, - 0.01497 - ] - ], - [ - [ - -0.0338, - 0.02373, - -0.06959 - ], - [ - 0.03166, - 0.01198, - 0.06806 - ], - [ - -0.03022, - 0.02067, - 0.00739 - ], - [ - 0.01239, - 0.02598, - 0.0297 - ], - [ - -0.01945, - 0.05274, - -0.03983 - ], - [ - -0.00359, - 0.01483, - -0.02065 - ], - [ - -0.00875, - 0.00158, - 0.04019 - ], - [ - -0.01189, - 0.04561, - -0.00244 - ], - [ - -0.03758, - 0.02729, - -0.03186 - ], - [ - -0.01054, - 0.01781, - -0.06306 - ] - ], - [ - [ - 0.03154, - 0.02968, - 0.0552 - ], - [ - -0.04492, - 0.00877, - -0.04155 - ], - [ - -0.00477, - 0.03216, - 0.00435 - ], - [ - -0.0023, - 0.02461, - -0.05262 - ], - [ - -0.03492, - 0.00104, - 0.00234 - ], - [ - -0.01097, - 0.02919, - -0.03038 - ], - [ - -0.00754, - 0.01281, - -0.00936 - ], - [ - 0.01201, - 0.03077, - 0.01999 - ], - [ - 0.02955, - 0.015, - 0.03963 - ], - [ - -0.01696, - 0.03229, - -0.01065 - ] - ], - [ - [ - -0.03429, - 0.15006, - -0.0334 - ], - [ - 0.03093, - 0.03534, - 0.06112 - ], - [ - -0.01386, - 0.02495, - -0.0922 - ], - [ - 0.02118, - 0.12149, - 0.04485 - ], - [ - -0.02544, - 0.03965, - -0.01748 - ], - [ - -0.01071, - 0.09867, - -0.07886 - ], - [ - -0.01355, - 0.09263, - -0.00563 - ], - [ - 0.01192, - 0.03451, - 0.01784 - ], - [ - 0.01219, - 0.03774, - -0.02933 - ], - [ - 0.02983, - 0.09163, - 0.06656 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.R.003": 21.153115252758028, - "DEF-skirt.007": 18.84494971969905, - "DEF-skirt.007.seg1": 0.7598426687282531, - "DEF-skirt.R.seg1": 3.2886585052114654, - "DEF-skirt.007.seg3": 0.5922937660442745, - "DEF-skirt.R.003.seg1": 0.958425160444825, - "DEF-skirt.003.seg3": 0.058546876452437446, - "DEF-skirt.007.seg2": 0.17227260051985324, - "DEF-skirt.L": 0.7833441613586325, - "DEF-skirt.L.003.seg1": 0.6615938216234099, - "DEF-skirt.L.003.seg3": 0.9611470727391278, - "DEF-skirt.L.003.seg2": 0.7641415590885685, - "DEF-skirt.L.003": 0.7233312277085479, - "DEF-skirt.003": 1.8390214778296556, - "DEF-skirt.R.003.seg2": 0.0144911590928406 - }, - { - "DEF-skirt.R.seg2": 3.9264200827331246, - "DEF-skirt.007.seg3": 5.990418387665851, - "DEF-skirt.003.seg3": 0.07982489265778714, - "DEF-skirt.R.003.seg1": 2.624934006784733, - "DEF-skirt.R": 3.2886585052114654, - "DEF-skirt.R.003.seg2": 0.26822713323056313, - "DEF-skirt.R.003": 1.2574131429974016, - "DEF-skirt.007.seg2": 0.014808934502137577, - "DEF-skirt.R.001": 0.07198854155120338, - "DEF-skirt.R.003.seg3": 0.02249763078136091 - }, - { - "DEF-skirt.R.003.seg3": 1.6634802939376083, - "DEF-skirt.R.003.seg2": 0.7907999176082137, - "DEF-skirt.R.001": 1.5263588944722746, - "DEF-skirt.007.seg3": 2.713153852899096, - "DEF-skirt.R.seg1": 3.9264200827331246, - "DEF-skirt.003.seg3": 0.02413577489838903, - "DEF-skirt.R.004": 0.16426175880538585, - "DEF-skirt.R.seg3": 0.09424409085089636, - "DEF-skirt.R.003.seg1": 0.04449534258909127 - }, - { - "DEF-skirt.R.005": 0.020397376975925585, - "DEF-skirt.R.001.seg1": 0.15912477238749645, - "DEF-skirt.R.004.seg1": 0.19151314001136188, - "DEF-skirt.R.004.seg2": 0.04455171348530562, - "DEF-skirt.009.seg3": 4.171412457713149, - "DEF-skirt.R.003.seg3": 7.040713262058846, - "DEF-skirt.009.seg2": 0.6798349904287918, - "DEF-skirt.009.seg1": 0.03686175164852512, - "DEF-skirt.007.seg3": 0.939187919331689, - "DEF-skirt.R.001": 0.6250004873314858, - "DEF-skirt.R.seg2": 0.09424409085089636, - "DEF-skirt.009": 0.08669143553370695, - "DEF-skirt.R.004": 0.21310817027062778, - "DEF-skirt.003.seg3": 0.018324890406280048 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine.001", - "bones": [ - "DEF-skirt.R.003", - "DEF-skirt.R.003.seg1", - "DEF-skirt.R.003.seg2", - "DEF-skirt.R.003.seg3" - ], - "tips": [ - [ - -0.0, - 0.03347, - -0.0 - ], - [ - -0.0, - 0.03347, - -0.0 - ], - [ - -0.0, - 0.03347, - 0.0 - ], - [ - -0.02389, - 0.20399, - 0.00927 - ] - ], - "hulls": [ - [ - [ - 0.0844, - -0.03265, - 0.07657 - ], - [ - -0.05994, - -0.02462, - -0.01868 - ], - [ - 0.04684, - 0.03075, - -0.00621 - ], - [ - 0.01437, - -0.03527, - -0.00805 - ], - [ - 0.05972, - -0.01943, - 0.03067 - ], - [ - -0.02106, - -0.00097, - 0.013 - ], - [ - 0.08081, - 0.02144, - 0.01002 - ], - [ - -0.02151, - -0.02616, - -0.02293 - ], - [ - 0.05128, - -0.00727, - -0.00235 - ], - [ - -0.05568, - 0.00449, - 0.0053 - ] - ], - [ - [ - -0.06047, - 0.01677, - -0.03231 - ], - [ - 0.00516, - 0.03249, - 0.00244 - ], - [ - -0.02776, - 0.02669, - -0.00895 - ], - [ - -0.04241, - 0.02239, - -0.02278 - ], - [ - -0.01253, - 0.03006, - 0.00094 - ], - [ - 0.00139, - 0.01878, - 0.00639 - ], - [ - -0.06013, - 0.00359, - -0.03558 - ], - [ - -0.04959, - 0.02001, - -0.02242 - ], - [ - -0.05991, - 0.01697, - -0.03323 - ], - [ - -0.04301, - 0.02224, - -0.02188 - ] - ], - [ - [ - -0.10588, - 0.01972, - -0.04355 - ], - [ - 0.02367, - 0.0003, - 0.00112 - ], - [ - 0.02368, - 0.00034, - 3e-05 - ], - [ - -0.1051, - 0.02009, - -0.04422 - ] - ], - [ - [ - -0.0632, - 0.17938, - -0.00054 - ], - [ - 0.06162, - 0.01689, - 0.02182 - ], - [ - -0.08635, - 0.01546, - -0.03002 - ], - [ - 0.04345, - 0.13606, - 0.01532 - ], - [ - -0.0313, - 0.08171, - 0.01205 - ], - [ - -0.03046, - 0.01385, - 0.01574 - ], - [ - -0.07855, - 0.12217, - -0.02139 - ], - [ - -0.01458, - 0.15008, - 0.01746 - ], - [ - 0.0647, - 0.08471, - 0.01586 - ], - [ - -0.09754, - 0.06735, - -0.03116 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.L.003.seg3": 4.763912732133256, - "DEF-skirt.L.003.seg2": 5.005937962569201, - "DEF-skirt.R": 21.153115252758028, - "DEF-skirt.007": 1.0301041294739737, - "DEF-skirt.007.seg1": 0.22632611848096784, - "DEF-skirt.L.003.seg1": 3.5362091427452844, - "DEF-skirt.L.003": 3.8759857822260786, - "DEF-skirt.L": 3.3102703706591687, - "DEF-skirt.R.seg1": 1.2574131429974016, - "DEF-skirt.R.003.seg1": 1.0807317991880538 - }, - { - "DEF-skirt.L.003.seg2": 0.1158529500045482, - "DEF-skirt.L.003.seg1": 0.0075091120423149416, - "DEF-skirt.L.003.seg3": 1.7362462282224314, - "DEF-skirt.R.003.seg2": 2.390378788205351, - "DEF-skirt.R.seg1": 2.624934006784733, - "DEF-skirt.R": 0.958425160444825, - "DEF-skirt.R.003": 1.0807317991880538, - "DEF-skirt.R.seg2": 0.04449534258909127 - }, - { - "DEF-skirt.R.003.seg3": 0.33796963284365766, - "DEF-skirt.R.seg2": 0.7907999176082137, - "DEF-skirt.R.001": 0.15603431496875952, - "DEF-skirt.L.003.seg3": 1.2716413510540925, - "DEF-skirt.R.003.seg1": 2.390378788205351, - "DEF-skirt.R.004": 0.016111401937035363, - "DEF-skirt.R.seg1": 0.26822713323056313, - "DEF-skirt.R": 0.0144911590928406 - }, - { - "DEF-skirt.R.005": 1.091596357605304, - "DEF-skirt.L.005": 0.1715765989223872, - "DEF-skirt.L.003.seg3": 5.539891594630126, - "DEF-skirt.R.005.seg3": 0.1178247570438451, - "DEF-skirt.R.002": 0.012816917290188745, - "DEF-skirt.R.001.seg1": 0.11280987179495439, - "DEF-skirt.L.004.seg2": 5.3545195307750895, - "DEF-skirt.L.004.seg1": 1.9171418898471628, - "DEF-skirt.R.004": 4.464269337643888, - "DEF-skirt.L.004.seg3": 1.2748603054718204, - "DEF-skirt.L.004": 0.845388225157766, - "DEF-skirt.R.004.seg1": 3.6078875543646496, - "DEF-skirt.R.seg3": 7.040713262058846, - "DEF-skirt.R.004.seg2": 3.272260373183556, - "DEF-skirt.R.seg2": 1.6634802939376083, - "DEF-skirt.R.003.seg2": 0.33796963284365766, - "DEF-skirt.R.001": 0.07035853311526641, - "DEF-skirt.R.005.seg1": 0.15873896284533995, - "DEF-skirt.R.seg1": 0.02249763078136091 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine.001", - "bones": [ - "DEF-skirt.007", - "DEF-skirt.007.seg1", - "DEF-skirt.007.seg2", - "DEF-skirt.007.seg3" - ], - "tips": [ - [ - 0.0, - 0.02335, - 0.0 - ], - [ - -0.0, - 0.02335, - -0.0 - ], - [ - -0.0, - 0.02335, - -0.0 - ], - [ - 0.03535, - 0.15778, - -0.0477 - ] - ], - "hulls": [ - [ - [ - 0.07416, - 0.02127, - -0.0527 - ], - [ - -0.02713, - -0.02362, - 0.00114 - ], - [ - 0.04523, - -0.0157, - 0.01881 - ], - [ - 0.029, - 0.01019, - -0.02257 - ], - [ - 0.00178, - 0.0014, - 0.02398 - ], - [ - 0.01602, - -0.0364, - 0.01125 - ], - [ - 0.05427, - -0.02101, - -0.0099 - ], - [ - 0.02523, - 0.01242, - 0.00803 - ], - [ - -0.00408, - -0.00653, - -0.00666 - ], - [ - 0.04906, - 0.00665, - 0.00352 - ] - ], - [ - [ - 0.02545, - 0.01511, - -0.05992 - ], - [ - 0.02253, - 0.01262, - -0.04982 - ], - [ - 0.02635, - 0.0152, - -0.0593 - ], - [ - 0.02174, - 0.01292, - -0.05051 - ] - ], - [ - [ - 0.01812, - 0.00211, - -0.04788 - ], - [ - 0.01567, - 0.01366, - -0.02762 - ], - [ - 0.01857, - 0.00977, - -0.03272 - ], - [ - 0.0163, - 0.01437, - -0.02815 - ], - [ - 0.01752, - 0.01005, - -0.0328 - ], - [ - 0.01709, - 0.00219, - -0.04823 - ] - ], - [ - [ - 0.07896, - 0.05691, - -0.14429 - ], - [ - -0.01373, - 0.01928, - 0.01223 - ], - [ - 0.03479, - 0.13195, - -0.03282 - ], - [ - 0.01653, - 0.03488, - -0.07348 - ], - [ - 0.04884, - 0.09787, - -0.09754 - ], - [ - 0.02565, - 0.07312, - -0.01098 - ], - [ - -0.004, - 6e-05, - -0.03836 - ], - [ - 0.01205, - 0.13481, - 0.00179 - ], - [ - 0.04044, - 0.05296, - -0.0456 - ], - [ - 0.04575, - 0.02487, - -0.09731 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.R": 18.84494971969905, - "DEF-skirt.R.003": 1.0301041294739737, - "DEF-skirt.007.seg1": 1.8201384429565644, - "DEF-skirt": 0.09543175411841748, - "DEF-skirt.003": 17.306699839384407, - "DEF-skirt.003.seg3": 1.5140279785753088, - "DEF-skirt.seg1": 0.04866960387074015, - "DEF-skirt.003.seg2": 0.1699214252576411 - }, - { - "DEF-skirt.R.003": 0.22632611848096784, - "DEF-skirt.R": 0.7598426687282531, - "DEF-skirt.007": 1.8201384429565644, - "DEF-skirt.003.seg3": 2.9822736639827294, - "DEF-skirt.seg2": 0.09495247018795672, - "DEF-skirt.007.seg2": 0.7137936641847316, - "DEF-skirt.003": 0.03645670787178257 - }, - { - "DEF-skirt.seg2": 0.05601081196344071, - "DEF-skirt.003.seg3": 1.980115868442557, - "DEF-skirt.seg3": 0.04747602245800686, - "DEF-skirt.005": 0.016322135250931735, - "DEF-skirt.007.seg1": 0.7137936641847316, - "DEF-skirt.007.seg3": 0.623153370820441, - "DEF-skirt.seg1": 0.01650988077651644, - "DEF-skirt.R": 0.17227260051985324, - "DEF-skirt.R.seg1": 0.014808934502137577 - }, - { - "DEF-skirt.010.seg1": 0.015989096555482363, - "DEF-skirt.R.001": 0.2677451548737494, - "DEF-skirt.010": 0.23355445061974422, - "DEF-skirt.009.seg2": 4.5295165581740635, - "DEF-skirt.009.seg3": 1.5188658246344597, - "DEF-skirt.009.seg1": 2.906074152306811, - "DEF-skirt.003.seg3": 2.7651239767046945, - "DEF-skirt.R.seg2": 2.713153852899096, - "DEF-skirt.R.seg1": 5.990418387665851, - "DEF-skirt.005.seg2": 0.3911211444359659, - "DEF-skirt.005.seg1": 0.7179370267663041, - "DEF-skirt.seg1": 0.12245594475491542, - "DEF-skirt.seg3": 0.42513202420982066, - "DEF-skirt.009": 9.43662584664051, - "DEF-skirt.005": 0.23030644633612807, - "DEF-skirt.R": 0.5922937660442745, - "DEF-skirt.R.seg3": 0.939187919331689, - "DEF-skirt.seg2": 0.1889945161820596, - "DEF-skirt.007.seg2": 0.623153370820441 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.002", - "DEF-skirt.002.seg1", - "DEF-skirt.002.seg2", - "DEF-skirt.002.seg3" - ], - "tips": [ - [ - -0.0, - 0.02131, - 0.0 - ], - [ - 0.0, - 0.02131, - 0.0 - ], - [ - 0.0, - 0.02131, - 0.0 - ], - [ - 0.00417, - 0.04451, - 0.05542 - ] - ], - "hulls": [ - [ - [ - 0.00491, - 0.02036, - 0.02338 - ], - [ - 0.00118, - -0.01008, - -0.00922 - ], - [ - -0.00535, - 0.02023, - 0.02123 - ], - [ - 0.00431, - 0.02083, - 0.0226 - ], - [ - 0.00206, - -0.00953, - -0.00956 - ], - [ - -0.00449, - 0.02079, - 0.02086 - ] - ], - [], - [ - [ - -0.006, - 0.00129, - -0.04552 - ], - [ - 0.00173, - 0.0009, - -0.04551 - ], - [ - 0.00111, - 0.00086, - -0.04461 - ], - [ - -0.00544, - 0.00115, - -0.0446 - ] - ], - [ - [ - 0.00349, - 0.00442, - 0.04343 - ], - [ - -0.0032, - 0.02814, - 0.02127 - ], - [ - 0.00456, - 0.02879, - 0.02285 - ], - [ - -0.00265, - 0.02743, - 0.02189 - ], - [ - 0.00243, - 0.00429, - 0.04321 - ], - [ - 0.00375, - 0.0281, - 0.0231 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.002.seg1": 0.9936210139550372, - "DEF-skirt.011.seg2": 0.6140337423742315, - "DEF-skirt.011.seg1": 1.0432309729624893, - "DEF-skirt.seg3": 0.25142821495506595, - "DEF-skirt.L.001": 0.17433376040275084, - "DEF-skirt.011": 0.17798888515931122 - }, - { - "DEF-skirt.002.seg2": 1.0752501775274976, - "DEF-skirt.011.seg3": 0.3210989021621229, - "DEF-skirt.002": 0.9936210139550372, - "DEF-skirt.011.seg2": 0.5566789920191999, - "DEF-skirt.L.002": 0.09750582234609562, - "DEF-skirt.011.seg1": 0.07562430091019157, - "DEF-skirt.L.001": 0.23053920218779567, - "DEF-skirt.L.001.seg1": 0.07011653736356815 - }, - { - "DEF-skirt.002.seg1": 1.0752501775274976, - "DEF-skirt.011.seg3": 0.5137219364069667, - "DEF-skirt.L.002": 0.08363487573375844, - "DEF-skirt.002.seg3": 0.15905285339786146, - "DEF-skirt.011.seg2": 0.09621669328872662, - "DEF-skirt.L.001.seg1": 0.07726076569748527, - "DEF-skirt.011.seg1": 0.012493393667064645, - "DEF-skirt.L.001": 0.03671082080693666 - }, - { - "DEF-skirt.L.002": 0.5233835144511887, - "DEF-skirt.011.seg3": 0.8674111035481489, - "DEF-skirt.L.001.seg1": 0.45340715496411454, - "DEF-skirt.002.seg2": 0.15905285339786146, - "DEF-skirt.011.seg2": 0.11034352255838153 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.011", - "DEF-skirt.011.seg1", - "DEF-skirt.011.seg2", - "DEF-skirt.011.seg3" - ], - "tips": [ - [ - 0.0, - 0.04148, - -0.0 - ], - [ - 0.0, - 0.04148, - 0.0 - ], - [ - 0.0, - 0.04148, - -0.0 - ], - [ - 0.01816, - 0.02605, - 0.10442 - ] - ], - "hulls": [ - [ - [ - 0.04555, - 0.01719, - 0.05065 - ], - [ - -0.01818, - 0.0315, - -0.01141 - ], - [ - 0.04426, - 0.04093, - 0.00452 - ], - [ - -0.01417, - 0.04083, - 0.02064 - ], - [ - 0.01652, - 0.02853, - 0.01278 - ], - [ - 0.0061, - 0.04136, - -0.0152 - ], - [ - 0.04377, - 0.02788, - 0.02971 - ], - [ - -0.00782, - 0.01914, - 0.01736 - ], - [ - 0.00605, - 0.04099, - -0.01622 - ], - [ - 0.01608, - 0.02948, - 0.01247 - ] - ], - [ - [ - -0.03298, - 0.02109, - -0.1014 - ], - [ - 0.044, - 0.0191, - 0.03982 - ], - [ - -0.02397, - 0.02043, - -0.02027 - ], - [ - -0.01378, - 0.02499, - 0.05045 - ], - [ - 0.01469, - 0.03475, - -0.05242 - ], - [ - -0.02705, - 0.01748, - -0.06127 - ], - [ - 0.00949, - 0.00032, - -0.02296 - ], - [ - -0.02195, - 0.00929, - 0.01784 - ], - [ - 0.00381, - 0.03256, - -0.00438 - ], - [ - 0.02225, - 0.02837, - 0.01891 - ] - ], - [ - [ - 0.00183, - 0.02126, - 0.06646 - ], - [ - -0.00828, - 0.0029, - -0.04347 - ], - [ - 0.05092, - 0.0052, - 0.02605 - ], - [ - 0.0205, - 0.03405, - -0.00684 - ], - [ - -0.00219, - 0.02441, - 0.02832 - ], - [ - -0.01201, - 0.00779, - -0.00708 - ], - [ - 0.02938, - 0.00509, - -0.0261 - ], - [ - 0.02464, - 0.00163, - 0.01023 - ], - [ - -0.03066, - 0.01182, - -0.03711 - ], - [ - 0.0055, - 0.02391, - -0.02374 - ] - ], - [ - [ - 0.00434, - 0.01589, - 0.1013 - ], - [ - 0.03806, - 0.00779, - 0.00774 - ], - [ - -0.00642, - 0.01225, - 0.04932 - ], - [ - 0.00873, - 0.00112, - 0.01833 - ], - [ - 0.01395, - 0.02871, - 0.07745 - ], - [ - -0.00506, - 0.00682, - 0.08185 - ], - [ - 0.0056, - 0.02706, - 0.07629 - ], - [ - 0.01711, - 0.00205, - 0.01798 - ], - [ - 0.00143, - 0.01361, - 0.04976 - ], - [ - -0.00568, - 0.01153, - 0.04966 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.011.seg1": 4.58106049623973, - "DEF-skirt.002": 0.17798888515931122, - "DEF-skirt.seg3": 4.6961528319649295, - "DEF-skirt.003.seg3": 1.6883411692987111, - "DEF-skirt.005.seg2": 1.117578855302317, - "DEF-skirt.005.seg1": 1.9069345583682153, - "DEF-skirt.005": 0.20566438243868845, - "DEF-skirt.L.seg3": 0.13239584627096912, - "DEF-skirt.seg2": 0.15320185432908007, - "DEF-skirt.L.seg2": 0.01208546172060343, - "DEF-skirt.005.seg3": 0.006026445906482214 - }, - { - "DEF-skirt.011": 4.58106049623973, - "DEF-skirt.011.seg2": 7.206079765921628, - "DEF-skirt.L.002": 0.02658579166615116, - "DEF-skirt.002.seg2": 0.012493393667064645, - "DEF-skirt.002.seg1": 0.07562430091019157, - "DEF-skirt.L.001": 1.879150088933239, - "DEF-skirt.L.001.seg1": 0.060376459628942836, - "DEF-skirt.002": 1.0432309729624893, - "DEF-skirt.seg3": 5.570096570820647, - "DEF-skirt.003.seg3": 1.3719167847603355, - "DEF-skirt.005.seg2": 3.0893325526066313, - "DEF-skirt.005.seg1": 1.0448083213115291, - "DEF-skirt.L.seg3": 0.6236902469398551, - "DEF-skirt.005.seg3": 0.031181913198806976 - }, - { - "DEF-skirt.011.seg1": 7.206079765921628, - "DEF-skirt.L.002": 0.1453999495240184, - "DEF-skirt.002.seg3": 0.11034352255838153, - "DEF-skirt.011.seg3": 2.7529092791157663, - "DEF-skirt.002.seg2": 0.09621669328872662, - "DEF-skirt.L.001.seg1": 0.8697016458385258, - "DEF-skirt.002.seg1": 0.5566789920191999, - "DEF-skirt.002": 0.6140337423742315, - "DEF-skirt.L.001": 1.0570456406771491, - "DEF-skirt.L.seg3": 3.722823119417367, - "DEF-skirt.seg3": 2.8357110381653126, - "DEF-skirt.005.seg2": 1.4663347373091402, - "DEF-skirt.005.seg1": 0.06153290296288072, - "DEF-skirt.003.seg3": 0.25246157887255904, - "DEF-skirt.L.001.seg2": 0.02397434757873418 - }, - { - "DEF-skirt.002.seg2": 0.5137219364069667, - "DEF-skirt.002.seg1": 0.3210989021621229, - "DEF-skirt.L.002": 0.2979373224340631, - "DEF-skirt.002.seg3": 0.8674111035481489, - "DEF-skirt.L.001.seg1": 0.43628207869348157, - "DEF-skirt.011.seg2": 2.7529092791157663, - "DEF-skirt.L.seg3": 4.602221938080493, - "DEF-skirt.seg3": 0.14448731559887507, - "DEF-skirt.005.seg2": 0.06419735157119555, - "DEF-skirt.L.001.seg2": 0.020762523308016423 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.006", - "DEF-skirt.006.seg1", - "DEF-skirt.006.seg2", - "DEF-skirt.006.seg3" - ], - "tips": [ - [ - 0.0, - 0.00893, - -0.0 - ], - [ - 0.0, - 0.00893, - 0.0 - ], - [ - 0.0, - 0.00893, - -0.0 - ], - [ - -0.00117, - 0.04461, - -0.0053 - ] - ], - "hulls": [ - [ - [ - 0.00216, - -0.04675, - -0.00554 - ], - [ - 0.00152, - -0.01123, - -0.00176 - ], - [ - 0.00053, - -0.01137, - -0.00133 - ], - [ - 0.00319, - -0.04662, - -0.00587 - ] - ], - [], - [], - [ - [ - -0.00142, - 0.03972, - -0.00011 - ], - [ - -0.00123, - 0.00532, - -0.00385 - ], - [ - 0.00706, - 0.00757, - 0.0056 - ], - [ - -0.00256, - 0.03845, - -0.00764 - ], - [ - 0.00607, - 0.00733, - 0.00522 - ], - [ - -0.002, - 0.03881, - -0.00032 - ], - [ - -0.00168, - 0.00542, - -0.00286 - ], - [ - -0.0029, - 0.03793, - -0.00674 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.005.seg1": 1.2229846542125553, - "DEF-skirt.005.seg2": 0.38039666877214595, - "DEF-skirt.003.seg3": 0.32341552001905205, - "DEF-skirt.005": 0.26069539357345217, - "DEF-skirt.seg3": 0.1217170026805725 - }, - {}, - {}, - { - "DEF-skirt.005.seg2": 0.6104225058278434, - "DEF-skirt.005.seg3": 0.24410953164354598 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.005", - "DEF-skirt.005.seg1", - "DEF-skirt.005.seg2", - "DEF-skirt.005.seg3" - ], - "tips": [ - [ - -0.0, - 0.04557, - 0.0 - ], - [ - -0.0, - 0.04557, - 0.0 - ], - [ - -0.0, - 0.04557, - 0.0 - ], - [ - -0.0, - 0.04557, - 0.0 - ] - ], - "hulls": [ - [], - [ - [ - 0.04663, - 0.01394, - 0.04105 - ], - [ - -0.02303, - 0.03049, - -0.02364 - ], - [ - -0.00475, - 0.0113, - 0.02904 - ], - [ - 0.02439, - 0.03557, - 0.01084 - ], - [ - -0.00047, - 0.01757, - -0.00073 - ], - [ - 0.0183, - 0.01347, - 0.04315 - ], - [ - -0.00173, - 0.03373, - -0.00677 - ], - [ - 0.02397, - 0.01999, - 0.01807 - ], - [ - -0.00389, - 0.01687, - -0.00884 - ], - [ - -0.0011, - 0.01791, - 9e-05 - ] - ], - [ - [ - -0.08284, - 0.01524, - -0.00176 - ], - [ - 0.0806, - 0.01055, - 0.01714 - ], - [ - -0.00087, - 0.0032, - -0.02655 - ], - [ - 0.02081, - 0.01037, - 0.02289 - ], - [ - -0.05224, - 0.01847, - -0.02383 - ], - [ - 0.06023, - 0.0273, - -0.00431 - ], - [ - -0.02686, - 0.00487, - -0.00898 - ], - [ - -0.02613, - 0.02093, - -0.03629 - ], - [ - 0.02407, - 0.02705, - 0.00052 - ], - [ - -0.00454, - 0.02535, - -0.01046 - ] - ], - [] - ], - "neighbours": [ - { - "DEF-skirt.003.seg3": 2.6745725910706306, - "DEF-skirt.006": 0.26069539357345217, - "DEF-skirt.005.seg1": 1.0802518478592633, - "DEF-skirt.seg3": 1.3755075436915636, - "DEF-skirt.009": 0.7109617764468392, - "DEF-skirt.seg2": 0.4334825864653958, - "DEF-skirt.011": 0.20566438243868845, - "DEF-skirt.009.seg1": 0.3450042763204117, - "DEF-skirt.007.seg3": 0.23030644633612807, - "DEF-skirt.seg1": 0.16033306364800098, - "DEF-skirt.007.seg2": 0.016322135250931735, - "DEF-skirt.010": 0.010310972552469655, - "DEF-skirt.009.seg2": 0.05151539510096881 - }, - { - "DEF-skirt.006": 1.2229846542125553, - "DEF-skirt.005.seg2": 2.2839419227847424, - "DEF-skirt.003.seg3": 4.093617441427922, - "DEF-skirt.005": 1.0802518478592633, - "DEF-skirt.seg3": 3.920393819893497, - "DEF-skirt.011.seg1": 1.0448083213115291, - "DEF-skirt.011": 1.9069345583682153, - "DEF-skirt.011.seg2": 0.06153290296288072, - "DEF-skirt.009": 1.1164002439418375, - "DEF-skirt.010": 0.18882265189366965, - "DEF-skirt.009.seg1": 1.364221007427845, - "DEF-skirt.009.seg2": 1.0288586076015256, - "DEF-skirt.007.seg3": 0.7179370267663041, - "DEF-skirt.009.seg3": 0.16007123804476198, - "DEF-skirt.seg2": 0.007790076778398158 - }, - { - "DEF-skirt.006.seg3": 0.6104225058278434, - "DEF-skirt.005.seg3": 0.22486251911275246, - "DEF-skirt.006": 0.38039666877214595, - "DEF-skirt.005.seg1": 2.2839419227847424, - "DEF-skirt.003.seg3": 4.8612654356533715, - "DEF-skirt.seg3": 4.61498402848288, - "DEF-skirt.009": 0.3083252302655602, - "DEF-skirt.009.seg1": 1.2483108134831402, - "DEF-skirt.011.seg1": 3.0893325526066313, - "DEF-skirt.011": 1.117578855302317, - "DEF-skirt.011.seg2": 1.4663347373091402, - "DEF-skirt.L.seg3": 0.03759796665737425, - "DEF-skirt.010": 0.11437498180926176, - "DEF-skirt.009.seg2": 0.8744732184988231, - "DEF-skirt.007.seg3": 0.3911211444359659, - "DEF-skirt.011.seg3": 0.06419735157119555, - "DEF-skirt.009.seg3": 0.08906398621705636 - }, - { - "DEF-skirt.006.seg3": 0.24410953164354598, - "DEF-skirt.005.seg2": 0.22486251911275246, - "DEF-skirt.011.seg1": 0.031181913198806976, - "DEF-skirt.seg3": 0.040614199510271565, - "DEF-skirt.003.seg3": 0.04013802336446326, - "DEF-skirt.011": 0.006026445906482214 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.L.002" - ], - "tips": [ - [ - 0.03121, - 0.10187, - -0.02038 - ] - ], - "hulls": [ - [ - [ - 0.06915, - 0.02444, - -0.07296 - ], - [ - 2e-05, - 0.06885, - 1e-05 - ], - [ - 0.0171, - 0.03783, - 0.00801 - ], - [ - -0.00058, - 0.03804, - -0.00026 - ], - [ - 0.00719, - 0.06892, - 0.00329 - ], - [ - 0.00668, - 0.06797, - 0.00311 - ], - [ - 0.00041, - 0.03801, - 0.00021 - ], - [ - 0.06968, - 0.02446, - -0.07201 - ], - [ - 0.01613, - 0.03765, - 0.00756 - ], - [ - 0.00073, - 0.06811, - 0.00037 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.002.seg3": 0.5233835144511887, - "DEF-skirt.011.seg3": 0.2979373224340631, - "DEF-skirt.L.001.seg1": 1.4627848674895, - "DEF-skirt.L.001.seg3": 0.988772566592522, - "DEF-skirt.L.001.seg2": 1.6368903816027336, - "DEF-skirt.L.005.seg3": 0.26989297982735216, - "DEF-skirt.002.seg2": 0.08363487573375844, - "DEF-skirt.011.seg2": 0.1453999495240184, - "DEF-skirt.L.005.seg2": 0.16870674232060612, - "DEF-skirt.002.seg1": 0.09750582234609562, - "DEF-skirt.011.seg1": 0.02658579166615116, - "DEF-skirt.L.001": 0.08222018431886025, - "DEF-skirt.L.seg3": 0.10102962666415694, - "DEF-skirt.L.005": 0.13331009360929014, - "DEF-skirt.L.004": 0.808795880142356 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.L.001", - "DEF-skirt.L.001.seg1", - "DEF-skirt.L.001.seg2", - "DEF-skirt.L.001.seg3" - ], - "tips": [ - [ - -0.0, - 0.05706, - -0.0 - ], - [ - 0.0, - 0.05706, - 0.0 - ], - [ - -0.0, - 0.05706, - 0.0 - ], - [ - -0.01745, - 0.0433, - 0.00879 - ] - ], - "hulls": [ - [], - [ - [ - 0.0453, - 0.03397, - -0.03352 - ], - [ - -0.02599, - 0.04117, - 0.03719 - ], - [ - 0.01442, - 0.0089, - -0.03226 - ], - [ - 0.03648, - 0.00736, - -0.02149 - ], - [ - 0.02389, - 0.0376, - -0.04501 - ], - [ - -0.03656, - 0.03795, - 0.02107 - ], - [ - 0.02876, - 0.0087, - -0.02573 - ], - [ - 0.03718, - 0.03412, - -0.03711 - ], - [ - 0.04134, - 0.03589, - -0.03638 - ], - [ - 0.03182, - 0.0073, - -0.02352 - ] - ], - [ - [ - -0.02291, - 0.04811, - -0.00045 - ], - [ - -0.02001, - 0.01423, - 0.02762 - ], - [ - -0.03027, - 0.01093, - 0.01132 - ], - [ - -0.02969, - 0.01111, - 0.01223 - ], - [ - -0.02058, - 0.01404, - 0.02671 - ], - [ - -0.02238, - 0.0481, - 0.00051 - ] - ], - [ - [ - -0.0072, - 0.02243, - 0.00665 - ], - [ - -0.01133, - 0.02133, - 2e-05 - ], - [ - -0.01118, - 0.02051, - 0.00071 - ], - [ - -0.00772, - 0.0216, - 0.00617 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.L.002": 0.08222018431886025, - "DEF-skirt.011.seg2": 1.0570456406771491, - "DEF-skirt.002.seg2": 0.03671082080693666, - "DEF-skirt.002.seg1": 0.23053920218779567, - "DEF-skirt.011.seg1": 1.879150088933239, - "DEF-skirt.L.001.seg1": 1.7333563654776996, - "DEF-skirt.002": 0.17433376040275084, - "DEF-skirt.seg3": 0.4132729913690881, - "DEF-skirt.L.seg3": 1.7430155018692777, - "DEF-skirt.seg2": 0.07432020535983441, - "DEF-skirt.L.003": 0.23974937651991812, - "DEF-skirt.L.seg2": 0.10371684437270923 - }, - { - "DEF-skirt.L.002": 1.4627848674895, - "DEF-skirt.002.seg3": 0.45340715496411454, - "DEF-skirt.011.seg3": 0.43628207869348157, - "DEF-skirt.002.seg2": 0.07726076569748527, - "DEF-skirt.011.seg2": 0.8697016458385258, - "DEF-skirt.L.001.seg2": 1.5718474151046895, - "DEF-skirt.002.seg1": 0.07011653736356815, - "DEF-skirt.011.seg1": 0.060376459628942836, - "DEF-skirt.L.001": 1.7333563654776996, - "DEF-skirt.L.seg3": 0.19093190472623134, - "DEF-skirt.L.005": 0.36357547421065073, - "DEF-skirt.L.004": 0.8051463519527622 - }, - { - "DEF-skirt.L.002": 1.6368903816027336, - "DEF-skirt.L.001.seg3": 0.4428292313472455, - "DEF-skirt.L.005.seg3": 0.0646526762887677, - "DEF-skirt.L.001.seg1": 1.5718474151046895, - "DEF-skirt.L.005.seg2": 0.21270272007869018, - "DEF-skirt.L.seg3": 0.1699154302186351, - "DEF-skirt.L.005": 0.358614413021921, - "DEF-skirt.L.004": 0.6736723993011002, - "DEF-skirt.011.seg2": 0.02397434757873418, - "DEF-skirt.011.seg3": 0.020762523308016423 - }, - { - "DEF-skirt.L.002": 0.988772566592522, - "DEF-skirt.L.001.seg2": 0.4428292313472455, - "DEF-skirt.L.005.seg3": 0.47097637669824355, - "DEF-skirt.L.005.seg2": 0.12279700596246146 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.L.005", - "DEF-skirt.L.005.seg1", - "DEF-skirt.L.005.seg2", - "DEF-skirt.L.005.seg3" - ], - "tips": [ - [ - -0.0, - 0.01609, - -0.0 - ], - [ - -0.0, - 0.01609, - 0.0 - ], - [ - -0.0, - 0.01609, - -0.0 - ], - [ - -0.00328, - 0.03949, - 0.01665 - ] - ], - "hulls": [ - [ - [ - -0.00485, - -0.03063, - 0.00248 - ], - [ - -0.00254, - 0.0033, - 0.01753 - ], - [ - -0.00239, - 0.00046, - -0.0018 - ], - [ - -0.00253, - 0.00314, - 0.01645 - ], - [ - -0.0024, - 0.00062, - -0.00072 - ], - [ - -0.00485, - -0.03047, - 0.00356 - ] - ], - [], - [ - [ - -0.00236, - 0.00948, - -0.012 - ], - [ - 0.00046, - 0.00643, - 0.00707 - ], - [ - -0.00219, - 0.00911, - -0.01099 - ], - [ - 0.00031, - 0.00659, - 0.006 - ] - ], - [ - [ - -0.00127, - 0.01968, - 0.01112 - ], - [ - -0.00243, - 0.02102, - 0.00345 - ], - [ - -0.00226, - 0.02, - 0.00382 - ], - [ - -0.00131, - 0.01905, - 0.01023 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.L.004.seg3": 0.18071373217817754, - "DEF-skirt.L.003.seg3": 1.1878016405318974, - "DEF-skirt.L.004.seg2": 0.48988694772853547, - "DEF-skirt.R.005.seg3": 0.8433881844420577, - "DEF-skirt.L.seg3": 0.04318668795316161, - "DEF-skirt.L.001.seg2": 0.358614413021921, - "DEF-skirt.L.002": 0.13331009360929014, - "DEF-skirt.L.001.seg1": 0.36357547421065073, - "DEF-skirt.R.003.seg3": 0.1715765989223872, - "DEF-skirt.R.004": 0.030876031930140435, - "DEF-skirt.R.004.seg1": 0.020803836908132034, - "DEF-skirt.L.004": 0.22954852875263643, - "DEF-skirt.L.004.seg1": 1.117666014626619, - "DEF-skirt.R.005": 0.00681448318365796 - }, - { - "DEF-skirt.L.005.seg2": 0.2999459253805148, - "DEF-skirt.L.004.seg3": 0.286943106992394, - "DEF-skirt.L.003.seg3": 0.09606776911003378, - "DEF-skirt.R.005.seg3": 0.289521499895125 - }, - { - "DEF-skirt.L.004.seg3": 0.2371031746007346, - "DEF-skirt.L.003.seg3": 0.07865966284521508, - "DEF-skirt.L.005.seg1": 0.2999459253805148, - "DEF-skirt.R.005.seg3": 0.2372906590447128, - "DEF-skirt.L.002": 0.16870674232060612, - "DEF-skirt.L.001.seg3": 0.12279700596246146, - "DEF-skirt.L.001.seg2": 0.21270272007869018, - "DEF-skirt.L.005.seg3": 0.14366482829699256 - }, - { - "DEF-skirt.L.002": 0.26989297982735216, - "DEF-skirt.L.001.seg3": 0.47097637669824355, - "DEF-skirt.L.001.seg2": 0.0646526762887677, - "DEF-skirt.L.005.seg2": 0.14366482829699256 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.L.004", - "DEF-skirt.L.004.seg1", - "DEF-skirt.L.004.seg2", - "DEF-skirt.L.004.seg3" - ], - "tips": [ - [ - 0.0, - 0.0568, - -0.0 - ], - [ - 0.0, - 0.05681, - -0.0 - ], - [ - 0.0, - 0.05681, - 0.0 - ], - [ - -0.0338, - 0.05438, - 0.08144 - ] - ], - "hulls": [ - [ - [ - -0.15102, - 0.0078, - 0.08207 - ], - [ - 0.00824, - 0.03576, - 0.00178 - ], - [ - -0.05835, - 0.0553, - -0.00941 - ], - [ - -0.09639, - 0.03085, - 0.06009 - ], - [ - -0.12285, - -0.00656, - 0.08433 - ], - [ - -0.12841, - -0.01172, - 0.07969 - ], - [ - -0.1452, - 0.0128, - 0.08653 - ], - [ - -0.05826, - 0.05446, - -0.01011 - ], - [ - -0.15, - 0.00781, - 0.08246 - ], - [ - 0.00825, - 0.03587, - 0.00286 - ] - ], - [ - [ - 0.08615, - 0.03472, - -0.05979 - ], - [ - -0.05431, - 0.05202, - 0.02224 - ], - [ - 0.02518, - 0.0375, - 0.00055 - ], - [ - -0.02527, - 0.02548, - -0.02003 - ], - [ - 0.07674, - 0.02108, - 0.00321 - ], - [ - 0.04784, - 0.00565, - -0.01696 - ], - [ - 0.05354, - 0.035, - -0.0416 - ], - [ - -0.0043, - 0.04893, - 0.0021 - ], - [ - 0.05709, - 0.03934, - -0.00321 - ], - [ - 0.07479, - 0.0475, - -0.02989 - ] - ], - [ - [ - -0.0656, - 0.03387, - 0.0299 - ], - [ - 0.06661, - 0.00056, - -0.00535 - ], - [ - -0.00376, - 0.00758, - -0.01638 - ], - [ - -0.01302, - 0.03632, - 0.01466 - ], - [ - -0.03914, - 0.01854, - 0.04737 - ], - [ - -0.02557, - 0.00374, - 0.01734 - ], - [ - 0.03762, - 0.0123, - -0.00983 - ], - [ - -0.04518, - 0.02831, - 0.01086 - ], - [ - -0.03363, - 0.04644, - 0.02948 - ], - [ - 0.00975, - 0.02427, - -0.00104 - ] - ], - [] - ], - "neighbours": [ - { - "DEF-skirt.L.004.seg1": 4.0678154269769955, - "DEF-skirt.L.005": 0.22954852875263643, - "DEF-skirt.L.001.seg2": 0.6736723993011002, - "DEF-skirt.L.002": 0.808795880142356, - "DEF-skirt.L.001.seg1": 0.8051463519527622, - "DEF-skirt.L.seg3": 3.8285142497834164, - "DEF-skirt.L.003": 0.21646333191305667, - "DEF-skirt.L.seg2": 0.06935733543747569, - "DEF-skirt.L.003.seg3": 5.274030080809885, - "DEF-skirt.R.003.seg3": 0.845388225157766, - "DEF-skirt.L.003.seg2": 0.677365454643255, - "DEF-skirt.seg3": 0.8155590405608446, - "DEF-skirt": 0.19603096959511881, - "DEF-skirt.seg2": 0.14029811314042995, - "DEF-skirt.L.003.seg1": 0.3003293425343978, - "DEF-skirt.R.004": 0.13967539384350525, - "DEF-skirt.seg1": 0.20676807425859 - }, - { - "DEF-skirt.L.004": 4.0678154269769955, - "DEF-skirt.L.005": 1.117666014626619, - "DEF-skirt.R.003.seg3": 1.9171418898471628, - "DEF-skirt.L.004.seg2": 4.7309382362110455, - "DEF-skirt.L.003.seg3": 7.552187895272386, - "DEF-skirt.R.004": 1.5419089252454603, - "DEF-skirt.L.seg3": 0.9764221426855458, - "DEF-skirt.R.004.seg1": 0.07202823700664285, - "DEF-skirt.seg3": 0.029836616482513456 - }, - { - "DEF-skirt.L.004.seg3": 1.095682577471105, - "DEF-skirt.L.003.seg3": 5.510255386232652, - "DEF-skirt.L.005": 0.48988694772853547, - "DEF-skirt.R.005.seg3": 0.4449604793694335, - "DEF-skirt.R.004": 1.9079217533708204, - "DEF-skirt.R.004.seg1": 1.6832252773454213, - "DEF-skirt.R.003.seg3": 5.3545195307750895, - "DEF-skirt.L.004.seg1": 4.7309382362110455, - "DEF-skirt.R.004.seg2": 0.15842716545330904, - "DEF-skirt.R.005.seg1": 0.035164787307590095, - "DEF-skirt.R.005": 0.007233539959747903 - }, - { - "DEF-skirt.L.005.seg2": 0.2371031746007346, - "DEF-skirt.L.003.seg3": 0.5971418419198388, - "DEF-skirt.L.005.seg1": 0.286943106992394, - "DEF-skirt.R.005.seg3": 0.7169556622842608, - "DEF-skirt.L.005": 0.18071373217817754, - "DEF-skirt.L.004.seg2": 1.095682577471105, - "DEF-skirt.R.003.seg3": 1.2748603054718204, - "DEF-skirt.R.004.seg1": 0.5166879515818066, - "DEF-skirt.R.004.seg2": 0.25998774990698886 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.R.002" - ], - "tips": [ - [ - -0.00968, - 0.11247, - 0.00454 - ] - ], - "hulls": [ - [ - [ - -0.00719, - 0.06892, - 0.00329 - ], - [ - -0.0171, - 0.03783, - 0.00801 - ], - [ - 0.00058, - 0.03804, - -0.00026 - ], - [ - -2e-05, - 0.06885, - 1e-05 - ], - [ - -0.00073, - 0.06811, - 0.00037 - ], - [ - -0.00041, - 0.03801, - 0.00021 - ], - [ - -0.01612, - 0.03765, - 0.00756 - ], - [ - -0.00668, - 0.06797, - 0.00311 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.R.001.seg3": 0.4721447238142513, - "DEF-skirt.R.001.seg2": 1.3861041106828074, - "DEF-skirt.R.005": 0.5360177981787058, - "DEF-skirt.R.001.seg1": 1.026548805673111, - "DEF-skirt.010.seg3": 0.27244704748154724, - "DEF-skirt.010.seg2": 0.18258340776049164, - "DEF-skirt.009.seg3": 0.5169325843054446, - "DEF-skirt.010.seg1": 0.051648361776784, - "DEF-skirt.R.003.seg3": 0.012816917290188745, - "DEF-skirt.R.004.seg2": 0.018325292971089646, - "DEF-skirt.R.001": 0.06312256627611201, - "DEF-skirt.010": 0.029037682237823992 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.R.001", - "DEF-skirt.R.001.seg1", - "DEF-skirt.R.001.seg2", - "DEF-skirt.R.001.seg3" - ], - "tips": [ - [ - 0.0, - 0.0643, - -0.0 - ], - [ - -0.0, - 0.0643, - 0.0 - ], - [ - -0.0, - 0.0643, - 0.0 - ], - [ - -0.00053, - 0.0016, - 0.01591 - ] - ], - "hulls": [ - [ - [ - 0.0053, - 0.01949, - 0.11977 - ], - [ - 0.03099, - 0.06327, - 0.02728 - ], - [ - 0.00431, - 0.01991, - 0.11959 - ], - [ - 0.02996, - 0.0634, - 0.02764 - ] - ], - [ - [ - 0.0247, - 0.06188, - 0.01354 - ], - [ - -0.04416, - 0.02727, - -0.03656 - ], - [ - -0.01632, - 0.00129, - -0.03211 - ], - [ - 0.02158, - 0.03459, - 0.03826 - ], - [ - -0.02664, - 0.0297, - -0.04487 - ], - [ - 0.03255, - 0.03188, - 0.02232 - ], - [ - -0.0173, - 0.00116, - -0.03165 - ], - [ - 0.0253, - 0.06173, - 0.01264 - ], - [ - -0.04318, - 0.0274, - -0.03703 - ], - [ - 0.02218, - 0.03444, - 0.03736 - ] - ], - [ - [ - 0.0003, - 0.06429, - 0.00748 - ], - [ - 0.01463, - 0.00023, - 0.02877 - ], - [ - 0.01676, - 0.03439, - 0.00096 - ], - [ - 0.0061, - 0.03696, - 0.01711 - ], - [ - 0.00403, - 0.06421, - 0.00139 - ], - [ - 0.00669, - 0.0368, - 0.01621 - ], - [ - 0.01523, - 7e-05, - 0.02786 - ], - [ - 0.0039, - 0.06337, - 0.00209 - ], - [ - 0.01621, - 0.03435, - 0.0019 - ] - ], - [] - ], - "neighbours": [ - { - "DEF-skirt.010.seg1": 0.027378974892033114, - "DEF-skirt.007.seg3": 0.2677451548737494, - "DEF-skirt.010": 0.31555339612032274, - "DEF-skirt.009.seg2": 0.4018968056498796, - "DEF-skirt.009.seg3": 0.24376088738124668, - "DEF-skirt.R.001.seg1": 1.0607064606861076, - "DEF-skirt.009.seg1": 0.05524117514372051, - "DEF-skirt.R.002": 0.06312256627611201, - "DEF-skirt.R.seg2": 1.5263588944722746, - "DEF-skirt.R.003.seg3": 0.07035853311526641, - "DEF-skirt.R.003.seg2": 0.15603431496875952, - "DEF-skirt.R.seg3": 0.6250004873314858, - "DEF-skirt.R.seg1": 0.07198854155120338 - }, - { - "DEF-skirt.010.seg3": 0.2332891127301584, - "DEF-skirt.R.002": 1.026548805673111, - "DEF-skirt.010.seg2": 0.12614822889344357, - "DEF-skirt.009.seg3": 0.39026871228668636, - "DEF-skirt.R.001.seg2": 0.45374872514639186, - "DEF-skirt.010.seg1": 0.033309799028286846, - "DEF-skirt.R.005": 0.4914130450568921, - "DEF-skirt.R.003.seg3": 0.11280987179495439, - "DEF-skirt.R.001": 1.0607064606861076, - "DEF-skirt.R.004.seg2": 0.14391126107917818, - "DEF-skirt.010": 0.01830603357103766, - "DEF-skirt.R.seg3": 0.15912477238749645, - "DEF-skirt.R.004.seg1": 0.10724688171236707 - }, - { - "DEF-skirt.R.001.seg3": 0.48646409436291727, - "DEF-skirt.R.002": 1.3861041106828074, - "DEF-skirt.R.005": 0.7915718703374228, - "DEF-skirt.R.001.seg1": 0.45374872514639186, - "DEF-skirt.R.004.seg2": 0.04626688987928218 - }, - { - "DEF-skirt.R.002": 0.4721447238142513, - "DEF-skirt.R.001.seg2": 0.48646409436291727, - "DEF-skirt.R.005": 0.2593759751341018 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.R.005", - "DEF-skirt.R.005.seg1", - "DEF-skirt.R.005.seg2", - "DEF-skirt.R.005.seg3" - ], - "tips": [ - [ - 0.0, - 0.01912, - 0.0 - ], - [ - 0.0, - 0.01912, - -0.0 - ], - [ - 0.0, - 0.01912, - -0.0 - ], - [ - 0.08661, - 0.13329, - 0.07735 - ] - ], - "hulls": [ - [ - [ - 0.0341, - 0.01431, - 0.01316 - ], - [ - -0.02492, - -0.01564, - -0.00996 - ], - [ - -0.00759, - 0.00361, - 0.01594 - ], - [ - 0.00187, - -0.00238, - -7e-05 - ], - [ - 0.00134, - -0.00204, - 0.00082 - ], - [ - 0.03357, - 0.01464, - 0.01405 - ], - [ - -0.00707, - 0.00327, - 0.01504 - ], - [ - -0.02438, - -0.01598, - -0.01085 - ] - ], - [ - [ - -0.05443, - 0.00812, - -0.03221 - ], - [ - -0.02008, - 0.00129, - -0.03264 - ], - [ - -0.04983, - 0.01063, - -0.03808 - ], - [ - -0.02056, - 0.00086, - -0.03176 - ], - [ - -0.05337, - 0.00801, - -0.03246 - ], - [ - -0.04941, - 0.01002, - -0.03727 - ] - ], - [], - [ - [ - 0.08062, - 0.02593, - 0.06383 - ], - [ - 0.01748, - 0.09156, - 0.02803 - ], - [ - 0.04592, - 0.07269, - 0.02198 - ], - [ - 0.06121, - 0.04666, - 0.05168 - ], - [ - 0.03615, - 0.07203, - 0.03887 - ], - [ - 0.08118, - 0.02596, - 0.06289 - ], - [ - 0.03671, - 0.07206, - 0.03793 - ], - [ - 0.01837, - 0.09107, - 0.02763 - ], - [ - 0.06066, - 0.04663, - 0.05262 - ], - [ - 0.04551, - 0.07252, - 0.02298 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.R.005.seg1": 1.1971281659610125, - "DEF-skirt.R.001.seg3": 0.2593759751341018, - "DEF-skirt.R.002": 0.5360177981787058, - "DEF-skirt.R.001.seg2": 0.7915718703374228, - "DEF-skirt.R.001.seg1": 0.4914130450568921, - "DEF-skirt.R.003.seg3": 1.091596357605304, - "DEF-skirt.R.004.seg2": 9.754011944602853, - "DEF-skirt.R.seg3": 0.020397376975925585, - "DEF-skirt.R.004.seg1": 6.500489989848925, - "DEF-skirt.L.004.seg2": 0.007233539959747903, - "DEF-skirt.R.004": 0.0037763106041570904, - "DEF-skirt.R.004.seg3": 0.2621371448345676, - "DEF-skirt.L.003.seg3": 0.04733610412909939, - "DEF-skirt.L.005": 0.00681448318365796 - }, - { - "DEF-skirt.R.005": 1.1971281659610125, - "DEF-skirt.R.003.seg3": 0.15873896284533995, - "DEF-skirt.R.004.seg2": 0.05547518726174139, - "DEF-skirt.R.004.seg1": 0.09565318256508437, - "DEF-skirt.L.004.seg2": 0.035164787307590095, - "DEF-skirt.R.004": 0.018476782348616994, - "DEF-skirt.R.005.seg2": 0.05278311122623336 - }, - { - "DEF-skirt.R.005.seg1": 0.05278311122623336 - }, - { - "DEF-skirt.L.005.seg2": 0.2372906590447128, - "DEF-skirt.L.004.seg3": 0.7169556622842608, - "DEF-skirt.L.003.seg3": 0.5377347293486077, - "DEF-skirt.L.005.seg1": 0.289521499895125, - "DEF-skirt.L.005": 0.8433881844420577, - "DEF-skirt.L.004.seg2": 0.4449604793694335, - "DEF-skirt.R.003.seg3": 0.1178247570438451, - "DEF-skirt.R.004": 0.03221048762352208, - "DEF-skirt.R.004.seg1": 0.021702974422598054 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.R.004", - "DEF-skirt.R.004.seg1", - "DEF-skirt.R.004.seg2", - "DEF-skirt.R.004.seg3" - ], - "tips": [ - [ - 0.0, - 0.0491, - -0.0 - ], - [ - -0.0, - 0.0491, - 0.0 - ], - [ - -0.0, - 0.0491, - 0.0 - ], - [ - -0.0, - 0.0491, - 0.0 - ] - ], - "hulls": [ - [ - [ - -0.1167, - 0.01819, - 0.04148 - ], - [ - -0.02518, - 0.01477, - 0.0226 - ], - [ - -0.11245, - 0.01782, - 0.04839 - ], - [ - -0.11668, - 0.01753, - 0.04061 - ], - [ - -0.11302, - 0.01872, - 0.0486 - ], - [ - -0.02577, - 0.01467, - 0.02169 - ] - ], - [ - [ - -0.03391, - 0.04902, - 0.02667 - ], - [ - 0.02743, - 0.01628, - -0.00983 - ], - [ - -0.00994, - 0.03813, - -0.0066 - ], - [ - 0.01438, - 0.02973, - 0.00607 - ], - [ - -0.00761, - 0.03188, - 0.01466 - ], - [ - 0.01318, - 0.02813, - -0.01841 - ], - [ - 0.03012, - 0.01732, - 0.01001 - ], - [ - -0.00982, - 0.03887, - 0.00271 - ], - [ - 0.0137, - 0.03, - -0.00279 - ], - [ - -0.03511, - 0.04812, - 0.01805 - ] - ], - [ - [ - 0.04779, - 0.02322, - -0.02176 - ], - [ - -0.04971, - 0.00448, - 0.02323 - ], - [ - 0.00662, - 0.02205, - 0.01108 - ], - [ - -0.03678, - 0.02978, - -0.00441 - ], - [ - -0.02466, - 0.02702, - 0.02605 - ], - [ - 0.04436, - 0.00749, - 0.00644 - ], - [ - -0.0462, - 0.00208, - -0.00291 - ], - [ - 0.02212, - 0.00747, - -0.00466 - ], - [ - 0.00344, - 0.01565, - -0.0085 - ], - [ - 0.04078, - 0.00627, - -0.01328 - ] - ], - [] - ], - "neighbours": [ - { - "DEF-skirt.L.005": 0.030876031930140435, - "DEF-skirt.L.004.seg2": 1.9079217533708204, - "DEF-skirt.R.005.seg3": 0.03221048762352208, - "DEF-skirt.R.004.seg1": 1.0732347883554358, - "DEF-skirt.R.003.seg3": 4.464269337643888, - "DEF-skirt.L.003.seg3": 1.6662561376102858, - "DEF-skirt.L.004.seg1": 1.5419089252454603, - "DEF-skirt.R.seg2": 0.16426175880538585, - "DEF-skirt.L.004": 0.13967539384350525, - "DEF-skirt.R.003.seg2": 0.016111401937035363, - "DEF-skirt.R.seg3": 0.21310817027062778, - "DEF-skirt.R.005.seg1": 0.018476782348616994, - "DEF-skirt.R.005": 0.0037763106041570904 - }, - { - "DEF-skirt.R.004.seg2": 10.738716948372819, - "DEF-skirt.L.005": 0.020803836908132034, - "DEF-skirt.L.004.seg2": 1.6832252773454213, - "DEF-skirt.R.005.seg3": 0.021702974422598054, - "DEF-skirt.R.004": 1.0732347883554358, - "DEF-skirt.R.seg3": 0.19151314001136188, - "DEF-skirt.R.005": 6.500489989848925, - "DEF-skirt.R.001.seg1": 0.10724688171236707, - "DEF-skirt.R.003.seg3": 3.6078875543646496, - "DEF-skirt.L.003.seg3": 0.2699483450459348, - "DEF-skirt.L.004.seg3": 0.5166879515818066, - "DEF-skirt.R.005.seg1": 0.09565318256508437, - "DEF-skirt.L.004.seg1": 0.07202823700664285 - }, - { - "DEF-skirt.R.004.seg1": 10.738716948372819, - "DEF-skirt.R.005": 9.754011944602853, - "DEF-skirt.R.001.seg2": 0.04626688987928218, - "DEF-skirt.R.002": 0.018325292971089646, - "DEF-skirt.R.001.seg1": 0.14391126107917818, - "DEF-skirt.R.seg3": 0.04455171348530562, - "DEF-skirt.R.003.seg3": 3.272260373183556, - "DEF-skirt.L.004.seg3": 0.25998774990698886, - "DEF-skirt.L.004.seg2": 0.15842716545330904, - "DEF-skirt.R.005.seg1": 0.05547518726174139, - "DEF-skirt.R.004.seg3": 0.5314995138158436 - }, - { - "DEF-skirt.R.005": 0.2621371448345676, - "DEF-skirt.R.004.seg2": 0.5314995138158436 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.010", - "DEF-skirt.010.seg1", - "DEF-skirt.010.seg2", - "DEF-skirt.010.seg3" - ], - "tips": [ - [ - -0.0, - 0.02681, - -0.0 - ], - [ - 0.0, - 0.02681, - 0.0 - ], - [ - 0.0, - 0.02681, - -0.0 - ], - [ - -0.04852, - 0.01964, - 0.12108 - ] - ], - "hulls": [ - [ - [ - 0.08387, - 0.01976, - -0.01391 - ], - [ - -0.06183, - 0.02188, - -0.00739 - ], - [ - -0.03781, - -0.01788, - 0.00557 - ], - [ - 0.05871, - 0.01031, - -0.02099 - ], - [ - -0.05956, - 0.01908, - 0.01173 - ], - [ - 0.08341, - 0.01978, - -0.01491 - ], - [ - -0.05956, - 0.01926, - 0.01065 - ], - [ - 0.05915, - 0.0103, - -0.01999 - ], - [ - -0.06175, - 0.02156, - -0.00635 - ], - [ - -0.03783, - -0.01771, - 0.0045 - ] - ], - [ - [ - 0.02077, - 0.02144, - -0.07553 - ], - [ - 0.02768, - 0.02288, - -0.07198 - ], - [ - 0.02139, - 0.02087, - -0.07483 - ], - [ - 0.02711, - 0.02196, - -0.07183 - ] - ], - [], - [ - [ - -0.02616, - 0.01002, - 0.06035 - ], - [ - -0.01828, - 0.01042, - 0.06088 - ], - [ - -0.02535, - 0.00932, - 0.06056 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.010.seg1": 0.5487628936145512, - "DEF-skirt.009.seg2": 2.1035028646904017, - "DEF-skirt.009.seg3": 1.345437829411113, - "DEF-skirt.009.seg1": 1.1850910995461685, - "DEF-skirt.007.seg3": 0.23355445061974422, - "DEF-skirt.R.001": 0.31555339612032274, - "DEF-skirt.009": 0.23097140030928476, - "DEF-skirt.R.002": 0.029037682237823992, - "DEF-skirt.R.001.seg1": 0.01830603357103766, - "DEF-skirt.005.seg2": 0.11437498180926176, - "DEF-skirt.005.seg1": 0.18882265189366965, - "DEF-skirt.005": 0.010310972552469655 - }, - { - "DEF-skirt.010.seg2": 0.9157422602751464, - "DEF-skirt.009.seg3": 1.5453642087036952, - "DEF-skirt.010": 0.5487628936145512, - "DEF-skirt.009.seg2": 0.04938591538750098, - "DEF-skirt.R.001.seg1": 0.033309799028286846, - "DEF-skirt.R.002": 0.051648361776784, - "DEF-skirt.007.seg3": 0.015989096555482363, - "DEF-skirt.R.001": 0.027378974892033114 - }, - { - "DEF-skirt.R.001.seg1": 0.12614822889344357, - "DEF-skirt.010.seg3": 0.11231192164214132, - "DEF-skirt.R.002": 0.18258340776049164, - "DEF-skirt.009.seg3": 0.7490264894622274, - "DEF-skirt.010.seg1": 0.9157422602751464 - }, - { - "DEF-skirt.R.001.seg1": 0.2332891127301584, - "DEF-skirt.R.002": 0.27244704748154724, - "DEF-skirt.010.seg2": 0.11231192164214132, - "DEF-skirt.009.seg3": 0.7566836571082174 - } - ] - }, - { - "class": "skirt", - "root_parent": "DEF-spine", - "bones": [ - "DEF-skirt.009", - "DEF-skirt.009.seg1", - "DEF-skirt.009.seg2", - "DEF-skirt.009.seg3" - ], - "tips": [ - [ - 0.0, - 0.02818, - 0.0 - ], - [ - 0.0, - 0.02818, - 0.0 - ], - [ - 0.0, - 0.02818, - -0.0 - ], - [ - -0.03619, - 0.04068, - -0.00405 - ] - ], - "hulls": [ - [ - [ - -0.0934, - 0.01308, - -0.05741 - ], - [ - 0.09157, - 0.01046, - 6e-05 - ], - [ - -0.04856, - 0.02623, - 0.00682 - ], - [ - -0.07845, - 0.01877, - -0.02838 - ], - [ - -0.06331, - 0.02572, - -0.00594 - ], - [ - 0.08445, - 0.01441, - 0.00469 - ], - [ - 0.08911, - 0.01898, - -0.0017 - ], - [ - 0.09016, - 0.01656, - 0.00119 - ], - [ - 0.09129, - 0.01152, - 1e-05 - ], - [ - 0.08904, - 0.01844, - -0.00264 - ] - ], - [ - [ - 0.09362, - 0.00863, - -0.08179 - ], - [ - -0.05087, - 0.02478, - 0.04259 - ], - [ - 0.02526, - 0.01004, - -0.02974 - ], - [ - 0.02918, - 0.02075, - -0.07815 - ], - [ - 0.05947, - 0.00301, - -0.06983 - ], - [ - 0.0185, - 0.0247, - -0.00059 - ], - [ - 0.06892, - 0.0245, - -0.06689 - ], - [ - 0.02461, - 0.02325, - -0.04076 - ], - [ - 0.03153, - 0.00983, - -0.06629 - ], - [ - 0.05892, - 0.01463, - -0.081 - ] - ], - [ - [ - -0.07292, - 0.02213, - 0.09532 - ], - [ - 0.06001, - 0.00472, - -0.09372 - ], - [ - -0.01288, - 0.01989, - 0.00375 - ], - [ - 0.0358, - 0.01343, - -0.04136 - ], - [ - -0.04798, - 0.00526, - 0.04757 - ], - [ - 0.01666, - 0.01022, - -0.01058 - ], - [ - 0.02945, - 0.01043, - -0.0912 - ], - [ - -0.03094, - 0.01476, - 0.02644 - ], - [ - 0.02255, - 0.02536, - -0.06376 - ], - [ - 0.04226, - 0.00062, - -0.06501 - ] - ], - [ - [ - -0.0721, - 0.06955, - -0.00063 - ], - [ - 0.021, - 0.01513, - -0.02777 - ], - [ - -0.0281, - 0.01052, - 0.04322 - ], - [ - -0.01592, - 0.00469, - -0.00519 - ], - [ - -0.05269, - 0.04819, - 0.01577 - ], - [ - -0.04035, - 0.01305, - 0.00828 - ], - [ - -0.00951, - 0.02526, - -0.01258 - ], - [ - -0.0199, - 0.01686, - 0.01286 - ], - [ - -0.04182, - 0.01246, - 0.03474 - ], - [ - -0.01674, - 0.02392, - -0.01633 - ] - ] - ], - "neighbours": [ - { - "DEF-skirt.010": 0.23097140030928476, - "DEF-skirt.009.seg1": 2.8972802492875527, - "DEF-skirt.003.seg3": 1.1286794898007495, - "DEF-skirt.seg3": 0.7582600522037454, - "DEF-skirt.005.seg2": 0.3083252302655602, - "DEF-skirt.005.seg1": 1.1164002439418375, - "DEF-skirt.005": 0.7109617764468392, - "DEF-skirt.007.seg3": 9.43662584664051, - "DEF-skirt.seg2": 0.16869629844680606, - "DEF-skirt.seg1": 0.09372445010591768, - "DEF-skirt.R.seg3": 0.08669143553370695 - }, - { - "DEF-skirt.010": 1.1850910995461685, - "DEF-skirt.009.seg2": 2.723864090688208, - "DEF-skirt.009": 2.8972802492875527, - "DEF-skirt.R.001": 0.05524117514372051, - "DEF-skirt.007.seg3": 2.906074152306811, - "DEF-skirt.003.seg3": 0.8940016644600399, - "DEF-skirt.seg3": 0.9144382488613035, - "DEF-skirt.005.seg2": 1.2483108134831402, - "DEF-skirt.005.seg1": 1.364221007427845, - "DEF-skirt.005": 0.3450042763204117, - "DEF-skirt.R.seg3": 0.03686175164852512, - "DEF-skirt.seg2": 0.0714384640111776 - }, - { - "DEF-skirt.010.seg1": 0.04938591538750098, - "DEF-skirt.010": 2.1035028646904017, - "DEF-skirt.009.seg3": 1.8092874106016312, - "DEF-skirt.009.seg1": 2.723864090688208, - "DEF-skirt.007.seg3": 4.5295165581740635, - "DEF-skirt.R.001": 0.4018968056498796, - "DEF-skirt.005.seg2": 0.8744732184988231, - "DEF-skirt.005.seg1": 1.0288586076015256, - "DEF-skirt.003.seg3": 0.18218562382336984, - "DEF-skirt.R.seg3": 0.6798349904287918, - "DEF-skirt.seg3": 0.16737698817810986, - "DEF-skirt.seg2": 0.01027086487236381, - "DEF-skirt.005": 0.05151539510096881 - }, - { - "DEF-skirt.R.001.seg1": 0.39026871228668636, - "DEF-skirt.010.seg3": 0.7566836571082174, - "DEF-skirt.R.002": 0.5169325843054446, - "DEF-skirt.010.seg2": 0.7490264894622274, - "DEF-skirt.010.seg1": 1.5453642087036952, - "DEF-skirt.010": 1.345437829411113, - "DEF-skirt.009.seg2": 1.8092874106016312, - "DEF-skirt.007.seg3": 1.5188658246344597, - "DEF-skirt.R.001": 0.24376088738124668, - "DEF-skirt.R.seg3": 4.171412457713149, - "DEF-skirt.005.seg1": 0.16007123804476198, - "DEF-skirt.005.seg2": 0.08906398621705636 - } - ] - } - ], - "twist": [ - { - "bone": "DEF-upper_arm.L.001", - "parent": "DEF-upper_arm.L", - "child": "DEF-forearm.L" - }, - { - "bone": "DEF-forearm.L.001", - "parent": "DEF-forearm.L", - "child": "DEF-hand.L" - }, - { - "bone": "DEF-upper_arm.R.001", - "parent": "DEF-upper_arm.R", - "child": "DEF-forearm.R" - }, - { - "bone": "DEF-forearm.R.001", - "parent": "DEF-forearm.R", - "child": "DEF-hand.R" - }, - { - "bone": "DEF-thigh.L.001", - "parent": "DEF-thigh.L", - "child": "DEF-shin.L" - }, - { - "bone": "DEF-shin.L.001", - "parent": "DEF-shin.L", - "child": "DEF-foot.L" - }, - { - "bone": "DEF-thigh.R.001", - "parent": "DEF-thigh.R", - "child": "DEF-shin.R" - }, - { - "bone": "DEF-shin.R.001", - "parent": "DEF-shin.R", - "child": "DEF-foot.R" - } - ], - "colliders": [ - { - "bone": "DEF-spine", - "child": "DEF-spine.001", - "from": 0.0, - "radius_head": 0.1842, - "radius_tail": 0.1842, - "radius": 0.1842, - "lid": true - }, - { - "bone": "DEF-thigh.L", - "child": "DEF-shin.L", - "from": 0.1, - "radius_head": 0.1104, - "radius_tail": 0.063, - "radius": 0.063 - }, - { - "bone": "DEF-thigh.R", - "child": "DEF-shin.R", - "from": 0.1, - "radius_head": 0.1107, - "radius_tail": 0.062, - "radius": 0.062 - }, - { - "bone": "DEF-shin.L", - "child": "DEF-foot.L", - "from": 0.1, - "radius_head": 0.0953, - "radius_tail": 0.082, - "radius": 0.082 - }, - { - "bone": "DEF-shin.R", - "child": "DEF-foot.R", - "from": 0.1, - "radius_head": 0.0955, - "radius_tail": 0.0815, - "radius": 0.0815 - } - ], - "weights_authored": true, - "driven_bones": [ - "DEF-f_index.01.L", - "DEF-f_index.01.R", - "DEF-f_index.02.L", - "DEF-f_index.02.R", - "DEF-f_index.03.L", - "DEF-f_index.03.R", - "DEF-f_middle.01.L", - "DEF-f_middle.01.R", - "DEF-f_middle.02.L", - "DEF-f_middle.02.R", - "DEF-f_middle.03.L", - "DEF-f_middle.03.R", - "DEF-f_pinky.01.L", - "DEF-f_pinky.01.R", - "DEF-f_pinky.02.L", - "DEF-f_pinky.02.R", - "DEF-f_pinky.03.L", - "DEF-f_pinky.03.R", - "DEF-f_ring.01.L", - "DEF-f_ring.01.R", - "DEF-f_ring.02.L", - "DEF-f_ring.02.R", - "DEF-f_ring.03.L", - "DEF-f_ring.03.R", - "DEF-foot.L", - "DEF-foot.R", - "DEF-forearm.L", - "DEF-forearm.R", - "DEF-hand.L", - "DEF-hand.R", - "DEF-shin.L", - "DEF-shin.R", - "DEF-shoulder.L", - "DEF-shoulder.R", - "DEF-spine", - "DEF-spine.001", - "DEF-spine.002", - "DEF-spine.003", - "DEF-spine.004", - "DEF-spine.005", - "DEF-spine.006", - "DEF-thigh.L", - "DEF-thigh.R", - "DEF-thumb.01.L", - "DEF-thumb.01.R", - "DEF-thumb.02.L", - "DEF-thumb.02.R", - "DEF-thumb.03.L", - "DEF-thumb.03.R", - "DEF-toe.L", - "DEF-toe.R", - "DEF-upper_arm.L", - "DEF-upper_arm.R" - ], - "surfaces": [ - { - "mesh": "Object_10", - "surface": 0, - "material": "EyesInvL", - "class": "body", - "detail": "eyes_ink", - "why": "material names itself an eye card", - "verts": 140, - "textured": false, - "chain_share": {} - }, - { - "mesh": "Object_11", - "surface": 0, - "material": "EyesHL", - "class": "body", - "detail": "eyes_highlight", - "why": "material names itself an eye card", - "verts": 80, - "textured": false, - "chain_share": {} - }, - { - "mesh": "Object_12", - "surface": 0, - "material": "EyesFullBlack", - "class": "body", - "detail": "eyes_ink", - "why": "material names itself an eye card", - "verts": 269, - "textured": false, - "chain_share": {} - }, - { - "mesh": "Object_14", - "surface": 0, - "material": "ClothB", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 1658, - "textured": true, - "chain_share": { - "skirt": 0.698 - } - }, - { - "mesh": "Object_15", - "surface": 0, - "material": "FullBlack", - "class": "linework", - "detail": "outline_hull", - "why": "material is untextured ink or inside-out", - "verts": 2052, - "textured": false, - "chain_share": { - "skirt": 0.698 - } - }, - { - "mesh": "Object_17", - "surface": 0, - "material": "ClothA", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 706, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_18", - "surface": 0, - "material": "FullBlack", - "class": "linework", - "detail": "outline_hull", - "why": "material is untextured ink or inside-out", - "verts": 786, - "textured": false, - "chain_share": {} - }, - { - "mesh": "Object_20", - "surface": 0, - "material": "ClothA", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 124, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_22", - "surface": 0, - "material": "ClothA", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 106, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_24", - "surface": 0, - "material": "ClothCAndW", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 78, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_26", - "surface": 0, - "material": "ClothCAndW", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 312, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_27", - "surface": 0, - "material": "FullBlack", - "class": "linework", - "detail": "outline_hull", - "why": "material is untextured ink or inside-out", - "verts": 312, - "textured": false, - "chain_share": {} - }, - { - "mesh": "Object_29", - "surface": 0, - "material": "ClothCAndW", - "class": "cloth", - "detail": "garment", - "why": "material name says 'garment'", - "verts": 54, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_31", - "surface": 0, - "material": "Hair", - "class": "hair", - "detail": "hair", - "why": "material name says 'hair'", - "verts": 3479, - "textured": true, - "chain_share": { - "hair": 0.51, - "skirt": 0.002 - } - }, - { - "mesh": "Object_32", - "surface": 0, - "material": "material", - "class": "linework", - "detail": "outline_hull", - "why": "material is untextured ink or inside-out", - "verts": 3814, - "textured": false, - "chain_share": { - "hair": 0.474, - "skirt": 0.002 - } - }, - { - "mesh": "Object_7", - "surface": 0, - "material": "Face", - "class": "body", - "detail": "face", - "why": "material name says 'face'", - "verts": 1296, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_8", - "surface": 0, - "material": "Body", - "class": "body", - "detail": "skin", - "why": "material name says 'skin'", - "verts": 1980, - "textured": true, - "chain_share": {} - }, - { - "mesh": "Object_9", - "surface": 0, - "material": "Eyes", - "class": "body", - "detail": "eyes", - "why": "material name says 'eyes'", - "verts": 112, - "textured": true, - "chain_share": {} - } - ] } \ No newline at end of file diff --git a/assets/props/cars/Textures/colormap.png b/assets/props/cars/Textures/colormap.png new file mode 100644 index 0000000..5c4b185 Binary files /dev/null and b/assets/props/cars/Textures/colormap.png differ diff --git a/assets/shaders/anime_clouds.gdshaderinc b/assets/shaders/anime_clouds.gdshaderinc new file mode 100644 index 0000000..f25a922 --- /dev/null +++ b/assets/shaders/anime_clouds.gdshaderinc @@ -0,0 +1,96 @@ +#ifndef ANIME_CLOUDS_INCLUDED +#define ANIME_CLOUDS_INCLUDED + +// Organic 2D cloud field shared by the actual Sky shader and the fullscreen +// ink composite. Broad domain-warped masses define the cloud, a second rotated +// octave breaks up its contour into billows, and a quiet high-frequency layer +// erodes the edge. Unlike a grid of radial primitives, no stage exposes a +// circle or a square as the final silhouette. + +float cloud_hash(vec2 p) { + // Sine-free hash: substantially cheaper when the field is sampled several + // times to estimate the direction the sun meets the cloud. + vec3 p3 = fract(vec3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + + +float cloud_noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + // Quintic interpolation keeps both the value and its slope continuous at + // cell boundaries, so a density threshold cannot reveal the noise grid. + vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); + float a = cloud_hash(i); + float b = cloud_hash(i + vec2(1.0, 0.0)); + float c = cloud_hash(i + vec2(0.0, 1.0)); + float d = cloud_hash(i + vec2(1.0, 1.0)); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + + +float cloud_fbm(vec2 p) { + mat2 turn = mat2(vec2(0.80, 0.60), vec2(-0.60, 0.80)); + float value = cloud_noise(p) * 0.52; + p = turn * p * 2.03 + vec2(9.17, 4.31); + value += cloud_noise(p) * 0.27; + p = turn * p * 2.07 + vec2(3.73, 12.61); + value += cloud_noise(p) * 0.14; + p = turn * p * 2.01 + vec2(15.19, 7.07); + value += cloud_noise(p) * 0.07; + return value; +} + + +float cloud_detail(vec2 p) { + mat2 turn = mat2(vec2(0.86, 0.51), vec2(-0.51, 0.86)); + return cloud_noise(turn * p * 2.75 + vec2(23.7, 6.1)) * 0.68 + + cloud_noise(p * 5.20 + vec2(2.9, 31.4)) * 0.32; +} + + +float cloud_field(vec2 p) { + // Low-frequency vector warp makes the mass curl and fork without making + // the outline busy. The two components use unrelated offsets so this does + // not merely slide the source noise along its own contour. + vec2 domain = p * 0.34; + vec2 warp = vec2( + cloud_noise(domain + vec2(17.2, 4.8)), + cloud_noise(domain + vec2(3.1, 29.6))) - 0.5; + vec2 q = p + warp * 1.35; + + float mass = cloud_fbm(q * 0.52); + float billows = cloud_fbm( + mat2(vec2(0.74, 0.67), vec2(-0.67, 0.74)) * q * 1.18 + + vec2(11.3, 8.7)); + float erosion = cloud_detail(q); + float density = mass * 0.70 + billows * 0.24 + erosion * 0.06; + + // A third, much broader field separates the weather into distinct banks. + // Without this macro mask a perfectly organic contour can still connect + // across half the dome and read as one enormous sheet instead of clouds. + float islands = cloud_fbm(q * 0.24 + vec2(41.7, 18.3)); + return density - (1.0 - islands) * 0.16; +} + + +float cloud_sun_response(vec2 p, vec3 sun_direction) { + // Treat density as a painted height field. Sampling a short distance toward + // and away from the sun tells us which edge faces it. This gives the cloud + // a bright leading rim and a colored lee side, and naturally reverses when + // the DirectionalLight turns. + vec2 sun_xz = sun_direction.xz; + float horizontal = length(sun_xz); + vec2 axis = horizontal > 0.0001 + ? sun_xz / horizontal : vec2(0.7071, 0.7071); + float toward = cloud_field(p + axis * 0.075); + float away = cloud_field(p - axis * 0.075); + float directional_slope = (away - toward) / 0.15; + // A high sun lights more of the cloud face; a low sun makes the directional + // division stronger and leaves a broader colored underside. + float overhead = clamp(sun_direction.y, 0.0, 1.0); + return smoothstep(-0.22, 0.22, directional_slope + overhead * 0.08); +} + +#endif diff --git a/assets/shaders/anime_clouds.gdshaderinc.uid b/assets/shaders/anime_clouds.gdshaderinc.uid new file mode 100644 index 0000000..cc6aef4 --- /dev/null +++ b/assets/shaders/anime_clouds.gdshaderinc.uid @@ -0,0 +1 @@ +uid://dbh0unyul1h7d diff --git a/assets/shaders/anime_sky.gdshader b/assets/shaders/anime_sky.gdshader new file mode 100644 index 0000000..39e06dc --- /dev/null +++ b/assets/shaders/anime_sky.gdshader @@ -0,0 +1,90 @@ +shader_type sky; + +// Cel sky: a painted gradient with rounded, flat-toned anime clouds. +// +// ProceduralSkyMaterial gives a smooth two-colour ramp, and a smooth ramp over +// 40% of the frame is exactly what tools/levels.py was reporting as dead%: +// somewhere between a quarter and a third of every shot was a single flat +// shade with nothing in it. Sky is the cheapest real estate in the picture and +// it was empty. +// +// The clouds are drawn the way an anime background artist draws them — a +// silhouette filled with two or three flat tones and a crisp edge, NOT a soft +// volumetric puff. That is both the correct style and the cheap option. + +uniform vec3 top_color : source_color = vec3(0.18, 0.40, 0.85); +uniform vec3 horizon_color : source_color = vec3(0.72, 0.88, 0.98); +uniform vec3 ground_color : source_color = vec3(0.24, 0.22, 0.30); +uniform vec3 cloud_color : source_color = vec3(1.0, 0.99, 0.97); +uniform vec3 cloud_shadow : source_color = vec3(0.72, 0.75, 0.88); +// Raise to thin the clouds out. This is a threshold on the noise, so it is the +// knob for "how much sky is covered", not a density. +uniform float cloud_cover : hint_range(0.0, 1.0) = 0.52; +uniform float cloud_scale : hint_range(0.2, 8.0) = 1.6; +uniform float cloud_softness : hint_range(0.001, 0.2) = 0.035; +// Slow. This is a competitive shooter and anything the eye can track in the +// sky is a distraction; it only needs to move enough that the sky is not a +// photograph. +uniform float drift : hint_range(0.0, 0.02) = 0.0022; +uniform float horizon_falloff : hint_range(0.1, 2.0) = 0.45; +uniform float sun_halo : hint_range(0.0, 2.0) = 0.35; + + +#include "res://assets/shaders/anime_clouds.gdshaderinc" + + +void sky() { + float up = clamp(EYEDIR.y, 0.0, 1.0); + float down = clamp(-EYEDIR.y, 0.0, 1.0); + vec3 col = EYEDIR.y >= 0.0 + ? mix(horizon_color, top_color, pow(up, horizon_falloff)) + : mix(horizon_color, ground_color, pow(down, 0.62)); + + // Soft halo around the sun. The procedural sky drew a disk here; a broad + // glow reads better under the filmic shoulder and does not clip. + if (LIGHT0_ENABLED) { + float d = clamp(dot(EYEDIR, LIGHT0_DIRECTION), 0.0, 1.0); + col += LIGHT0_COLOR * pow(d, 24.0) * sun_halo; + } + + if (EYEDIR.y > 0.005) { + // Project the dome onto a plane overhead, so clouds compress toward the + // horizon the way real ones do instead of smearing across the zenith. + vec2 uv = EYEDIR.xz / (EYEDIR.y + 0.18) * cloud_scale + vec2(TIME * drift, 0.0); + float n = cloud_field(uv); + + // The edge remains clean enough for the anime style, but it follows a + // layered organic field instead of revealing primitive circles. + float edge = max(cloud_softness, fwidth(n) * 1.35); + float shape = smoothstep(cloud_cover - edge, cloud_cover + edge, n); + vec3 sun_direction = LIGHT0_ENABLED + ? normalize(LIGHT0_DIRECTION) + : normalize(vec3(0.45, 0.55, -0.70)); + vec3 sun_color = LIGHT0_ENABLED + ? LIGHT0_COLOR : vec3(1.0, 0.91, 0.72); + float facing = cloud_sun_response(uv, sun_direction); + float core = smoothstep( + cloud_cover + 0.045 - edge, + cloud_cover + 0.155 + edge, n); + float carving = smoothstep( + 0.38, 0.64, cloud_detail(uv + vec2(2.4, 9.7))); + // Three painted tones respond to the actual DirectionalLight: a colored + // lee side, a broad lit face, and a narrow sunward silver lining. + float lit_face = clamp( + 0.12 + core * 0.25 + facing * 0.58 + carving * 0.05, + 0.0, 1.0); + vec3 lit_color = mix(cloud_color, sun_color, 0.18); + vec3 cloud = mix(cloud_shadow, lit_color, lit_face); + float boundary = 1.0 - smoothstep( + cloud_cover + edge, cloud_cover + 0.105 + edge, n); + float silver_lining = boundary * facing * facing + * mix(0.10, 0.24, clamp(sun_direction.y, 0.0, 1.0)); + cloud += sun_color * silver_lining; + + // Fade out near the horizon, where the projection stretches a cloud + // into an infinitely long streak. + col = mix(col, cloud, shape * smoothstep(0.0, 0.22, EYEDIR.y)); + } + + COLOR = col; +} diff --git a/assets/shaders/anime_sky.gdshader.uid b/assets/shaders/anime_sky.gdshader.uid new file mode 100644 index 0000000..426b1ec --- /dev/null +++ b/assets/shaders/anime_sky.gdshader.uid @@ -0,0 +1 @@ +uid://cx2rt7638txa1 diff --git a/assets/shaders/ink_edge.gdshader b/assets/shaders/ink_edge.gdshader index 1103d8c..c41123a 100644 --- a/assets/shaders/ink_edge.gdshader +++ b/assets/shaders/ink_edge.gdshader @@ -1,57 +1,259 @@ shader_type spatial; render_mode unshaded, fog_disabled, depth_draw_never, depth_test_disabled, cull_disabled; -// Screen-space ink outline: detects depth and normal discontinuities and -// draws thin dark edges over the scene — the drawn-line pass that unifies -// every model (kit, hero, character) into the cel look. Attach to a -// fullscreen quad (POSITION override) so it runs for whatever camera renders. +// ── The 3D-to-2D pass ──────────────────────────────────────────────────────── +// +// One fullscreen quad doing three jobs, in this order: +// +// sky repaint the background, because this pass owns those pixels +// ink screen-space line work from the depth buffer +// grade the anime split-tone +// +// ## Lines come from a SECOND DIFFERENCE of linearised depth +// +// This is the change that matters, and it is worth spelling out because the +// previous version of this file did the usual thing and it was wrong: +// +// float ddiff = abs(dl - dr) + abs(du - dd); // <- first difference +// +// A first difference measures the SLOPE of the depth buffer, and slope is large +// wherever a surface is oblique to the camera — which the ground always is. So +// the road, the pavement, every roof and every long wall seen at an angle all +// carried ink, and the threshold had to be raised until it smeared rather than +// drew. Raising it then lost the real silhouettes at distance, which is why +// `depth_threshold` had to be scaled by `dc` to stop far geometry dissolving +// into noise: the whole formula was fighting itself. +// +// A second difference measures CURVATURE, and curvature is exactly zero across +// any planar surface no matter how oblique it is to the camera. So it fires on +// real silhouettes and real creases and on nothing else — a flat road grazing +// the camera produces literally no signal, and the threshold can therefore be +// set low enough to catch a distant railing. +// +// The two signs are kept apart and drawn differently, which is also from the +// reference: positive curvature (the near side of a silhouette, a convex ridge) +// inks strongly, negative curvature (an inside corner, where two surfaces meet +// away from the viewer) inks faintly. That is what an animator does — the +// outside line of a shape is heavy, the contact line inside it is light. uniform sampler2D screen_tex : hint_screen_texture, filter_linear; uniform sampler2D depth_tex : hint_depth_texture, filter_nearest; -uniform sampler2D normal_tex : hint_normal_roughness_texture, filter_nearest; -uniform vec4 ink_color : source_color = vec4(0.07, 0.06, 0.09, 1.0); -uniform float depth_threshold : hint_range(0.0, 4.0) = 1.1; -uniform float normal_threshold : hint_range(0.0, 1.0) = 0.55; -uniform float edge_strength : hint_range(0.0, 1.0) = 0.8; -uniform float max_distance : hint_range(10.0, 500.0) = 220.0; +group_uniforms ink; +uniform vec4 ink_color : source_color = vec4(0.224, 0.196, 0.310, 1.0); +// How far apart the taps sit, in pixels. This IS the line weight. The reference +// runs 1.35 at a 1.5-2x internal resolution; this project renders at 1x, and the +// brief asks for a slightly harder and thicker line than the reference, so it +// sits at 2.0 — about a two-pixel line at 1080p. +uniform float ink_thickness : hint_range(0.5, 6.0) = 2.3; +// Curvature at which a convex edge reaches full ink. Small, because the second +// difference is already normalised by distance and produces almost nothing on +// flat ground — this can be sensitive in a way the old first-difference test +// could never afford to be. +uniform float ink_sensitivity : hint_range(0.0005, 0.05) = 0.0034; +uniform float ink_concave : hint_range(0.002, 0.2) = 0.024; +uniform float ink_concave_amount : hint_range(0.0, 1.0) = 0.45; +// Hardness: where the ramp to full ink STARTS, as a fraction of the threshold. +// The reference effectively runs 0.32 — ink begins to appear at a third of the +// threshold curvature, which is a soft, tapering line. Pushing it toward 1.0 +// gives a harder, more decisive one, which is what the brief asks for. +// +// It cannot BE 1.0: that is a step(), and a step on a diagonal edge is a +// staircase that shimmers the moment anything moves. +// +// It also sets the NOISE FLOOR, and that is not a side effect to ignore. A +// grazing ground plane quantises in the depth buffer into a staircase, and a +// staircase has a large second difference at every step — at 0.32 those steps +// ink faintly and the road comes out finely hatched, which is precisely what a +// first render at a wider tap spacing showed. +uniform float ink_hardness : hint_range(0.1, 0.95) = 0.62; +// How much of the surface's own colour survives inside the line. Low but not +// zero: pure flat ink over every edge reads as pasted on, a whisper of the +// underlying hue reads as drawn. +uniform float ink_hue_bleed : hint_range(0.0, 0.6) = 0.14; +uniform float ink_strength : hint_range(0.0, 1.0) = 1.0; +// Let the far background dissolve into haze instead of turning into a mess of +// busy line work. +uniform float ink_fade_start : hint_range(5.0, 400.0) = 85.0; +uniform float ink_fade_end : hint_range(10.0, 900.0) = 220.0; +// Depth taps land on the sky. Clamping rather than special-casing them keeps +// the silhouette signal huge but bounded, so an object against the sky inks at +// full weight on BOTH sides of its edge — which is where the reference's early +// return leaves the line only half as thick as it should be. +uniform float ink_sky_depth : hint_range(50.0, 4000.0) = 900.0; +// ── The grade ──────────────────────────────────────────────────────────────── +// +// A split-tone: cool violet into the darks, warm paper-white into the lights, +// plus a lift that keeps shadow off the floor. Godot tonemaps and converts to +// sRGB after this pass, so unlike the reference this stays in linear and does +// NOT do its own sRGB conversion — doing both would gamma the frame twice. +group_uniforms grade; +uniform bool grade_enabled = true; +uniform vec3 grade_shadow_tint : source_color = vec3(0.678, 0.659, 0.816); +uniform vec3 grade_light_tint : source_color = vec3(1.0, 0.969, 0.910); +uniform float grade_saturation : hint_range(0.0, 2.0) = 1.12; +uniform float grade_lift : hint_range(0.0, 0.2) = 0.028; +uniform float grade_warmth : hint_range(0.0, 0.3) = 0.05; +uniform float grade_vignette : hint_range(0.0, 0.6) = 0.15; + +// The fullscreen pass owns the background pixels as well as the geometry ones. +// Godot's screen copy holds only the viewport clear colour at far depth, so +// sampling screen_tex there silently erases the real Environment sky — these +// repaint it with the same palette and law as anime_sky.gdshader, and +// LevelEnvironment keeps the two materials in sync. +group_uniforms sky; +uniform vec3 sky_top_color : source_color = vec3(0.18, 0.40, 0.85); +uniform vec3 sky_horizon_color : source_color = vec3(0.72, 0.88, 0.98); +uniform vec3 sky_ground_color : source_color = vec3(0.24, 0.22, 0.30); +uniform vec3 sky_cloud_color : source_color = vec3(1.0, 0.99, 0.97); +uniform vec3 sky_cloud_shadow : source_color = vec3(0.72, 0.75, 0.88); +uniform float sky_cloud_cover : hint_range(0.0, 1.0) = 0.46; +uniform float sky_cloud_scale : hint_range(0.2, 8.0) = 1.6; +uniform float sky_cloud_softness : hint_range(0.001, 0.2) = 0.035; +uniform float sky_drift : hint_range(0.0, 0.02) = 0.0022; +uniform float sky_horizon_falloff : hint_range(0.1, 2.0) = 0.45; +uniform vec3 sky_sun_direction = vec3(0.45, 0.34, -0.82); +uniform vec3 sky_sun_color : source_color = vec3(1.0, 0.91, 0.72); +uniform float sky_sun_halo : hint_range(0.0, 2.0) = 0.35; + + +#include "res://assets/shaders/anime_clouds.gdshaderinc" + + +vec3 painted_sky(vec3 dir) { + float up = clamp(dir.y, 0.0, 1.0); + float down = clamp(-dir.y, 0.0, 1.0); + vec3 col = dir.y >= 0.0 + ? mix(sky_horizon_color, sky_top_color, pow(up, sky_horizon_falloff)) + : mix(sky_horizon_color, sky_ground_color, pow(down, 0.62)); + float sun = pow(max(dot(dir, normalize(sky_sun_direction)), 0.0), 28.0); + col += sky_sun_color * sun * sky_sun_halo; + + if (dir.y > 0.005) { + vec2 uv = dir.xz / (dir.y + 0.18) * sky_cloud_scale + + vec2(TIME * sky_drift, 0.0); + float n = cloud_field(uv); + float edge = max(sky_cloud_softness, fwidth(n) * 1.35); + float shape = smoothstep( + sky_cloud_cover - edge, sky_cloud_cover + edge, n); + vec3 sun_direction = normalize(sky_sun_direction); + float facing = cloud_sun_response(uv, sun_direction); + float core = smoothstep( + sky_cloud_cover + 0.045 - edge, + sky_cloud_cover + 0.155 + edge, n); + float carving = smoothstep( + 0.38, 0.64, cloud_detail(uv + vec2(2.4, 9.7))); + float lit_face = clamp( + 0.12 + core * 0.25 + facing * 0.58 + carving * 0.05, + 0.0, 1.0); + vec3 lit_color = mix(sky_cloud_color, sky_sun_color, 0.18); + vec3 cloud = mix(sky_cloud_shadow, lit_color, lit_face); + float boundary = 1.0 - smoothstep( + sky_cloud_cover + edge, + sky_cloud_cover + 0.105 + edge, n); + float silver_lining = boundary * facing * facing + * mix(0.10, 0.24, clamp(sun_direction.y, 0.0, 1.0)); + cloud += sky_sun_color * silver_lining; + col = mix(col, cloud, shape * smoothstep(0.0, 0.22, dir.y)); + } + return col; +} + + +// Linear view depth, with the sky pinned to a finite value so the second +// difference across a silhouette is large but never infinite. float linear_depth(vec2 uv, mat4 inv_proj) { float d = texture(depth_tex, uv).r; + // Godot 4.3+ is reverse-Z: the far plane and the sky read exactly 0. + if (d <= 0.000001) { + return ink_sky_depth; + } vec4 ndc = vec4(uv * 2.0 - 1.0, d, 1.0); vec4 view = inv_proj * ndc; - return -view.z / view.w; + return min(-view.z / view.w, ink_sky_depth); } + +vec3 apply_grade(vec3 c, vec2 uv) { + if (!grade_enabled) { + return c; + } + float l = dot(c, vec3(0.2126, 0.7152, 0.0722)); + + // Split-tone: cool violet in the darks, warm paper white in the lights. + float k = smoothstep(0.02, 0.55, l); + c *= mix(grade_shadow_tint, grade_light_tint, k); + + // A gentle overall warmth, like late afternoon light through blossom. + c += vec3(grade_warmth, grade_warmth * 0.45, 0.0) * l * 0.35; + + // Keep the shadows readable — never crushed to black. + c += grade_lift * (1.0 - k); + + c = mix(vec3(l), c, grade_saturation); + + float r = length(uv - 0.5) * 1.42; + c *= 1.0 - grade_vignette * pow(clamp(r, 0.0, 1.0), 2.6); + + return max(c, vec3(0.0)); +} + + void vertex() { POSITION = vec4(VERTEX.xy, 1.0, 1.0); } + void fragment() { vec2 px = 1.0 / VIEWPORT_SIZE; vec2 uv = SCREEN_UV; + // Background first: where the depth buffer has no geometry the screen copy + // holds nothing but the clear colour, so those pixels are painted rather + // than sampled. They still go through the ink test below — an object read + // against the sky should be inked on the sky side of its edge too, which is + // what makes the silhouette the heaviest line in the frame. + float raw_depth = texture(depth_tex, uv).r; + vec3 col; + if (raw_depth <= 0.000001) { + vec4 far_view = INV_PROJECTION_MATRIX * vec4(uv * 2.0 - 1.0, 0.0, 1.0); + vec3 view_dir = normalize(far_view.xyz / far_view.w); + vec3 world_dir = normalize((INV_VIEW_MATRIX * vec4(view_dir, 0.0)).xyz); + col = painted_sky(world_dir); + } else { + col = texture(screen_tex, uv).rgb; + } + + vec2 t = px * ink_thickness; float dc = linear_depth(uv, INV_PROJECTION_MATRIX); - float dl = linear_depth(uv - vec2(px.x, 0.0), INV_PROJECTION_MATRIX); - float dr = linear_depth(uv + vec2(px.x, 0.0), INV_PROJECTION_MATRIX); - float du = linear_depth(uv - vec2(0.0, px.y), INV_PROJECTION_MATRIX); - float dd = linear_depth(uv + vec2(0.0, px.y), INV_PROJECTION_MATRIX); + float dl = linear_depth(uv - vec2(t.x, 0.0), INV_PROJECTION_MATRIX); + float dr = linear_depth(uv + vec2(t.x, 0.0), INV_PROJECTION_MATRIX); + float du = linear_depth(uv + vec2(0.0, t.y), INV_PROJECTION_MATRIX); + float dd = linear_depth(uv - vec2(0.0, t.y), INV_PROJECTION_MATRIX); - // Depth edge: neighbor difference scaled by distance so far geometry - // doesn't dissolve into noise. - float ddiff = abs(dl - dr) + abs(du - dd); - float depth_edge = step(depth_threshold * (0.5 + dc * 0.08), ddiff); + // Second difference of linear depth, normalised by distance so that a given + // real-world crease inks with the same weight near and far. + float sx = (dl + dr - 2.0 * dc) / dc; + float sy = (du + dd - 2.0 * dc) / dc; - // Normal edge: crease lines on same-depth corners. - vec3 nc = texture(normal_tex, uv).xyz * 2.0 - 1.0; - vec3 nl = texture(normal_tex, uv - vec2(px.x, 0.0)).xyz * 2.0 - 1.0; - vec3 nu = texture(normal_tex, uv - vec2(0.0, px.y)).xyz * 2.0 - 1.0; - float ndiff = length(nc - nl) + length(nc - nu); - float normal_edge = step(normal_threshold, ndiff) * step(ddiff, depth_threshold); + float convex = max(0.0, sx) + max(0.0, sy); + float concave = max(0.0, -sx) + max(0.0, -sy); - float fade = 1.0 - smoothstep(max_distance * 0.6, max_distance, dc); - float edge = max(depth_edge, normal_edge * 0.6) * edge_strength * fade; + float edge = smoothstep( + ink_sensitivity * ink_hardness, ink_sensitivity, convex); + edge = max(edge, smoothstep( + ink_concave, ink_concave * 3.4, concave) * ink_concave_amount); - vec3 scene = texture(screen_tex, uv).rgb; - ALBEDO = mix(scene, ink_color.rgb, edge); + // Fade on the nearest real surface involved in the edge, not on dc: a sky + // pixel beside a nearby roof would otherwise be measured at 900 m and its + // half of the silhouette would fade out while the roof's half did not. + float near_d = min(dc, min(min(dl, dr), min(du, dd))); + edge *= 1.0 - smoothstep(ink_fade_start, ink_fade_end, near_d); + edge *= ink_strength; + + vec3 line = mix(ink_color.rgb, col * 0.42, ink_hue_bleed); + col = mix(col, line, clamp(edge, 0.0, 1.0)); + + ALBEDO = apply_grade(col, uv); } diff --git a/assets/shaders/sakura_cel.gdshader b/assets/shaders/sakura_cel.gdshader new file mode 100644 index 0000000..db0d327 --- /dev/null +++ b/assets/shaders/sakura_cel.gdshader @@ -0,0 +1,258 @@ +shader_type spatial; + +// ── The cel law ────────────────────────────────────────────────────────────── +// +// Ported from the Sakura Crossing renderer (three.js MeshToonMaterial + a +// patched toon BRDF). Two ideas, and the second one is the one that matters: +// +// 1. Direct light is QUANTISED against a hand-authored ramp — a tiny +// nearest-filtered texture sampled at `dot(N,L) * 0.5 + 0.5`. So a surface +// is never lit by a falloff, only ever by one of 2-5 flat values. +// +// 2. The darker bands are HUE-SHIFTED toward a cool violet rather than being +// a darker version of the base colour: +// +// irradiance = band * mix(shadow_tint, vec3(1.0), band) +// +// The band drives its own tint, so full light is untinted, the mid band is +// slightly cooled and the darkest band is almost entirely shadow-hue. That +// hue shift in shadow is most of what separates "anime cel" from +// "low-poly 3D" — a wall whose shade is just 40% of its lit colour reads +// as an unlit polygon; a wall whose shade swings violet reads as painted. +// +// Godot's light() is called per light and adds into DIFFUSE_LIGHT, and its +// Lambert term already carries the 1/PI that three.js puts in BRDF_Lambert, so +// the two pipelines agree once the ramp value is substituted for `dot(N,L)`. +// +// This is a SEPARATE shader from toon.gdshader on purpose. That one carries the +// weapon-viewmodel colour shaping and the character band calibration, both +// tuned against imported textures that are already painted with cel shading. +// This one is for FLAT-COLOURED WORLD GEOMETRY, which is what the reference is +// built out of end to end, and it would be wrong to make either compromise for +// the other. + +// ── Ramps ──────────────────────────────────────────────────────────────────── +// +// The reference's stop tables, kept as raw 0-255 values so they can be checked +// against the source line by line. These are LINEAR values, not sRGB: three.js +// gives a DataTexture no colour space, so the stops are used as authored, and +// the same has to be true here or every band lands too bright. +// +// 2/3/4/5 the standard ramps. 3 is the default and does most of the world. +// 6 "soft" — high key, 2 bands, for pale masses (blossom, cloud, +// 7 "soft3" — high key, 3 bands plaster) that must stay light even +// on the shadow side. Without these a +// cherry canopy goes grey the moment +// it turns away from the sun, which +// is the one thing a blossom tree may +// never do. +const float RAMP_2[5] = float[](96.0, 255.0, 255.0, 255.0, 255.0); +const float RAMP_3[5] = float[](92.0, 178.0, 255.0, 255.0, 255.0); +const float RAMP_4[5] = float[](80.0, 142.0, 202.0, 255.0, 255.0); +const float RAMP_5[5] = float[](74.0, 124.0, 172.0, 214.0, 255.0); +const float RAMP_SOFT[5] = float[](180.0, 255.0, 255.0, 255.0, 255.0); +const float RAMP_SOFT3[5] = float[](172.0, 214.0, 255.0, 255.0, 255.0); + +// Which ramp, and how many of its stops are live. +// 0 = 2 band 1 = 3 band (default) 2 = 4 band 3 = 5 band +// 4 = soft 2 band (high key) 5 = soft 3 band (high key) +uniform int ramp_id : hint_range(0, 5) = 1; + +uniform vec4 albedo_color : source_color = vec4(1.0); +uniform sampler2D albedo_texture : source_color, filter_linear_mipmap, repeat_enable; +uniform bool has_texture = false; +uniform bool use_vertex_color = false; + +// The cool violet the shadow bands swing toward. 0x6c5f8c is the reference's +// default; individual materials shift it (warmer for timber and blossom, bluer +// for metal and water) the same way the reference's `cel({tint})` does. +uniform vec4 shadow_tint : source_color = vec4(0.424, 0.373, 0.549, 1.0); + +// Faceted shading. The reference sets `flatShading: true` on essentially +// everything, and it is not a shortcut — a quantised ramp over SMOOTH normals +// puts a curved band boundary across a curved surface, which reads as an +// airbrushed gradient no matter how few steps it has. Faceting forces every +// band edge onto a polygon edge, which is where a cel painter would put it. +uniform bool flat_shading = true; + +// ── Cast shadow ────────────────────────────────────────────────────────────── +// +// The reference folds shadow attenuation into the light colour, so an occluded +// pixel there goes to zero direct light and is carried entirely by ambient. +// That works when the whole world is one authored scene; in a shooter, where +// players have to be read against the geometry they are standing on, a cast +// shadow that reaches black swallows them. +// +// So occlusion drives the surface to the ramp's DARKEST STOP — fully tinted, +// scaled by cast_shadow_depth — rather than to nothing. The result is still a +// hard, dark, clearly separate shape (which is what an animator inks), but it +// keeps hue, so a character standing in the shade of a shopfront stays legible. +uniform float cast_shadow_depth : hint_range(0.0, 1.0) = 0.72; +uniform float cast_shadow_softness : hint_range(0.01, 1.0) = 0.30; + +// Band-edge anti-aliasing floor, in NdotL. Nearest-filtered quantisation is +// a hard step, and a hard step on a slowly-curving surface crawls and +// stair-steps as the camera moves — this is shading aliasing, so MSAA cannot +// touch it. See light() for how the screen-space term works. +uniform float band_softness : hint_range(0.0, 0.5) = 0.012; + +// Rim and specular are OFF by default and should mostly stay that way: the +// reference has neither, and on large level surfaces a toon rim reads as a +// giant soft blob smeared across a wall. +uniform float rim_strength : hint_range(0.0, 2.0) = 0.0; +uniform float rim_width : hint_range(0.0, 1.0) = 0.28; + +// ── Surface law ────────────────────────────────────────────────────────────── +// +// Carried over from toon.gdshader, because the problem it solves is real here +// too: a flat cel colour is the style, but a twelve-metre wall holding exactly +// one value is not stylised, it is empty. Both default to off — the reference +// answers this with geometry (every fascia, sill and downpipe is modelled), and +// where this map does the same it does not need them. +group_uniforms detail; +uniform float seam_scale = 0.0; // metres per panel; 0 = off +uniform float seam_strength : hint_range(0.0, 1.0) = 0.24; +uniform float seam_width : hint_range(0.001, 0.2) = 0.010; +uniform float grade_height = 0.0; // metres to fade over; 0 = off +uniform float grade_strength : hint_range(0.0, 1.0) = 0.18; +uniform float grade_floor = 0.0; // world Y the grade starts from + +varying vec3 world_pos; +varying vec3 world_normal; +varying float normal_slope; + + +float ramp_stop(int i) { + // GLSL ES 3.0 will not index a const array with a non-constant expression on + // every driver, so the ramp is selected by branch and the stop by a small + // unrolled pick. Both indices are uniform-or-loop-constant, so this costs + // nothing measurable and works everywhere. + float s[5] = RAMP_3; + if (ramp_id == 0) { s = RAMP_2; } + else if (ramp_id == 2) { s = RAMP_4; } + else if (ramp_id == 3) { s = RAMP_5; } + else if (ramp_id == 4) { s = RAMP_SOFT; } + else if (ramp_id == 5) { s = RAMP_SOFT3; } + float v = s[0]; + if (i == 1) { v = s[1]; } + else if (i == 2) { v = s[2]; } + else if (i == 3) { v = s[3]; } + else if (i >= 4) { v = s[4]; } + return v / 255.0; +} + + +int ramp_bands() { + if (ramp_id == 0 || ramp_id == 4) { return 2; } + if (ramp_id == 2) { return 4; } + if (ramp_id == 3) { return 5; } + return 3; +} + + +float seam_lines(vec2 uv) { + vec2 g = abs(fract(uv) - 0.5); + vec2 fw = fwidth(uv) + 0.0001; + vec2 line = smoothstep(0.5 - seam_width - fw, 0.5 - seam_width + fw, g); + return max(line.x, line.y); +} + + +float panel_seam(vec3 p, vec3 n) { + vec3 w = pow(abs(n), vec3(4.0)); + w /= (w.x + w.y + w.z); + float s = seam_lines(p.zy / seam_scale) * w.x + + seam_lines(p.xz / seam_scale) * w.y + + seam_lines(p.xy / seam_scale) * w.z; + float cell_px = length(fwidth(p)) / seam_scale; + return s * (1.0 - smoothstep(0.25, 0.5, cell_px)); +} + + +void vertex() { + world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; + world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz); +} + + +void fragment() { + vec3 base = albedo_color.rgb; + if (has_texture) { + base *= texture(albedo_texture, UV).rgb; + } + if (use_vertex_color) { + base *= COLOR.rgb; + } + + if (seam_scale > 0.0) { + base *= 1.0 - panel_seam(world_pos, world_normal) * seam_strength; + } + if (grade_height > 0.0) { + float h = clamp((world_pos.y - grade_floor) / grade_height, 0.0, 1.0); + base *= mix(1.0 - grade_strength, 1.0, h); + } + + if (flat_shading) { + // VERTEX is view-space position here, so its screen derivatives span the + // triangle's own plane and their cross product is the true face normal. + // Forced to face the viewer, because the winding of that cross product + // flips with the handedness of the projection and a back-to-front normal + // lights the facet from behind. + vec3 fn = normalize(cross(dFdx(VERTEX), dFdy(VERTEX))); + NORMAL = fn * sign(dot(fn, VIEW)); + } + + ALBEDO = base; + ROUGHNESS = 1.0; + SPECULAR = 0.0; + + // Screen-space rate of turn of the normal, measured here because + // derivatives are a fragment-stage operation — taking fwidth() inside + // light(), which runs once per light, is undefined on some drivers. + // Faceted geometry has a slope of zero across a facet, so this only ever + // widens the band edge where the surface is genuinely curving. + normal_slope = length(fwidth(NORMAL)); + + float rim = 1.0 - clamp(dot(normalize(VIEW), NORMAL), 0.0, 1.0); + rim = smoothstep(1.0 - rim_width, 1.0, rim); + EMISSION = base * rim * rim_strength; +} + + +void light() { + float ndotl = dot(NORMAL, LIGHT); + + // Quantise. `t` is the reference's ramp lookup coordinate exactly — the + // texture is sampled at dot(N,L) * 0.5 + 0.5 with NearestFilter, so band + // boundaries fall at evenly spaced values of that coordinate and nowhere + // else. With the 3-stop ramp that puts them at dot(N,L) = ±1/3. + int bands = ramp_bands(); + float fb = clamp(ndotl * 0.5 + 0.5, 0.0, 0.999999) * float(bands); + int idx = int(floor(fb)); + float frac_b = fb - float(idx); + + float hi = ramp_stop(idx); + float lo = ramp_stop(max(idx - 1, 0)); + + // Anti-aliasing budget. band_softness is in NdotL, which says nothing about + // how many PIXELS a transition covers; normal_slope does. Widening the step + // to at least the on-screen rate of turn keeps every band edge about a pixel + // wide, and leaves edges that are already wider exactly as authored. The + // d(fb)/d(ndotl) factor of bands*0.5 converts the NdotL width into the + // band-index units `frac_b` is measured in. + float w = max(band_softness, normal_slope * 0.9) * float(bands) * 0.5; + float band = mix(lo, hi, smoothstep(0.0, max(w * 2.0, 0.0001), frac_b)); + + // The hue shift. Band drives its own tint: full light is untinted, the + // darkest band is very nearly pure shadow hue. + vec3 lit = band * mix(shadow_tint.rgb, vec3(1.0), band); + + // Cast shadow: down to the darkest stop, fully tinted, never to black. + float floor_band = ramp_stop(0); + vec3 occluded = floor_band * mix(shadow_tint.rgb, vec3(1.0), floor_band) + * cast_shadow_depth; + float occ = smoothstep(0.0, cast_shadow_softness, ATTENUATION); + vec3 shade = mix(occluded, lit, occ); + + DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade; +} diff --git a/assets/shaders/sakura_cel.gdshader.uid b/assets/shaders/sakura_cel.gdshader.uid new file mode 100644 index 0000000..9b1e6d1 --- /dev/null +++ b/assets/shaders/sakura_cel.gdshader.uid @@ -0,0 +1 @@ +uid://dk5nb354iw044 diff --git a/assets/shaders/toon.gdshader b/assets/shaders/toon.gdshader index b28c482..0058737 100644 --- a/assets/shaders/toon.gdshader +++ b/assets/shaders/toon.gdshader @@ -9,6 +9,7 @@ shader_type spatial; uniform vec4 albedo_color : source_color = vec4(1.0); uniform sampler2D albedo_texture : source_color, filter_linear_mipmap, repeat_enable; uniform bool has_texture = true; +uniform bool use_vertex_color = false; uniform bool use_triplanar = false; uniform float triplanar_tile = 2.0; // world units per texture tile @@ -21,15 +22,84 @@ uniform float mid_band_edge : hint_range(-1.0, 1.0) = 0.55; // second, brighter // reads as a glossy stripe sweeping across the hair. uniform float mid_tone : hint_range(0.0, 1.0) = 0.82; uniform vec4 shadow_color : source_color = vec4(0.62, 0.65, 0.78, 1.0); // cool shadow tint +// How far a CAST shadow drops below the form-shadow tint. Cel art draws the +// two separately: the terminator (a surface turning away from the sun) is a +// tone step, a cast shadow (something standing in the way) is a darker, harder +// shape. See light() for why collapsing them made benches float. +uniform float cast_shadow_depth : hint_range(0.0, 1.0) = 0.62; +// Width of the cast-shadow edge, in ATTENUATION. Small keeps it crisp like +// inked artwork; not zero, or the shadow map's own stair-steps show. +uniform float cast_shadow_softness : hint_range(0.01, 1.0) = 0.35; // Matte-anime defaults: zero specular (any stepped glint reads as shine // sweeping across hair/cloth when the camera moves), whisper of rim. uniform float rim_strength : hint_range(0.0, 2.0) = 0.05; uniform float rim_width : hint_range(0.0, 1.0) = 0.28; uniform float specular_strength : hint_range(0.0, 1.0) = 0.0; uniform float specular_shininess : hint_range(1.0, 128.0) = 64.0; +// Viewmodel/readability controls. Both default to zero so world materials are +// byte-for-byte unchanged. The first keeps near-black authored metal from +// collapsing below display range; the second is a controlled painted fill, +// useful in a transparent viewmodel viewport that has no environment ambient. +uniform float albedo_floor : hint_range(0.0, 0.5) = 0.0; +uniform float ambient_fill : hint_range(0.0, 1.0) = 0.0; +// Viewmodel color shaping. Defaults preserve every world/character material; +// weapon profiles opt in so dark receiver metal, pale trim and painted parts +// do not all converge on the same middle grey under a uniform fill. +uniform float albedo_contrast : hint_range(0.5, 2.0) = 1.0; +uniform float albedo_saturation : hint_range(0.0, 2.0) = 1.0; +uniform float albedo_gain : hint_range(0.25, 1.5) = 1.0; +uniform float viewmodel_light_strength : hint_range(0.0, 2.0) = 0.0; +uniform vec3 viewmodel_key_direction = vec3(-0.42, 0.58, 0.70); +uniform vec3 viewmodel_key_color : source_color = vec3(1.0, 0.91, 0.80); +uniform vec3 viewmodel_fill_color : source_color = vec3(0.72, 0.80, 1.0); +// Viewmodel ink controls. The old deterministic light was an additive +// key/fill wash: it made every normal visible, but also lifted receiver +// recesses and polymer undersides toward the same middle grey. These controls +// make that painted light a four-tone cel ramp and add a thin curvature ink +// pass. They default off/neutral so world and character materials are +// unchanged. +uniform float viewmodel_shadow_tone : hint_range(0.0, 1.0) = 0.18; +uniform float viewmodel_mid_tone : hint_range(0.0, 1.0) = 0.52; +uniform float viewmodel_crease_strength : hint_range(0.0, 1.0) = 0.0; +uniform float viewmodel_crease_threshold : hint_range(0.001, 1.0) = 0.12; + +// ── Surface law ────────────────────────────────────────────────────────────── +// Flat cel colour is the style, but a wall that is ONE value from end to end is +// not stylised, it is empty — tools/levels.py measured 0.28 mean adjacent-pixel +// difference across the probe, i.e. essentially nothing but the silhouettes. +// These give a surface its own quiet structure without touching the flat-shaded +// look: everything here is authored into ALBEDO, so the cel bands still land on +// top of it exactly as before. +// +// All default to OFF (scale 0). A material opts in through LevelMaterials' +// surface laws, so props, vehicles and foliage keep plain flat colour and only +// architecture and ground get the treatment. +group_uniforms detail; +// Panel seams: a world-space grid of thin darker lines, triplanar so it needs +// no UVs and stays the same physical size on every surface it crosses. This is +// the single biggest change to how "built" a surface reads. +uniform float seam_scale = 0.0; // metres per panel; 0 = off +uniform float seam_strength : hint_range(0.0, 1.0) = 0.30; +uniform float seam_width : hint_range(0.001, 0.2) = 0.012; // fraction of a panel +// Vertical grade: surfaces darken toward the ground. A painter would put this +// in by hand — it is ambient occlusion as artwork rather than as a screen +// effect, it costs nothing, and it works at any distance, which SSAO does not. +uniform float grade_height = 0.0; // metres to fade over; 0 = off +uniform float grade_strength : hint_range(0.0, 1.0) = 0.16; +uniform float grade_floor = 0.0; // world Y the grade starts from +// Halftone: screen-tone dots inside the shadow band. The signature manga move +// and ZZZ does use it, but sparingly — off unless a material asks. +uniform float halftone_scale = 0.0; // screen px per dot; 0 = off +uniform float halftone_strength : hint_range(0.0, 1.0) = 0.35; varying vec3 world_pos; varying vec3 world_normal; +// How fast the normal is turning across one pixel, measured in fragment() and +// read in light(). This is the band-edge anti-aliasing budget — see light(). +varying float normal_slope; +// Screen-tone dot mask, measured in fragment() where FRAGCOORD lives and spent +// in light() where the shadow band is known. +varying float halftone_dot; void vertex() { world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; @@ -49,6 +119,33 @@ vec3 sample_triplanar(vec3 p, vec3 n) { return cx * w.x + cy * w.y + cz * w.z; } +// One axis-plane's worth of panel grid. Returns 1 on a seam line, 0 on the +// panel face, anti-aliased by the on-screen size of a cell so the lines neither +// shimmer at distance nor turn into fat bars up close — a seam drawn with +// step() looks fine in a screenshot and crawls badly the moment anything moves. +float seam_lines(vec2 uv) { + vec2 g = abs(fract(uv) - 0.5); + vec2 fw = fwidth(uv) + 0.0001; + vec2 line = smoothstep(0.5 - seam_width - fw, 0.5 - seam_width + fw, g); + return max(line.x, line.y); +} + + +// Triplanar panel seams, weighted the same way the albedo triplanar is so the +// lines agree with the texture across a corner instead of crossfading against +// it. Far enough away that a cell is sub-pixel the whole thing fades out, +// otherwise the grid turns into moiré. +float panel_seam(vec3 p, vec3 n) { + vec3 w = pow(abs(n), vec3(4.0)); + w /= (w.x + w.y + w.z); + float s = seam_lines(p.zy / seam_scale) * w.x + + seam_lines(p.xz / seam_scale) * w.y + + seam_lines(p.xy / seam_scale) * w.z; + float cell_px = length(fwidth(p)) / seam_scale; + return s * (1.0 - smoothstep(0.25, 0.5, cell_px)); +} + + void fragment() { vec3 base = albedo_color.rgb; if (has_texture) { @@ -58,30 +155,137 @@ void fragment() { base *= texture(albedo_texture, UV).rgb; } } + if (use_vertex_color) { + base *= COLOR.rgb; + } + base *= albedo_gain; + float source_luma = dot(base, vec3(0.2126, 0.7152, 0.0722)); + base = mix(vec3(source_luma), base, albedo_saturation); + // Non-clipping power curve. Imported gun greys arrive as very small LINEAR + // values (an sRGB 0.17 receiver is roughly 0.025 here); a conventional 18% + // linear contrast pivot clips all of those finishes to exactly zero. The + // reciprocal curve expands their separation without moving black or white. + base = pow(max(base, vec3(0.0)), + vec3(1.0 / max(albedo_contrast, 0.001))); + if (albedo_floor > 0.0) { + float luma = dot(base, vec3(0.2126, 0.7152, 0.0722)); + base = min(base * max(1.0, albedo_floor / max(luma, 0.001)), vec3(1.0)); + } + + // Surface law, into the albedo so the cel bands still light it normally. + if (seam_scale > 0.0) { + base *= 1.0 - panel_seam(world_pos, world_normal) * seam_strength; + } + if (grade_height > 0.0) { + float h = clamp((world_pos.y - grade_floor) / grade_height, 0.0, 1.0); + base *= mix(1.0 - grade_strength, 1.0, h); + } + + // A one-pixel technical-ink accent where the surface normal changes + // quickly. Unlike an inverted hull this follows receiver cut-lines and + // bevels without exploding on the weapon FBXs' split hard normals. + float local_normal_slope = length(fwidth(normalize(NORMAL))); + float vm_crease = smoothstep( + viewmodel_crease_threshold, + viewmodel_crease_threshold * 2.6, + local_normal_slope); + base *= 1.0 - vm_crease * viewmodel_crease_strength; + + // Screen-tone dots, rotated 45° the way a real screentone sheet is. Spent + // in light(), which is where we know whether this pixel is in shadow. + halftone_dot = 0.0; + if (halftone_scale > 0.0) { + vec2 hp = FRAGCOORD.xy / halftone_scale; + vec2 r = vec2(hp.x - hp.y, hp.x + hp.y) * 0.70710678; + halftone_dot = smoothstep(0.36, 0.30, length(fract(r) - 0.5)); + } + ALBEDO = base; ROUGHNESS = 1.0; SPECULAR = 0.0; + // Screen-space rate of change of the normal, for the band-edge width in + // light(). It has to be measured here: derivatives are a fragment-stage + // operation, and light() runs once per light — taking fwidth() inside that + // loop is undefined behaviour on some drivers. + normal_slope = length(fwidth(NORMAL)); + // Rim: brighten grazing angles for that inked-silhouette pop. float rim = 1.0 - clamp(dot(normalize(VIEW), NORMAL), 0.0, 1.0); rim = smoothstep(1.0 - rim_width, 1.0, rim); - EMISSION = base * rim * rim_strength; + // Deterministic view-space key/fill for the separate transparent viewmodel + // viewport. Directional lights parented to its camera are renderer- and + // world-sharing-sensitive; this painted light map follows the gun normals + // directly and therefore survives every level environment. + float vm_ndotl = dot(normalize(NORMAL), normalize(viewmodel_key_direction)); + float vm_leave_shadow = smoothstep(-0.24, -0.04, vm_ndotl); + float vm_enter_light = smoothstep(0.18, 0.34, vm_ndotl); + float vm_highlight = smoothstep(0.64, 0.78, vm_ndotl); + float vm_tone = mix(viewmodel_shadow_tone, viewmodel_mid_tone, + vm_leave_shadow); + vm_tone = mix(vm_tone, 0.82, vm_enter_light); + vm_tone = mix(vm_tone, 1.0, vm_highlight); + vec3 vm_light = mix(viewmodel_fill_color, viewmodel_key_color, + vm_enter_light); + EMISSION = base * ( + vec3(rim * rim_strength + ambient_fill) + + vm_light * vm_tone * viewmodel_light_strength); } void light() { float ndotl = dot(NORMAL, LIGHT); - // Shadowing folds into the band test so shadow edges band too. - float lit = ndotl * ATTENUATION; - float band = smoothstep(band_edge - band_softness, band_edge + band_softness, lit); - float mid = smoothstep(mid_band_edge - band_softness, mid_band_edge + band_softness, lit); + + // Band-edge width, with a screen-space floor. + // + // band_softness is measured in NdotL, which says nothing about how many + // PIXELS the transition actually covers. On a slowly-curving surface it can + // span a third of a wall; on a tight curve, or simply at distance, it + // collapses below one pixel and the terminator crawls and stair-steps as + // the camera moves. MSAA cannot touch this — it is shading aliasing, not + // geometry. normal_slope is how much the normal turns across one pixel, so + // widening the step to at least that much keeps every band edge about a + // pixel wide while leaving edges that are already wider exactly as authored. + // Flat surfaces have a slope of zero and so keep their crisp break. + float w = max(band_softness, normal_slope * 0.9); + + // FORM shadow — the surface turning away from the light. This is the cel + // terminator, and its numbers are what the whole look was calibrated on, so + // it still lands exactly where it did. + float band = smoothstep(band_edge - w, band_edge + w, ndotl); + float mid = smoothstep(mid_band_edge - w, mid_band_edge + w, ndotl); // 3 tones: shadow tint -> base band (mid_tone) -> full light. float tone = mix(mid_tone, 1.0, mid); vec3 shade = mix(shadow_color.rgb, vec3(tone), band); + + // CAST shadow — something is standing between this pixel and the light. + // + // These used to be one thing: NdotL was multiplied by ATTENUATION and the + // PRODUCT was banded, so a cast shadow could never be darker than the + // terminator's own tint, about 0.65 of full light. Under the old ambient + // flood (Neon Alley used to run 1.35) that came out as a dip of roughly a + // seventh — which is why a bench standing in direct sun threw nothing onto + // the pavement, and why tools/levels.py measured a 1st percentile of 119 on + // a daylit frame: nothing in it was dark, so nothing rested on anything. + // + // Cel art draws the two separately, and so does this now: the terminator + // keeps its calibrated tone step, and occlusion drops below it to + // cast_shadow_depth as a harder, deeper shape. + float occ = smoothstep(0.0, cast_shadow_softness, ATTENUATION); + shade = mix(shadow_color.rgb * cast_shadow_depth, shade, occ); + + // Screen tone, in the shadow only — that is where a manga artist puts it, + // and confining it there means it reads as shading rather than as a filter + // laid over the picture. Scaled by how deep in shadow the pixel is, so the + // dots fade in across the terminator instead of switching on at it. + float in_shadow = (1.0 - band) * (1.0 - occ * 0.4); + shade *= 1.0 - halftone_dot * in_shadow * halftone_strength; + DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade; - // Stepped specular dot for glossy toon highlights. + // Stepped specular dot for glossy toon highlights. Gated by occ so a + // highlight cannot sit inside a shadow. vec3 h = normalize(VIEW + LIGHT); float spec = pow(clamp(dot(NORMAL, h), 0.0, 1.0), specular_shininess); - spec = smoothstep(0.5 - band_softness, 0.5 + band_softness, spec); - SPECULAR_LIGHT += LIGHT_COLOR * spec * specular_strength * band; + spec = smoothstep(0.5 - w, 0.5 + w, spec); + SPECULAR_LIGHT += LIGHT_COLOR * spec * specular_strength * band * occ; } diff --git a/assets/sounds/SOURCES.md b/assets/sounds/SOURCES.md index c0d3b31..8414872 100644 --- a/assets/sounds/SOURCES.md +++ b/assets/sounds/SOURCES.md @@ -13,9 +13,35 @@ from these CC0 packs, no attribution required: - Kenney Interface Sounds — https://kenney.nl/assets/interface-sounds (CC0) - Kenney Music Jingles — https://kenney.nl/assets/music-jingles (CC0) -Exceptions (still procedural from `generate_sounds.py`): `wind.wav`, -`wallrun.wav`, `slide.wav` — seamless noise loops that the Kenney one-shots -can't replace. +Runtime masters are built by `tools/build_audio_assets.py`. It retains the +recorded transient, layers a filtered body and short reflection tail, controls +peaks, and constructs seamless movement beds from pinned source recordings. -Menu music: `music_menu.wav` — "Menu Music" by mrpoly, CC0, +The industrial movement/UI pass uses: + +- "Wind Loop" — field recording by Jonathan Shaw (InspectorJ), loop edit by + AntumDeluge, CC-BY 3.0, + https://opengameart.org/content/wind-loop +- "Scrapes" by AntumDeluge — recorded cinder-block friction, CC0, + https://opengameart.org/content/scrapes +- "Mechanical Sounds" by Brian MacIntosh — clunks, ratchets and impacts, CC0, + https://opengameart.org/content/mechanical-sounds +- "Steam boiler sound loop" by bart — metallic generator source, CC0, + https://opengameart.org/content/steam-boiler-sound-loop +- "Mech Stomp / Step Sound" by hc/qubodup — low mechanical body, CC0, + https://opengameart.org/content/mech-stomp-step-sound +- "Mechanical Explosion" by Spring Spring — industrial debris layer, CC0, + https://opengameart.org/content/mechanical-explosion +- "Air whoosh" by pyranostudios — movement/thrust air layer, CC0, + https://opengameart.org/content/air-whoosh +- "Rocket Engine" by theMinesAreShakin — low jump/dash thruster body, CC0, + https://opengameart.org/content/rocket-engine + +Every remote input is SHA-256 pinned in the builder. No continuous runtime cue +is made from generated random noise. + +Current menu music: "PYNCHON" by James Gargette/cinameng, CC0, +https://opengameart.org/content/pynchon + +Legacy menu music retained as `music_menu.ogg`: "Menu Music" by mrpoly, CC0, https://opengameart.org/content/menu-music diff --git a/assets/sounds/explosion_deep_00.wav b/assets/sounds/explosion_deep_00.wav new file mode 100644 index 0000000..3b704a8 Binary files /dev/null and b/assets/sounds/explosion_deep_00.wav differ diff --git a/assets/sounds/explosion_deep_01.wav b/assets/sounds/explosion_deep_01.wav new file mode 100644 index 0000000..47b2619 Binary files /dev/null and b/assets/sounds/explosion_deep_01.wav differ diff --git a/assets/sounds/explosion_deep_02.wav b/assets/sounds/explosion_deep_02.wav new file mode 100644 index 0000000..c771315 Binary files /dev/null and b/assets/sounds/explosion_deep_02.wav differ diff --git a/assets/sounds/explosion_deep_03.wav b/assets/sounds/explosion_deep_03.wav new file mode 100644 index 0000000..dc4381c Binary files /dev/null and b/assets/sounds/explosion_deep_03.wav differ diff --git a/assets/sounds/explosion_deep_04.wav b/assets/sounds/explosion_deep_04.wav new file mode 100644 index 0000000..456de18 Binary files /dev/null and b/assets/sounds/explosion_deep_04.wav differ diff --git a/assets/sounds/footstep_concrete_00.wav b/assets/sounds/footstep_concrete_00.wav new file mode 100644 index 0000000..6c6281c Binary files /dev/null and b/assets/sounds/footstep_concrete_00.wav differ diff --git a/assets/sounds/footstep_concrete_01.wav b/assets/sounds/footstep_concrete_01.wav new file mode 100644 index 0000000..568da25 Binary files /dev/null and b/assets/sounds/footstep_concrete_01.wav differ diff --git a/assets/sounds/footstep_concrete_02.wav b/assets/sounds/footstep_concrete_02.wav new file mode 100644 index 0000000..3c0d686 Binary files /dev/null and b/assets/sounds/footstep_concrete_02.wav differ diff --git a/assets/sounds/footstep_concrete_03.wav b/assets/sounds/footstep_concrete_03.wav new file mode 100644 index 0000000..dee7900 Binary files /dev/null and b/assets/sounds/footstep_concrete_03.wav differ diff --git a/assets/sounds/footstep_glass_00.wav b/assets/sounds/footstep_glass_00.wav new file mode 100644 index 0000000..f37e8c2 Binary files /dev/null and b/assets/sounds/footstep_glass_00.wav differ diff --git a/assets/sounds/footstep_glass_01.wav b/assets/sounds/footstep_glass_01.wav new file mode 100644 index 0000000..1169200 Binary files /dev/null and b/assets/sounds/footstep_glass_01.wav differ diff --git a/assets/sounds/footstep_glass_02.wav b/assets/sounds/footstep_glass_02.wav new file mode 100644 index 0000000..6068ecb Binary files /dev/null and b/assets/sounds/footstep_glass_02.wav differ diff --git a/assets/sounds/footstep_glass_03.wav b/assets/sounds/footstep_glass_03.wav new file mode 100644 index 0000000..352e3b9 Binary files /dev/null and b/assets/sounds/footstep_glass_03.wav differ diff --git a/assets/sounds/footstep_metal_00.wav b/assets/sounds/footstep_metal_00.wav new file mode 100644 index 0000000..df40044 Binary files /dev/null and b/assets/sounds/footstep_metal_00.wav differ diff --git a/assets/sounds/footstep_metal_01.wav b/assets/sounds/footstep_metal_01.wav new file mode 100644 index 0000000..b7685df Binary files /dev/null and b/assets/sounds/footstep_metal_01.wav differ diff --git a/assets/sounds/footstep_metal_02.wav b/assets/sounds/footstep_metal_02.wav new file mode 100644 index 0000000..31d9ee7 Binary files /dev/null and b/assets/sounds/footstep_metal_02.wav differ diff --git a/assets/sounds/footstep_metal_03.wav b/assets/sounds/footstep_metal_03.wav new file mode 100644 index 0000000..98eeb5c Binary files /dev/null and b/assets/sounds/footstep_metal_03.wav differ diff --git a/assets/sounds/footstep_wood_00.wav b/assets/sounds/footstep_wood_00.wav new file mode 100644 index 0000000..2d511a6 Binary files /dev/null and b/assets/sounds/footstep_wood_00.wav differ diff --git a/assets/sounds/footstep_wood_01.wav b/assets/sounds/footstep_wood_01.wav new file mode 100644 index 0000000..9892a1f Binary files /dev/null and b/assets/sounds/footstep_wood_01.wav differ diff --git a/assets/sounds/footstep_wood_02.wav b/assets/sounds/footstep_wood_02.wav new file mode 100644 index 0000000..5c10c7d Binary files /dev/null and b/assets/sounds/footstep_wood_02.wav differ diff --git a/assets/sounds/footstep_wood_03.wav b/assets/sounds/footstep_wood_03.wav new file mode 100644 index 0000000..e602cf5 Binary files /dev/null and b/assets/sounds/footstep_wood_03.wav differ diff --git a/assets/sounds/impact_bullet_00.wav b/assets/sounds/impact_bullet_00.wav new file mode 100644 index 0000000..1be7e84 Binary files /dev/null and b/assets/sounds/impact_bullet_00.wav differ diff --git a/assets/sounds/impact_bullet_01.wav b/assets/sounds/impact_bullet_01.wav new file mode 100644 index 0000000..7ea9bf9 Binary files /dev/null and b/assets/sounds/impact_bullet_01.wav differ diff --git a/assets/sounds/impact_bullet_02.wav b/assets/sounds/impact_bullet_02.wav new file mode 100644 index 0000000..5359c06 Binary files /dev/null and b/assets/sounds/impact_bullet_02.wav differ diff --git a/assets/sounds/movement_dash_00.wav b/assets/sounds/movement_dash_00.wav new file mode 100644 index 0000000..9cba608 Binary files /dev/null and b/assets/sounds/movement_dash_00.wav differ diff --git a/assets/sounds/movement_double_jump_00.wav b/assets/sounds/movement_double_jump_00.wav new file mode 100644 index 0000000..5c2f409 Binary files /dev/null and b/assets/sounds/movement_double_jump_00.wav differ diff --git a/assets/sounds/movement_grapple_latch_00.wav b/assets/sounds/movement_grapple_latch_00.wav new file mode 100644 index 0000000..172b430 Binary files /dev/null and b/assets/sounds/movement_grapple_latch_00.wav differ diff --git a/assets/sounds/movement_grapple_launch_00.wav b/assets/sounds/movement_grapple_launch_00.wav new file mode 100644 index 0000000..d1794ca Binary files /dev/null and b/assets/sounds/movement_grapple_launch_00.wav differ diff --git a/assets/sounds/movement_grapple_reel.wav b/assets/sounds/movement_grapple_reel.wav new file mode 100644 index 0000000..a659643 Binary files /dev/null and b/assets/sounds/movement_grapple_reel.wav differ diff --git a/assets/sounds/movement_grapple_reel_base.wav b/assets/sounds/movement_grapple_reel_base.wav new file mode 100644 index 0000000..ad492a1 Binary files /dev/null and b/assets/sounds/movement_grapple_reel_base.wav differ diff --git a/assets/sounds/movement_jump_00.wav b/assets/sounds/movement_jump_00.wav new file mode 100644 index 0000000..41dd5a9 Binary files /dev/null and b/assets/sounds/movement_jump_00.wav differ diff --git a/assets/sounds/movement_land_00.wav b/assets/sounds/movement_land_00.wav new file mode 100644 index 0000000..0858201 Binary files /dev/null and b/assets/sounds/movement_land_00.wav differ diff --git a/assets/sounds/movement_slide_concrete.wav b/assets/sounds/movement_slide_concrete.wav new file mode 100644 index 0000000..2831cf0 Binary files /dev/null and b/assets/sounds/movement_slide_concrete.wav differ diff --git a/assets/sounds/movement_slide_glass.wav b/assets/sounds/movement_slide_glass.wav new file mode 100644 index 0000000..0639b4e Binary files /dev/null and b/assets/sounds/movement_slide_glass.wav differ diff --git a/assets/sounds/movement_slide_metal.wav b/assets/sounds/movement_slide_metal.wav new file mode 100644 index 0000000..4c88cba Binary files /dev/null and b/assets/sounds/movement_slide_metal.wav differ diff --git a/assets/sounds/movement_slide_wood.wav b/assets/sounds/movement_slide_wood.wav new file mode 100644 index 0000000..c62e955 Binary files /dev/null and b/assets/sounds/movement_slide_wood.wav differ diff --git a/assets/sounds/movement_vault_00.wav b/assets/sounds/movement_vault_00.wav new file mode 100644 index 0000000..01a5595 Binary files /dev/null and b/assets/sounds/movement_vault_00.wav differ diff --git a/assets/sounds/movement_wallrun.wav b/assets/sounds/movement_wallrun.wav new file mode 100644 index 0000000..048d993 Binary files /dev/null and b/assets/sounds/movement_wallrun.wav differ diff --git a/assets/sounds/movement_wind.wav b/assets/sounds/movement_wind.wav new file mode 100644 index 0000000..2b6ab56 Binary files /dev/null and b/assets/sounds/movement_wind.wav differ diff --git a/assets/sounds/music_menu_premium.ogg b/assets/sounds/music_menu_premium.ogg new file mode 100644 index 0000000..7b41dfb Binary files /dev/null and b/assets/sounds/music_menu_premium.ogg differ diff --git a/assets/sounds/projectile_rocket_loop.wav b/assets/sounds/projectile_rocket_loop.wav new file mode 100644 index 0000000..a106673 Binary files /dev/null and b/assets/sounds/projectile_rocket_loop.wav differ diff --git a/assets/sounds/projectile_swarm_loop.wav b/assets/sounds/projectile_swarm_loop.wav new file mode 100644 index 0000000..9da82d0 Binary files /dev/null and b/assets/sounds/projectile_swarm_loop.wav differ diff --git a/assets/sounds/ui_click_premium_00.wav b/assets/sounds/ui_click_premium_00.wav new file mode 100644 index 0000000..4e27198 Binary files /dev/null and b/assets/sounds/ui_click_premium_00.wav differ diff --git a/assets/sounds/ui_click_premium_01.wav b/assets/sounds/ui_click_premium_01.wav new file mode 100644 index 0000000..2b9db3f Binary files /dev/null and b/assets/sounds/ui_click_premium_01.wav differ diff --git a/assets/sounds/ui_confirm_premium_00.wav b/assets/sounds/ui_confirm_premium_00.wav new file mode 100644 index 0000000..285a219 Binary files /dev/null and b/assets/sounds/ui_confirm_premium_00.wav differ diff --git a/assets/sounds/ui_equip_premium_00.wav b/assets/sounds/ui_equip_premium_00.wav new file mode 100644 index 0000000..1ab5a6a Binary files /dev/null and b/assets/sounds/ui_equip_premium_00.wav differ diff --git a/assets/sounds/ui_error_premium_00.wav b/assets/sounds/ui_error_premium_00.wav new file mode 100644 index 0000000..26af91a Binary files /dev/null and b/assets/sounds/ui_error_premium_00.wav differ diff --git a/assets/sounds/ui_hover_premium_00.wav b/assets/sounds/ui_hover_premium_00.wav new file mode 100644 index 0000000..0fdb75d Binary files /dev/null and b/assets/sounds/ui_hover_premium_00.wav differ diff --git a/assets/sounds/ui_hover_premium_01.wav b/assets/sounds/ui_hover_premium_01.wav new file mode 100644 index 0000000..834b5d3 Binary files /dev/null and b/assets/sounds/ui_hover_premium_01.wav differ diff --git a/assets/sounds/ui_kill_premium_00.wav b/assets/sounds/ui_kill_premium_00.wav new file mode 100644 index 0000000..0f28a4b Binary files /dev/null and b/assets/sounds/ui_kill_premium_00.wav differ diff --git a/assets/sounds/weapon_ak47_00.wav b/assets/sounds/weapon_ak47_00.wav new file mode 100644 index 0000000..c391ec8 Binary files /dev/null and b/assets/sounds/weapon_ak47_00.wav differ diff --git a/assets/sounds/weapon_ak47_01.wav b/assets/sounds/weapon_ak47_01.wav new file mode 100644 index 0000000..7d26151 Binary files /dev/null and b/assets/sounds/weapon_ak47_01.wav differ diff --git a/assets/sounds/weapon_ak47_02.wav b/assets/sounds/weapon_ak47_02.wav new file mode 100644 index 0000000..382d680 Binary files /dev/null and b/assets/sounds/weapon_ak47_02.wav differ diff --git a/assets/sounds/weapon_awp_00.wav b/assets/sounds/weapon_awp_00.wav new file mode 100644 index 0000000..dced7cf Binary files /dev/null and b/assets/sounds/weapon_awp_00.wav differ diff --git a/assets/sounds/weapon_awp_01.wav b/assets/sounds/weapon_awp_01.wav new file mode 100644 index 0000000..ec6431b Binary files /dev/null and b/assets/sounds/weapon_awp_01.wav differ diff --git a/assets/sounds/weapon_dmr_00.wav b/assets/sounds/weapon_dmr_00.wav new file mode 100644 index 0000000..ea23c93 Binary files /dev/null and b/assets/sounds/weapon_dmr_00.wav differ diff --git a/assets/sounds/weapon_dmr_01.wav b/assets/sounds/weapon_dmr_01.wav new file mode 100644 index 0000000..2bf4b82 Binary files /dev/null and b/assets/sounds/weapon_dmr_01.wav differ diff --git a/assets/sounds/weapon_knife_00.wav b/assets/sounds/weapon_knife_00.wav new file mode 100644 index 0000000..6397692 Binary files /dev/null and b/assets/sounds/weapon_knife_00.wav differ diff --git a/assets/sounds/weapon_knife_01.wav b/assets/sounds/weapon_knife_01.wav new file mode 100644 index 0000000..4bd29d1 Binary files /dev/null and b/assets/sounds/weapon_knife_01.wav differ diff --git a/assets/sounds/weapon_knife_02.wav b/assets/sounds/weapon_knife_02.wav new file mode 100644 index 0000000..d750d33 Binary files /dev/null and b/assets/sounds/weapon_knife_02.wav differ diff --git a/assets/sounds/weapon_m4_00.wav b/assets/sounds/weapon_m4_00.wav new file mode 100644 index 0000000..433b924 Binary files /dev/null and b/assets/sounds/weapon_m4_00.wav differ diff --git a/assets/sounds/weapon_m4_01.wav b/assets/sounds/weapon_m4_01.wav new file mode 100644 index 0000000..364ec89 Binary files /dev/null and b/assets/sounds/weapon_m4_01.wav differ diff --git a/assets/sounds/weapon_m4_02.wav b/assets/sounds/weapon_m4_02.wav new file mode 100644 index 0000000..06776f1 Binary files /dev/null and b/assets/sounds/weapon_m4_02.wav differ diff --git a/assets/sounds/weapon_mortar_00.wav b/assets/sounds/weapon_mortar_00.wav new file mode 100644 index 0000000..3733441 Binary files /dev/null and b/assets/sounds/weapon_mortar_00.wav differ diff --git a/assets/sounds/weapon_mp7_00.wav b/assets/sounds/weapon_mp7_00.wav new file mode 100644 index 0000000..5220798 Binary files /dev/null and b/assets/sounds/weapon_mp7_00.wav differ diff --git a/assets/sounds/weapon_mp7_01.wav b/assets/sounds/weapon_mp7_01.wav new file mode 100644 index 0000000..c2e9884 Binary files /dev/null and b/assets/sounds/weapon_mp7_01.wav differ diff --git a/assets/sounds/weapon_mp7_02.wav b/assets/sounds/weapon_mp7_02.wav new file mode 100644 index 0000000..df95521 Binary files /dev/null and b/assets/sounds/weapon_mp7_02.wav differ diff --git a/assets/sounds/weapon_nailgun_00.wav b/assets/sounds/weapon_nailgun_00.wav new file mode 100644 index 0000000..9ad2691 Binary files /dev/null and b/assets/sounds/weapon_nailgun_00.wav differ diff --git a/assets/sounds/weapon_nailgun_01.wav b/assets/sounds/weapon_nailgun_01.wav new file mode 100644 index 0000000..06fdbd5 Binary files /dev/null and b/assets/sounds/weapon_nailgun_01.wav differ diff --git a/assets/sounds/weapon_plasma_00.wav b/assets/sounds/weapon_plasma_00.wav new file mode 100644 index 0000000..668d435 Binary files /dev/null and b/assets/sounds/weapon_plasma_00.wav differ diff --git a/assets/sounds/weapon_plasma_01.wav b/assets/sounds/weapon_plasma_01.wav new file mode 100644 index 0000000..51ccf2f Binary files /dev/null and b/assets/sounds/weapon_plasma_01.wav differ diff --git a/assets/sounds/weapon_reload_00.wav b/assets/sounds/weapon_reload_00.wav new file mode 100644 index 0000000..afa29b6 Binary files /dev/null and b/assets/sounds/weapon_reload_00.wav differ diff --git a/assets/sounds/weapon_rocket_00.wav b/assets/sounds/weapon_rocket_00.wav new file mode 100644 index 0000000..052f981 Binary files /dev/null and b/assets/sounds/weapon_rocket_00.wav differ diff --git a/assets/sounds/weapon_shotgun_00.wav b/assets/sounds/weapon_shotgun_00.wav new file mode 100644 index 0000000..c0a7fc4 Binary files /dev/null and b/assets/sounds/weapon_shotgun_00.wav differ diff --git a/assets/sounds/weapon_shotgun_01.wav b/assets/sounds/weapon_shotgun_01.wav new file mode 100644 index 0000000..c2be0a6 Binary files /dev/null and b/assets/sounds/weapon_shotgun_01.wav differ diff --git a/assets/sounds/weapon_swarm_00.wav b/assets/sounds/weapon_swarm_00.wav new file mode 100644 index 0000000..5664080 Binary files /dev/null and b/assets/sounds/weapon_swarm_00.wav differ diff --git a/assets/sounds/weapon_swarm_01.wav b/assets/sounds/weapon_swarm_01.wav new file mode 100644 index 0000000..b09390e Binary files /dev/null and b/assets/sounds/weapon_swarm_01.wav differ diff --git a/assets/ui/map_previews/dust2.png b/assets/ui/map_previews/dust2.png index 333dd63..0463176 100644 Binary files a/assets/ui/map_previews/dust2.png and b/assets/ui/map_previews/dust2.png differ diff --git a/assets/ui/map_previews/fps_blockout.png b/assets/ui/map_previews/fps_blockout.png index 04fed9e..b49bc97 100644 Binary files a/assets/ui/map_previews/fps_blockout.png and b/assets/ui/map_previews/fps_blockout.png differ diff --git a/assets/ui/map_previews/neon_alley.png b/assets/ui/map_previews/neon_alley.png index 58a9567..b3bfd59 100644 Binary files a/assets/ui/map_previews/neon_alley.png and b/assets/ui/map_previews/neon_alley.png differ diff --git a/assets/ui/map_previews/procedural_arena.png b/assets/ui/map_previews/procedural_arena.png index 43cdff0..690f0e9 100644 Binary files a/assets/ui/map_previews/procedural_arena.png and b/assets/ui/map_previews/procedural_arena.png differ diff --git a/assets/ui/map_previews/sakura_crossing.png b/assets/ui/map_previews/sakura_crossing.png new file mode 100644 index 0000000..39dcdde Binary files /dev/null and b/assets/ui/map_previews/sakura_crossing.png differ diff --git a/assets/ui/map_previews/test_level.png b/assets/ui/map_previews/test_level.png index d5b2156..7c034cc 100644 Binary files a/assets/ui/map_previews/test_level.png and b/assets/ui/map_previews/test_level.png differ diff --git a/characters/dance_modifier.gd b/characters/dance_modifier.gd deleted file mode 100644 index b996364..0000000 --- a/characters/dance_modifier.gd +++ /dev/null @@ -1,239 +0,0 @@ -extends SkeletonModifier3D -class_name DanceModifier - -## Drives a dance routine onto the skeleton, over whatever clip is playing. -## -## Runs AFTER the shooter pose layer and blends over it, so starting a dance -## takes the arms off the weapon smoothly rather than cutting, and stopping one -## hands them back the same way. The spring solver runs after both, so hair and -## cloth follow the dance without anything being asked to make that happen. -## -## The routine data — and the reasoning behind why the motion is built the way it -## is — lives in characters/dance_routines.gd. This is the machine that plays it. - -## The routine to play. See DanceRoutines.ROUTINES. -var routine: Dictionary = {} -## 0..1. Eased by the owner, so a dance fades in and out rather than snapping. -var weight: float = 0.0 -## `.rig.json` roles, for bone resolution. See RigRoles. -var roles: Dictionary = {} - -var _idx: Dictionary = {} -var _resolved := false -var _t: float = 0.0 - -## Per-link overlap, in seconds, from the hips outward. Index 0 is the hips. -const CHAIN := ["DEF-hips", "DEF-spine.001", "DEF-spine.002", "DEF-spine.003"] - - -func _process_modification() -> void: - var skel := get_skeleton() - if skel == null or weight <= 0.001 or routine.is_empty(): - return - if not _resolved: - _idx = RigRoles.resolve(skel, roles) - _resolved = true - - var delta := get_physics_process_delta_time() if Engine.is_in_physics_frame() \ - else get_process_delta_time() - _t += delta - - var bpm: float = float(routine.get("bpm", 100.0)) - var beat := _t * bpm / 60.0 - var lag: float = float(routine.get("lag", 0.05)) * bpm / 60.0 - var w := weight - - # ── Hips: the driver ──────────────────────────────────────────────────── - # - # Translated, not just rotated. A body that never leaves its own axis reads - # as a puppet on a stick; moving the hips is what makes the legs look like - # they are carrying someone, and every other channel below is a reaction to - # this one. - var swing := _wave(beat, 2.0, 0.0) - var bob := _wave(beat, float(routine.get("hip_bob_beats", 1.0)), 0.25) - var hips: int = _idx.get("DEF-hips", -1) - if hips >= 0: - # Character-right is -X in skeleton space, up is +Y. - var offset := Vector3(-swing * float(routine.get("hip_swing", 0.0)), - bob * float(routine.get("hip_bob", 0.0)), 0.0) * w - _offset_bone(skel, hips, offset) - _add_space(skel, hips, - Quaternion(Vector3(0, 0, 1), swing * float(routine.get("hip_roll", 0.0)) * w) - * Quaternion(Vector3(0, 1, 0), swing * float(routine.get("hip_yaw", 0.0)) * w) - * Quaternion(Vector3(1, 0, 0), bob * float(routine.get("hip_pitch", 0.0)) * w)) - - # ── Spine: the same motion, later ─────────────────────────────────────── - # - # Each link reads the wave at `beat - lag * i`, which is the whole of - # overlapping action. It is one subtraction and it is the difference between - # a rig oscillating and a person moving. - var counter: float = float(routine.get("spine_counter", 0.0)) - var links := CHAIN.slice(1) - var n := maxf(links.size(), 1) - for i in links.size(): - var b: int = _idx.get(links[i], -1) - if b < 0: - continue - var at := beat - lag * float(i + 1) - var s := _wave(at, 2.0, 0.0) - var v := _wave(at, float(routine.get("hip_bob_beats", 1.0)), 0.25) - var q := Quaternion(Vector3(0, 0, 1), - s * float(routine.get("spine_roll", 0.0)) * w / n) \ - * Quaternion(Vector3(0, 1, 0), - (s * float(routine.get("spine_yaw", 0.0)) - - s * float(routine.get("hip_yaw", 0.0)) * counter) * w / n) \ - * Quaternion(Vector3(1, 0, 0), - v * float(routine.get("spine_pitch", 0.0)) * w / n) - _add_space(skel, b, q) - - _head(skel, beat, lag, w) - _arms(skel, beat, lag, w) - _legs(skel, beat, w) - - -## Head and neck: the last link in the chain, and the one carrying the spot. -func _head(skel: Skeleton3D, beat: float, lag: float, w: float) -> void: - var at := beat - lag * float(CHAIN.size()) - var s := _wave(at, 2.0, 0.0) - var v := _wave(at, float(routine.get("hip_bob_beats", 1.0)), 0.25) - - var yaw: float = s * float(routine.get("head_yaw", 0.0)) - - # SPOTTING. A dancer turning keeps their head pointed at one place for as - # long as they can, then whips it round to catch up. It is what stops them - # getting dizzy, and it is the single most recognisable thing about a turn — - # a head that simply rotates with the shoulders reads as a mannequin on a - # turntable. - # - # So this is not a wave. The head COUNTERS the body's yaw exactly while the - # hold lasts, then releases over a short window and lets the neck catch up. - var spot: float = float(routine.get("spot", 0.0)) - if spot > 0.001: - var body_yaw: float = s * float(routine.get("hip_yaw", 0.0)) \ - + s * float(routine.get("spine_yaw", 0.0)) - # Where in the two-beat turn we are, 0..1. - var u := fposmod(at / 2.0, 1.0) - # Hold for the first 70%, then whip round over the next 20%, then arrive. - var hold := 1.0 - smoothstep(0.70, 0.90, u) - yaw -= body_yaw * spot * hold - - var q := Quaternion(Vector3(0, 1, 0), yaw * w) \ - * Quaternion(Vector3(0, 0, 1), s * float(routine.get("head_roll", 0.0)) * w) \ - * Quaternion(Vector3(1, 0, 0), - (v * float(routine.get("head_bob", 0.0)) - + v * float(routine.get("head_pitch", 0.0))) * w) - # Split, so the whole column leans rather than the skull hinging off a rigid - # neck. - _add_space(skel, _idx.get("DEF-neck", -1), Quaternion.IDENTITY.slerp(q, 0.4)) - _add_space(skel, _idx.get("DEF-head", -1), Quaternion.IDENTITY.slerp(q, 0.6)) - - -## Arms: out from the body, swinging in opposition, elbows folding on the beat. -## -## The two arms are half a cycle apart, which is what opposition is. Both read -## the wave later than the spine did, so the hands are the last thing to arrive — -## the end of the chain, where overlap is most visible. -func _arms(skel: Skeleton3D, beat: float, lag: float, w: float) -> void: - var at := beat - lag * float(CHAIN.size() + 1) - var beats: float = float(routine.get("arm_beats", 2.0)) - var out: float = float(routine.get("arm_out", 0.0)) - var swing: float = float(routine.get("arm_swing", 0.0)) - var elbow: float = float(routine.get("elbow", 0.0)) - - for sign_i in 2: - var right := sign_i == 0 - var side := 1.0 if right else -1.0 - var s := _wave(at + (0.0 if right else beats * 0.5), beats, 0.0) - var ua: int = _idx.get("DEF-upper_arm." + ("R" if right else "L"), -1) - var fa: int = _idx.get("DEF-forearm." + ("R" if right else "L"), -1) - if ua >= 0: - # Character-right is -X, so a positive Z rotation lifts the LEFT arm - # and drops the right — hence the side flip. `out` is a static lift - # that the swing then rides on top of, which is what stops the arms - # from passing through the body at the bottom of the stroke. - _add_space(skel, ua, - Quaternion(Vector3(0, 0, 1), -side * (out + s * 0.35 * swing) * w) - * Quaternion(Vector3(1, 0, 0), s * swing * w)) - if fa >= 0: - # The elbow only ever folds, never hyperextends: a signed wave here - # bends the forearm backwards through the joint on half of every - # cycle, which is the most obvious possible tell. - var fold := (0.5 + 0.5 * s) * elbow - _add_space(skel, fa, Quaternion(Vector3(1, 0, 0), fold * w)) - - -## Legs: knees absorbing the bob, out of phase with each other so the weight -## visibly transfers from one to the other. -func _legs(skel: Skeleton3D, beat: float, w: float) -> void: - var knee: float = float(routine.get("knee", 0.0)) - if knee <= 0.001: - return - var beats: float = float(routine.get("hip_bob_beats", 1.0)) - for sign_i in 2: - var right := sign_i == 0 - var s := _wave(beat + (0.0 if right else beats * 0.5), beats, 0.25) - var thigh: int = _idx.get("DEF-thigh." + ("R" if right else "L"), -1) - var shin: int = _idx.get("DEF-shin." + ("R" if right else "L"), -1) - var bend := (0.5 + 0.5 * s) * knee - # Thigh forward and shin back by twice as much, so the foot stays roughly - # under the hip instead of the whole leg swinging out in front. - _add_space(skel, thigh, Quaternion(Vector3(1, 0, 0), -bend * 0.5 * w)) - _add_space(skel, shin, Quaternion(Vector3(1, 0, 0), bend * w)) - - -# ── The wave ───────────────────────────────────────────────────────────────── - - -## One channel's value at `beat`, in -1..1. -## -## `shape` bends a sine so it HANGS at the extremes and SNAPS between them, which -## is what an animator's key-and-breakdown pass produces and what a raw sine -## cannot. At shape 1 this is exactly `sin`; above it the curve gets punchier -## while staying continuous and staying in -1..1, so no amount of shaping can -## make a channel overshoot its authored amplitude. -## -## `steps` quantises the result instead, for the robot — the one routine whose -## whole point is that it does NOT move like the others. -func _wave(beat: float, beats: float, phase: float) -> float: - if beats <= 0.001: - return 0.0 - var u := beat / beats + phase - var steps: int = int(routine.get("steps", 0)) - if steps > 0: - # Hold a value for a whole step, then jump. Rounded rather than floored - # so the extremes are actually reached — a floor never returns +1. - return sin(TAU * (round(u * float(steps)) / float(steps))) - var s := sin(TAU * u) - var shape: float = float(routine.get("shape", 1.0)) - if is_equal_approx(shape, 1.0): - return s - return signf(s) * pow(absf(s), 1.0 / shape) - - -# ── Bone plumbing ──────────────────────────────────────────────────────────── - - -## Move a bone by an offset expressed in SKELETON space. -## -## A bone's pose position is in its PARENT's space, so the offset has to be -## rotated out of skeleton space by the parent's rest basis first. Skipping that -## sends the hips sideways in whatever direction the rig happens to have called -## "x", which differs per character. -func _offset_bone(skel: Skeleton3D, idx: int, offset: Vector3) -> void: - if idx < 0 or offset == Vector3.ZERO: - return - var parent := skel.get_bone_parent(idx) - var local := offset - if parent >= 0: - local = skel.get_bone_global_rest(parent).basis.inverse() * offset - skel.set_bone_pose_position(idx, skel.get_bone_rest(idx).origin + local) - - -## Compose a skeleton-space rotation onto a bone's animated local pose. Same -## contract as ShooterPoseModifier._add_space. -func _add_space(skel: Skeleton3D, idx: int, q_space: Quaternion) -> void: - if idx < 0: - return - var b := skel.get_bone_global_rest(idx).basis.get_rotation_quaternion() - var local := b.inverse() * q_space * b - skel.set_bone_pose_rotation(idx, skel.get_bone_pose_rotation(idx) * local) diff --git a/characters/dance_modifier.gd.uid b/characters/dance_modifier.gd.uid deleted file mode 100644 index f5cfb65..0000000 --- a/characters/dance_modifier.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bab4y7ba0yl87 diff --git a/characters/dance_routines.gd b/characters/dance_routines.gd index 0a03b4b..4e457fc 100644 --- a/characters/dance_routines.gd +++ b/characters/dance_routines.gd @@ -1,182 +1,43 @@ extends Object class_name DanceRoutines -## The five emotes, as data. +## Five authored emotes available through the radial wheel. ## -## They are procedural rather than authored clips because the shared animation -## library ships exactly one `Dance_Loop`, and five copies of one clip is not -## five dances. What the runtime DOES have is a working procedural pose layer -## over a real skeleton with spring-driven hair and cloth, which is enough to -## build a dance out of if the motion is constructed the way an animator would -## construct it rather than the way a programmer reaches for first. -## -## ── Why not just wire sine waves to the bones ─────────────────────────────── -## -## Because that is what "programmer animation" looks like, and everyone can tell. -## A raw sine moves fastest through the middle and slowest at the ends by exactly -## the same amount on every channel, all in phase, forever. The result floats. It -## has no weight, no accent, and no sense that one part of the body is driving and -## the rest is following. -## -## Four principles fix that, and all four are cheap: -## -## OVERLAP the body is a chain, and a chain does not move as one piece. -## Hips lead, spine follows a beat later, chest later still, -## head last. `lag` below is that, in seconds per link. It is -## the single largest difference between "a rig oscillating" -## and "a person moving", and the spring bones then carry it -## out through the hair and the skirt for free. -## ACCENT a dance HITS poses. `shape` bends the wave so it hangs at -## the extremes and snaps between them — the same asymmetry a -## key-and-breakdown pass produces by hand. `sin` is shape 1.0; -## above that it gets punchier. -## WEIGHT a body that never leaves its own axis reads as a puppet. Real -## dances move the HIPS — side to side, up and down — and the -## rest of the body reacts. `hip_swing` and `hip_bob` are -## translations, not rotations, and they are what make the legs -## look like they are carrying someone. -## ANTICIPATION the counter-move before the move. Handled per routine by -## running a channel at a fraction of a beat AHEAD of the one it -## precedes, rather than by a separate mechanism. -## -## ── The shape of a routine ────────────────────────────────────────────────── -## -## Everything is derived from one `bpm`, so no two channels can drift apart no -## matter how long the emote runs — which is the other thing that goes wrong when -## channels are given independent frequencies that are not exact ratios. -## -## Channel amounts are radians (rotations) or metres (the two hip translations). -## `beats` is how many beats that channel takes for one full cycle, so 2 is a -## side-to-side that takes two beats to return, 1 is once per beat, and 0.5 is -## twice per beat. Fractions of a beat are how a routine gets a cross-rhythm -## without leaving the grid. - -## `id` is what gets networked, so these strings must stay stable. +## Every entry names an imported motion-capture clip. The previous version +## synthesized five routines by oscillating individual bones over one dance +## clip; that produced the programmer-animation quality this project is moving +## away from. Network IDs stay stable so existing peers and saved selections +## remain compatible. const ROUTINES := [ { "id": "two_step", - "name": "Two-Step", + "name": "Dance", "icon": "♪", - "bpm": 104.0, - "lag": 0.055, - "shape": 1.35, - # The foundation step: weight rocks side to side, the shoulders counter - # the hips, the arms hang and swing off the shoulders a beat behind. - "hip_swing": 0.075, - "hip_bob": 0.022, - "hip_bob_beats": 1.0, - "hip_roll": 0.16, - "hip_yaw": 0.20, - "spine_roll": 0.13, - "spine_counter": 0.55, - "head_roll": 0.22, - "head_bob": 0.10, - "arm_swing": 0.55, - "arm_out": 0.42, - "arm_beats": 2.0, - "elbow": 0.75, - "knee": 0.30, + "clip": "Dance", }, { "id": "body_wave", - "name": "Body Wave", - "icon": "〜", - "bpm": 88.0, - # The whole point of this one is the lag: a wave travelling up the spine - # IS overlap, made visible. At 0.13 s per link the crest takes most of a - # beat to get from the hips to the head. - "lag": 0.13, - "shape": 1.15, - "hip_swing": 0.03, - "hip_bob": 0.045, - "hip_bob_beats": 2.0, - "hip_pitch": 0.26, - "spine_pitch": 0.30, - "spine_counter": 0.0, - "head_pitch": 0.22, - "head_roll": 0.06, - "arm_out": 0.85, - "arm_swing": 0.20, - "arm_beats": 4.0, - "elbow": 0.55, - "knee": 0.18, + "name": "Chest Stretch", + "icon": "◆", + "clip": "EmoteStretch", }, { "id": "robot", - "name": "Robot", - "icon": "▣", - "bpm": 112.0, - # No lag, and the motion is QUANTISED — see `steps`. Both are deliberate - # violations of everything above, and they work for exactly that reason: - # the robot reads as mechanical because the viewer has been shown four - # other routines that do not. - "lag": 0.0, - "shape": 1.0, - "steps": 4, - "hip_swing": 0.04, - "hip_bob": 0.012, - "hip_bob_beats": 1.0, - "hip_yaw": 0.30, - "spine_yaw": 0.34, - "spine_counter": 0.0, - "head_yaw": 0.42, - "arm_out": 1.05, - "arm_swing": 0.85, - "arm_beats": 2.0, - "elbow": 1.35, - "knee": 0.10, + "name": "Call Out", + "icon": "!", + "clip": "EmoteCall", }, { "id": "bounce", - "name": "Bounce", - "icon": "▲", - "bpm": 128.0, - "lag": 0.035, - # The punchiest shape in the set. A bounce lives entirely in the accent: - # the body hangs at the top and slams through the bottom, which is a - # gravity read, and a plain sine cannot express it. - "shape": 2.2, - "hip_swing": 0.03, - "hip_bob": 0.070, - "hip_bob_beats": 1.0, - "hip_roll": 0.08, - "spine_pitch": 0.14, - "spine_counter": 0.30, - "head_bob": 0.16, - "head_roll": 0.10, - "arm_swing": 0.95, - "arm_out": 0.30, - "arm_beats": 1.0, - "elbow": 1.05, - # Deep knees. This is the routine where the legs do the work, and a bounce - # with straight legs looks like a character being shaken. - "knee": 0.85, + "name": "Yes!", + "icon": "✓", + "clip": "EmoteYes", }, { "id": "spin", - "name": "Spin", - "icon": "↻", - "bpm": 96.0, - "lag": 0.07, - "shape": 1.5, - "hip_swing": 0.05, - "hip_bob": 0.030, - "hip_bob_beats": 2.0, - "hip_yaw": 0.85, - "spine_yaw": 0.30, - "spine_counter": 0.0, - "spine_roll": 0.14, - # SPOTTING: the head holds its heading while the body turns under it, then - # whips round to catch up. It is what a real dancer does to keep from - # getting dizzy, and it is the most recognisable thing about a turn. See - # `spot` in DanceModifier — this is not a wave, it is a hold and a snap. - "spot": 1.0, - "head_yaw": 0.0, - "arm_out": 1.15, - "arm_swing": 0.25, - "arm_beats": 4.0, - "elbow": 0.35, - "knee": 0.22, + "name": "No", + "icon": "×", + "clip": "EmoteNo", }, ] @@ -185,14 +46,16 @@ static func count() -> int: return ROUTINES.size() -## A routine by index, wrapped so an out-of-range network value cannot crash a -## remote peer's model. static func get_routine(index: int) -> Dictionary: if ROUTINES.is_empty(): return {} return ROUTINES[posmod(index, ROUTINES.size())] +static func clip_of(index: int) -> String: + return String(get_routine(index).get("clip", "Dance")) + + static func name_of(index: int) -> String: return String(get_routine(index).get("name", "")) diff --git a/characters/humanoid_model.gd b/characters/humanoid_model.gd index 6116bd1..ce32b74 100644 --- a/characters/humanoid_model.gd +++ b/characters/humanoid_model.gd @@ -35,6 +35,14 @@ var calf_r: MeshInstance3D var current_state: String = "idle" var movement_speed: float = 0.0 var _anim_time: float = 0.0 +var _locomotion_strafe: float = 0.0 +var _locomotion_fwd: float = 0.0 +var _brake_left: float = 0.0 +var _brake_fwd: float = 1.0 +var _brake_strength: float = 0.0 +var _brake_weight: float = 0.0 +var _brake_armed := true +const BRAKE_DURATION := 0.46 func _ready() -> void: rotation_degrees.y = 180 @@ -240,11 +248,44 @@ func _apply_color(col: Color) -> void: LevelMaterials.apply_toon_recursive(root_pivot, 0.004) func update_state(state: String, speed: float, is_crouching: bool = false) -> void: + var previous_speed := movement_speed + var slowdown := previous_speed - speed + if state in ["ground", "idle"]: + if speed > 4.5 and slowdown < 0.05: + _brake_armed = true + if _brake_armed and previous_speed > 4.5 and slowdown > 0.12: + _brake_left = BRAKE_DURATION + _brake_strength = clampf(previous_speed / 10.0, 0.55, 1.0) + _brake_armed = false + if absf(_locomotion_fwd) > 0.15: + _brake_fwd = signf(_locomotion_fwd) + elif state not in ["ground", "idle"]: + _brake_left = 0.0 + _brake_armed = true current_state = state movement_speed = speed if is_crouching and current_state == "ground": current_state = "crouch" + +func set_locomotion(strafe: float, fwd: float, _ads: float) -> void: + _locomotion_strafe = clampf(strafe, -1.0, 1.0) + _locomotion_fwd = clampf(fwd, -1.0, 1.0) + + +func get_jet_socket_world_positions() -> Array[Vector3]: + if not root_pivot: + return [] + return [ + upper_arm_l_pivot.global_position \ + + root_pivot.global_basis * Vector3(-0.04, -0.02, -0.16), + upper_arm_r_pivot.global_position \ + + root_pivot.global_basis * Vector3(0.04, -0.02, -0.16), + calf_l_pivot.to_global(Vector3(0.0, -0.35, -0.045)), + calf_r_pivot.to_global(Vector3(0.0, -0.35, -0.045)), + ] + + var is_holding_weapon: bool = false func set_weapon(script_path: String) -> void: @@ -336,6 +377,17 @@ func _set_shadows_recursive(node: Node) -> void: _set_shadows_recursive(child) func _process(delta: float) -> void: + var brake_target := 0.0 + if _brake_left > 0.0 and current_state in ["ground", "idle"]: + _brake_left = maxf(0.0, _brake_left - delta) + var progress := 1.0 - _brake_left / BRAKE_DURATION + var envelope := smoothstep(0.0, 1.0, progress / 0.30) \ + if progress < 0.30 else \ + 1.0 - smoothstep(0.0, 1.0, (progress - 0.30) / 0.70) + brake_target = envelope * _brake_strength + _brake_weight = lerpf( + _brake_weight, brake_target, + 1.0 - exp((-22.0 if brake_target > _brake_weight else -10.0) * delta)) var anim_speed = 1.0 if current_state == "ground" and movement_speed > 1.0: anim_speed = movement_speed * 0.4 @@ -358,7 +410,8 @@ func _process(delta: float) -> void: "ground", "idle": if movement_speed > 1.0: # Run cycle - var swing = sin(_anim_time * 5.0) + var reverse_cycle := -1.0 if _locomotion_fwd < -0.12 else 1.0 + var swing = sin(_anim_time * 5.0) * reverse_cycle # Hips t_thigh_l_rot.x = -swing * 1.6 t_thigh_r_rot.x = swing * 1.6 @@ -376,6 +429,11 @@ func _process(delta: float) -> void: # Bounce slightly t_root_pos.y = abs(cos(_anim_time * 5.0)) * 0.1 + # Directional silhouette: reverse travel moves the shoulders + # behind the hips instead of reusing the forward sprint lean. + t_root_rot.x = deg_to_rad(10.0) if _locomotion_fwd < -0.12 \ + else deg_to_rad(-8.0) + t_root_rot.z = deg_to_rad(-7.0) * _locomotion_strafe else: # Idle var breath = sin(_anim_time * 2.0) @@ -465,7 +523,7 @@ func _process(delta: float) -> void: t_calf_r_rot.x = deg_to_rad(10) "dash": - t_root_rot.x = deg_to_rad(-70) + t_root_rot.x = deg_to_rad(-38) t_upper_arm_l_rot.x = deg_to_rad(-170) t_lower_arm_l_rot.x = 0.0 t_upper_arm_r_rot.x = deg_to_rad(-170) @@ -487,7 +545,24 @@ func _process(delta: float) -> void: t_calf_l_rot.x = 0.0 t_thigh_r_rot.x = deg_to_rad(10) t_calf_r_rot.x = 0.0 - + + # Blend a single catching step over the locomotion result. It rises quickly + # and releases over the same span as the run-to-idle crossfade, preserving + # momentum without freezing the whole mannequin in an exaggerated plant. + if current_state in ["ground", "idle"] and _brake_weight > 0.001: + var travel_sign := -1.0 if _brake_fwd < 0.0 else 1.0 + t_root_pos.y = lerpf(t_root_pos.y, -0.025, _brake_weight) + t_root_rot.x = lerp_angle( + t_root_rot.x, deg_to_rad(8.0) * travel_sign, _brake_weight) + t_thigh_r_rot.x = lerp_angle( + t_thigh_r_rot.x, deg_to_rad(-24.0) * travel_sign, _brake_weight) + t_calf_r_rot.x = lerp_angle( + t_calf_r_rot.x, deg_to_rad(12.0) * travel_sign, _brake_weight) + t_thigh_l_rot.x = lerp_angle( + t_thigh_l_rot.x, deg_to_rad(8.0) * travel_sign, _brake_weight) + t_calf_l_rot.x = lerp_angle( + t_calf_l_rot.x, deg_to_rad(14.0), _brake_weight) + # Override right arm (and left) if holding a weapon if is_holding_weapon and current_state != "death": # Right arm holds the grip: elbow tucked at the ribs, forearm level so @@ -503,7 +578,7 @@ func _process(delta: float) -> void: t_upper_arm_l_rot.z = 0.55 t_lower_arm_l_rot.x = -1.0 - var lerp_speed = 40.0 * delta + var lerp_speed := 1.0 - exp(-14.0 * delta) root_pivot.position = root_pivot.position.lerp(t_root_pos, lerp_speed) root_pivot.rotation = _lerp_vec3(root_pivot.rotation, t_root_rot, lerp_speed) diff --git a/characters/player_jet_vfx.gd b/characters/player_jet_vfx.gd new file mode 100644 index 0000000..a766804 --- /dev/null +++ b/characters/player_jet_vfx.gd @@ -0,0 +1,159 @@ +extends Node3D +class_name PlayerJetVFX + +## Four restrained anime-thrust accents: two shoulder nozzles and two foot +## nozzles. The geometry is deliberately graphic instead of particle-heavy so +## the character remains 90% silhouette/animation and only 10% visible tech. + +const DOUBLE_JUMP_TIME := 0.40 +const FLAME_LENGTH := 0.28 +const SHOULDER_SOCKETS := [ + Vector3(-0.27, 1.34, 0.16), + Vector3(0.27, 1.34, 0.16), +] +const FOOT_SOCKETS := [ + Vector3(-0.16, 0.14, 0.08), + Vector3(0.16, 0.14, 0.08), +] + +var _flames: Array[Node3D] = [] +var _burst_left := 0.0 +var _dash_active := false +var _dash_tail := 0.0 +var _render_enabled := true +var _exhaust_direction := Vector3.DOWN +var _time := 0.0 +var _visual_model: Node3D = null + + +func _ready() -> void: + for socket in SHOULDER_SOCKETS + FOOT_SOCKETS: + _flames.append(_make_flame(socket)) + visible = false + + +func _make_flame(socket: Vector3) -> Node3D: + var flame := Node3D.new() + flame.name = "JetFlame%02d" % _flames.size() + flame.position = socket + flame.set_meta("jet_flame", true) + add_child(flame) + + var outer := MeshInstance3D.new() + outer.name = "OuterFlame" + outer.mesh = _cone_mesh(0.045, FLAME_LENGTH) + outer.material_override = _flame_material( + Color(1.0, 0.24, 0.05, 0.72), Color(1.0, 0.12, 0.02), 3.2) + outer.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + outer.position.y = FLAME_LENGTH * 0.5 + flame.add_child(outer) + + var inner := MeshInstance3D.new() + inner.name = "HotCore" + inner.mesh = _cone_mesh(0.016, FLAME_LENGTH * 0.72) + inner.material_override = _flame_material( + Color(0.82, 0.96, 1.0, 0.86), Color(0.36, 0.82, 1.0), 3.2) + inner.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + inner.position.y = FLAME_LENGTH * 0.36 + flame.add_child(inner) + return flame + + +func _cone_mesh(radius: float, height: float) -> CylinderMesh: + var mesh := CylinderMesh.new() + mesh.bottom_radius = radius + mesh.top_radius = 0.002 + mesh.height = height + mesh.radial_segments = 8 + mesh.rings = 1 + return mesh + + +func _flame_material(albedo: Color, emission: Color, energy: float) -> StandardMaterial3D: + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + mat.albedo_color = albedo + mat.emission_enabled = true + mat.emission = emission + mat.emission_energy_multiplier = energy + return mat + + +func set_render_enabled(enabled: bool) -> void: + _render_enabled = enabled + if not enabled: + visible = false + + +func set_visual_model(model: Node3D) -> void: + _visual_model = model + + +func burst_double_jump() -> void: + _burst_left = DOUBLE_JUMP_TIME + + +func set_dash(active: bool, movement_local: Vector2) -> void: + if _dash_active and not active: + _dash_tail = 0.14 + _dash_active = active + if not active: + return + var move := Vector3(movement_local.x, 0.0, -movement_local.y) + if move.length_squared() < 0.01: + move = Vector3.FORWARD + # Exhaust opposes travel and dips slightly, which makes the force direction + # readable without turning the flame into a large sci-fi effect. + _exhaust_direction = (-move.normalized() + Vector3.DOWN * 0.20).normalized() + + +func get_flame_count() -> int: + return _flames.size() + + +func get_exhaust_direction() -> Vector3: + return _exhaust_direction + + +func _process(delta: float) -> void: + _time += delta + _update_sockets() + _burst_left = maxf(0.0, _burst_left - delta) + _dash_tail = maxf(0.0, _dash_tail - delta) + var dash_visible := _dash_active or _dash_tail > 0.0 + var active := dash_visible or _burst_left > 0.0 + visible = _render_enabled and active + if not visible: + return + + if _burst_left > 0.0 and not dash_visible: + # The double-jump jets push mostly down, with just enough rear cant to + # keep the four flames separated in the silhouette. + _exhaust_direction = Vector3(0.0, -1.0, 0.16).normalized() + + var pulse := 0.90 + sin(_time * 54.0) * 0.08 + sin(_time * 31.0) * 0.04 + if _burst_left > 0.0 and not _dash_active: + pulse *= clampf(_burst_left / 0.08, 0.15, 1.0) + for i in _flames.size(): + var flame := _flames[i] + # A tiny outward fan keeps all four graphic wedges legible from the + # chase camera, even when the centerline points almost straight at it. + var side: float = [0.13, -0.13, 0.06, -0.06][i] + var nozzle_dir := (_exhaust_direction + Vector3(side, 0.0, 0.0)).normalized() + flame.quaternion = Quaternion(Vector3.UP, nozzle_dir) + var stagger := 1.0 + sin(_time * 43.0 + float(i) * 1.7) * 0.05 + flame.scale = Vector3(1.0, pulse * stagger, 1.0) + + +func _update_sockets() -> void: + if not is_instance_valid(_visual_model) \ + or not _visual_model.has_method("get_jet_socket_world_positions"): + return + var sockets: Array = _visual_model.get_jet_socket_world_positions() + if sockets.size() != _flames.size(): + return + for i in _flames.size(): + _flames[i].position = to_local(sockets[i]) diff --git a/characters/player_jet_vfx.gd.uid b/characters/player_jet_vfx.gd.uid new file mode 100644 index 0000000..4e1705f --- /dev/null +++ b/characters/player_jet_vfx.gd.uid @@ -0,0 +1 @@ +uid://b6fppxauwra26 diff --git a/characters/player_skin.gd b/characters/player_skin.gd index b4249d7..4c04df7 100644 --- a/characters/player_skin.gd +++ b/characters/player_skin.gd @@ -10,3 +10,16 @@ class_name PlayerSkin @export var model_path: String = "" # Path to .glb file, empty = use procedural @export var color_tint: Color = Color(0.2, 0.4, 0.8) # For procedural model @export var is_unlocked: bool = true + +## The first-person arms use lightweight procedural geometry, but their +## palette and sleeve coverage still identify the selected character. +@export_group("Viewmodel Arms") +@export var viewmodel_sleeve_color: Color = Color(0.16, 0.34, 0.72) +@export var viewmodel_accent_color: Color = Color(0.28, 0.72, 1.0) +@export var viewmodel_skin_color: Color = Color(0.96, 0.80, 0.72) +@export var viewmodel_glove_color: Color = Color(0.10, 0.11, 0.16) +@export var viewmodel_nail_color: Color = Color(0.92, 0.68, 0.72) +## bare: exposed hand + nails; fingerless: glove palm with exposed fingertips; +## glove: fully covered; android: segmented synthetic hand. +@export_enum("bare", "fingerless", "glove", "android") var viewmodel_hand_style := "bare" +@export_range(0.2, 0.9, 0.01) var viewmodel_sleeve_ratio: float = 0.68 diff --git a/characters/rig_roles.gd b/characters/rig_roles.gd index d9084f1..74bc09f 100644 --- a/characters/rig_roles.gd +++ b/characters/rig_roles.gd @@ -18,10 +18,13 @@ class_name RigRoles const SPINE := ["DEF-hips", "DEF-spine.001", "DEF-spine.002", "DEF-spine.003"] ## Everything either pose layer asks for. -const CANONICAL := SPINE + ["DEF-neck", "DEF-head", +const CANONICAL := [ + "DEF-hips", "DEF-spine.001", "DEF-spine.002", "DEF-spine.003", + "DEF-neck", "DEF-head", "DEF-upper_arm.R", "DEF-forearm.R", "DEF-hand.R", "DEF-upper_arm.L", "DEF-forearm.L", "DEF-hand.L", - "DEF-thigh.R", "DEF-shin.R", "DEF-thigh.L", "DEF-shin.L"] + "DEF-thigh.R", "DEF-shin.R", "DEF-thigh.L", "DEF-shin.L", +] ## Resolve `names` against a skeleton, given the sidecar's role table. diff --git a/characters/skin_joint_helper.gd b/characters/skin_joint_helper.gd index 1018e40..afd4962 100644 --- a/characters/skin_joint_helper.gd +++ b/characters/skin_joint_helper.gd @@ -130,6 +130,7 @@ static func update(skeleton: Skeleton3D, driven: Array) -> void: static func _reweight(mi: MeshInstance3D, skeleton: Skeleton3D, joint_helpers: Dictionary, chain_l: PackedVector3Array, chain_r: PackedVector3Array, limb_radius: float) -> void: + var safe_bounds := mi.mesh.get_aabb() var skin: Skin = mi.skin var bone_of := {} for b in skin.get_bind_count(): @@ -179,6 +180,13 @@ static func _reweight(mi: MeshInstance3D, skeleton: Skeleton3D, rebuilt.surface_set_material(i, e["material"]) if e["name"] != "": rebuilt.surface_set_name(i, e["name"]) + # Preserve an explicit conservative bound across this second mesh rebuild. + # See SkinLegRepair: sparse runtime Skin binds can otherwise produce more + # bone AABBs than the renderer skeleton contains. + var padding := Vector3.ONE * maxf(safe_bounds.size.length() * 0.12, 0.25) + safe_bounds.position -= padding + safe_bounds.size += padding * 2.0 + rebuilt.custom_aabb = safe_bounds mi.mesh = rebuilt diff --git a/characters/skin_leg_repair.gd b/characters/skin_leg_repair.gd index 886b1e2..72525e3 100644 --- a/characters/skin_leg_repair.gd +++ b/characters/skin_leg_repair.gd @@ -144,6 +144,7 @@ static func _knee_height(skeleton: Skeleton3D) -> float: static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float, chain_l: PackedVector3Array, chain_r: PackedVector3Array, limb_radius: float) -> Array: + var safe_bounds := mi.mesh.get_aabb() var side := _side_map(mi.skin, skeleton) var torso_bone := _torso_map(mi.skin, skeleton) var surfaces: Array = [] @@ -169,6 +170,14 @@ static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float, rebuilt.surface_set_material(i, entry["material"]) if entry["name"] != "": rebuilt.surface_set_name(i, entry["name"]) + # ArrayMesh rebuilds per-bind bone bounds from ARRAY_BONES. Some runtime + # GLBs have a sparse Skin bind table, so that derived array can be longer + # than the renderer skeleton and mesh_get_aabb rejects it. A padded bind-pose + # bound is conservative for humanoid animation and avoids that invalid path. + var padding := Vector3.ONE * maxf(safe_bounds.size.length() * 0.12, 0.25) + safe_bounds.position -= padding + safe_bounds.size += padding * 2.0 + rebuilt.custom_aabb = safe_bounds mi.mesh = rebuilt return [snapped, removed] diff --git a/characters/skin_manager.gd b/characters/skin_manager.gd index 9ad4c73..8d8b257 100644 --- a/characters/skin_manager.gd +++ b/characters/skin_manager.gd @@ -38,6 +38,11 @@ func _add_color_skin(id: String, display_name: String, description: String, tint skin.skin_name = display_name skin.description = description skin.color_tint = tint + skin.viewmodel_sleeve_color = tint.darkened(0.18) + skin.viewmodel_accent_color = tint.lightened(0.22) + skin.viewmodel_glove_color = tint.darkened(0.48) + skin.viewmodel_nail_color = tint.lightened(0.34) + skin.viewmodel_hand_style = "fingerless" skin.is_unlocked = true skins[id] = skin @@ -61,9 +66,34 @@ func _load_skins_json() -> void: skin.description = entry.get("description", "") skin.model_path = entry.get("model", "") skin.is_unlocked = entry.get("unlocked", true) + var viewmodel = entry.get("viewmodel", {}) + if viewmodel is Dictionary: + skin.viewmodel_sleeve_color = _json_color( + viewmodel.get("sleeve", ""), skin.viewmodel_sleeve_color) + skin.viewmodel_accent_color = _json_color( + viewmodel.get("accent", ""), skin.viewmodel_accent_color) + skin.viewmodel_skin_color = _json_color( + viewmodel.get("skin", ""), skin.viewmodel_skin_color) + skin.viewmodel_glove_color = _json_color( + viewmodel.get("glove", ""), skin.viewmodel_glove_color) + skin.viewmodel_nail_color = _json_color( + viewmodel.get("nail", ""), skin.viewmodel_nail_color) + var hand_style: String = str(viewmodel.get( + "hand_style", skin.viewmodel_hand_style)) + if hand_style in ["bare", "fingerless", "glove", "android"]: + skin.viewmodel_hand_style = hand_style + skin.viewmodel_sleeve_ratio = clampf( + float(viewmodel.get("sleeve_ratio", skin.viewmodel_sleeve_ratio)), + 0.2, 0.9) skins[entry["id"]] = skin +func _json_color(value: Variant, fallback: Color) -> Color: + if value is String and not value.is_empty(): + return Color.from_string(value, fallback) + return fallback + + # ── Selection ───────────────────────────────────────────────────────────────── func get_skin(skin_id: String) -> PlayerSkin: diff --git a/characters/skinned_player_model.gd b/characters/skinned_player_model.gd index 451c59c..0eae46d 100644 --- a/characters/skinned_player_model.gd +++ b/characters/skinned_player_model.gd @@ -22,9 +22,20 @@ class_name SkinnedPlayerModel @export var shadows_only: bool = false @export var facing_flip: bool = true # glTF forward is +Z; players face -Z -## Horizontal speed (m/s) at which the Run clip plays at authored speed. -@export var run_anim_reference_speed: float = 8.0 -@export var walk_anim_reference_speed: float = 3.0 +## Ground speed (m/s) encoded by each root-motion source at 1x playback. +## Measured with tools/audit_animation_speeds.py, not eyeballed. The shipped +## clips are in-place, but these reference velocities let the BlendSpace and +## TimeScale reproduce the source stride length without foot sliding. +@export var walk_anim_reference_speed: float = 0.975 +@export var run_anim_reference_speed: float = 5.26148 +@export var sprint_anim_reference_speed: float = 8.25 +## Low-speed authored combat steps and the high-speed anime run are separate +## clips. The old setup put a 1.8 m/s side-step in the high-speed RunLeft/Right +## slots, then time-scaled it more than 6x at the game's 11 m/s ground speed. +@export var strafe_walk_anim_reference_speed: float = 3.0 +@export var strafe_run_anim_reference_speed: float = 8.25 +@export var wall_run_anim_reference_speed: float = 8.25 +@export var crouch_anim_reference_speed: float = 0.75 ## Canonical clip names -> fallback chain. First clip that exists wins, so a ## model with only Idle/Walk/Run still animates in every movement state. @@ -38,14 +49,33 @@ const CLIP_FALLBACKS := { "Land": ["Land", "Idle"], "Crouch": ["CrouchIdle", "Crouch", "Idle"], "CrouchWalk": ["CrouchWalk", "Crouch", "CrouchIdle", "Walk"], + "SlideStart": ["SlideStart", "Slide"], "Slide": ["Slide", "CrouchIdle", "Crouch", "Idle"], - "WallRun": ["WallRun", "Run", "Walk"], + "SlideExit": ["SlideExit", "Slide"], + "WallRunStart": ["WallRunStart", "WallRunLeft", "Fall"], + "WallRunLeft": ["WallRunLeft", "Fall"], + "WallRunRight": ["WallRunRight", "Fall"], + "WallRunExit": ["WallRunExit", "WallRunLeft", "Fall"], "WallCling": ["WallCling", "CrouchIdle", "Idle"], + "WallClimb": ["WallClimb", "WallCling", "Jump"], "Grapple": ["Grapple", "Fall", "Jump", "Idle"], "Dash": ["Dash", "Sprint", "Run", "Idle"], + "StrafeWalkForward": ["StrafeWalkForward", "Walk", "Run", "Idle"], + "StrafeWalkBackward": ["StrafeWalkBackward", "RunBackward", "Walk", "Idle"], + "StrafeWalkLeft": ["StrafeWalkLeft", "RunLeft", "Walk", "Idle"], + "StrafeWalkRight": ["StrafeWalkRight", "RunRight", "Walk", "Idle"], + "RunForward": ["RunForward", "Run", "Walk"], + "RunBackward": ["RunBackward", "Run", "Walk"], + "RunLeft": ["RunLeft", "Run", "Walk"], + "RunRight": ["RunRight", "Run", "Walk"], + "Roll": ["Roll", "Dash"], "Death": ["Death", "Fall"], "Hit": ["Hit", "Idle"], "Dance": ["Dance", "Idle"], + "EmoteStretch": ["EmoteStretch", "Dance", "Idle"], + "EmoteCall": ["EmoteCall", "Dance", "Idle"], + "EmoteYes": ["EmoteYes", "Dance", "Idle"], + "EmoteNo": ["EmoteNo", "Dance", "Idle"], "PistolIdle": ["PistolIdle", "Idle"], "PistolShoot": ["PistolShoot"], "PistolReload": ["PistolReload"], @@ -53,8 +83,11 @@ const CLIP_FALLBACKS := { } const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch", - "CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple", - "Dance", "PistolIdle"] + "CrouchIdle", "CrouchWalk", "Slide", "WallRunLeft", "WallRunRight", + "WallCling", "Grapple", + "StrafeWalkForward", "StrafeWalkBackward", "StrafeWalkLeft", "StrafeWalkRight", + "RunForward", "RunBackward", "RunLeft", "RunRight", "Dance", + "EmoteStretch", "EmoteCall", "EmoteYes", "EmoteNo", "PistolIdle"] const BLEND_TIME := 0.32 ## Per-clip blend overrides. Reaction moves still need to read as instant, but @@ -66,7 +99,8 @@ const BLEND_TIMES := { "Dash": 0.14, "Jump": 0.16, "Hit": 0.10, "Land": 0.16, "Slide": 0.22, "Death": 0.20, "Throw": 0.14, "PistolReload": 0.24, "Idle": 0.42, "PistolIdle": 0.42, "Walk": 0.40, "Run": 0.40, "Sprint": 0.40, - "CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.28, "WallRun": 0.30, + "CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.28, + "WallRunLeft": 0.18, "WallRunRight": 0.18, "WallCling": 0.28, "Grapple": 0.28, } @@ -82,10 +116,18 @@ const ACTIONS := { var skeleton: Skeleton3D var animation_player: AnimationPlayer var loaded: bool = false +## Coordinate frame for authored traversal clips. It only aligns the imported +## performance's forward axis with gameplay travel; it never rotates bones or +## manufactures poses. +var _motion_root: Node3D +var _wall_run_velocity_world := Vector3.ZERO +var _motion_yaw := 0.0 var _resolved_clips: Dictionary = {} # canonical name -> actual clip name var _current_clip: String = "" var _weapon_attachment: BoneAttachment3D +var _grapple_attachment: BoneAttachment3D +var _grapple_shoulder_attachment: BoneAttachment3D ## [child_bone, helper_bone] pairs driven every frame by the pose modifier. var _joint_helpers: Array = [] ## Contents of .rig.json — resolved bone roles, cloth chains, twist pairs @@ -96,8 +138,6 @@ var _rig_info: Dictionary = {} ## the same sidecar; drives the per-class cel look and answers `surfaces_of()`. var _surfaces: SkinSurfaces = null var _spring_mod: SpringBones -## The emote layer, between the shooter pose and the cloth springs. -var _dance_mod: DanceModifier var is_holding_weapon: bool = false ## Which hold archetype the equipped weapon uses — see WeaponHoldProfiles. Read ## by `_process` (a blade releases the off arm) and by the checks. @@ -115,13 +155,17 @@ var anchors: Dictionary = {} ## the wrist counter-rotation. See `ShooterPoseModifier.wrist_comp_r`. var _weapon_seat: Transform3D = Transform3D.IDENTITY -# Animation blending: locomotion plays full-body through a Transition node; +# Animation blending: authored locomotion cycles play through synchronized +# speed/direction blend spaces, while traversal states use their own clips; # gameplay one-shots (reload/throw/shoot/hit) play through an # AnimationNodeOneShot FILTERED to upper-body bones, so the legs keep # sliding/running underneath instead of popping to the one-shot's standing legs. var _anim_tree: AnimationTree -var _loco_trans: AnimationNodeTransition +var _state_trans: AnimationNodeTransition +var _loco_blend: AnimationNodeBlendSpace2D +var _crouch_blend: AnimationNodeBlendSpace1D var _upper_anim: AnimationNodeAnimation +var _current_state_node: String = "" var _upper_lock: float = 0.0 # seconds the one-shot owns the ARMS var _upper_total: float = 0.0 # its full duration, for progress 0..1 var _upper_action: String = "" # which ACTIONS entry is playing @@ -138,49 +182,17 @@ var _upper_action: String = "" # which ACTIONS entry is playing const UPPER_BONE_HINTS := ["shoulder", "upper_arm", "forearm", "hand", "thumb", "f_index", "f_middle", "f_ring", "f_pinky", "spine.002", "spine.003"] -# Grapple: world-space anchor the hook is attached to (drives the procedural -# zip pose — body aligned to the line, free arm reaching for the point). -var _grapple_point_world: Vector3 = Vector3.ZERO -var _cur_grapple: float = 0.0 - -# Procedural shooter pose layer (lean / slide / weapon hold), applied on top of -# the base clip by a SkeletonModifier3D so it composes with the animation. +# Mechanical shooter pose layer (aim, recoil, and weapon IK), applied after the +# authored animation by a SkeletonModifier3D. var _pose_mod: ShooterPoseModifier var _target_strafe: float = 0.0 var _target_fwd: float = 0.0 var _target_ads: float = 0.0 var _target_wall: float = 0.0 -var _cur_strafe: float = 0.0 -var _cur_fwd: float = 0.0 var _cur_ads: float = 0.0 -var _cur_slide: float = 0.0 var _cur_wall: float = 0.0 -## Which way the legs are actually travelling, in radians about the character's -## own up axis, and whether the locomotion cycle is running backwards to achieve -## it. See _update_travel. -var _cur_travel: float = 0.0 -var _travel_reverse: bool = false var _owner_visible: bool = false -## Horizontal speed from the last update_state, so the lean can scale with how -## fast the character is really moving. -var _speed: float = 0.0 -var _loco_tier: int = 0 -var _tier_age: float = 0.0 const POSE_SMOOTH := 10.0 -## Separate, slower rate for the whole-body lean. Roughly a 0.36 s time constant, -## so the posture arrives with the clip crossfade instead of a tenth of a second -## ahead of it. -const LEAN_SMOOTH := 4.5 -## Ground locomotion tiers, slowest first, and how far below the promoting -## threshold the speed must fall before dropping back a tier. -const LOCO_TIERS := ["Idle", "Walk", "Run", "Sprint"] -const LOCO_HYSTERESIS := 0.78 -## A locomotion tier is held at least this long before another change is -## allowed. Hard acceleration genuinely passes through walking pace in about a -## fifth of a second, so without this Walk got 0.19 s — less than half of its own -## 0.40 s crossfade — and was cut off mid-blend by Run. Multi-tier jumps still -## happen in one step, so this delays nothing that was not already a blur. -const MIN_TIER_DWELL := 0.22 func _ready() -> void: @@ -193,10 +205,16 @@ func load_model(path: String) -> void: child.queue_free() skeleton = null animation_player = null + _motion_root = null + _wall_run_velocity_world = Vector3.ZERO + _motion_yaw = 0.0 loaded = false _resolved_clips.clear() _current_clip = "" + _current_state_node = "" _weapon_attachment = null + _grapple_attachment = null + _grapple_shoulder_attachment = null # Cleared, not left standing: a model with no skeleton never reaches the # branch that reloads these, and would otherwise be described by the LAST # character's sidecar. @@ -207,7 +225,10 @@ func load_model(path: String) -> void: if not scene: push_warning("SkinnedPlayerModel: failed to load '%s'" % path) return - add_child(scene) + _motion_root = Node3D.new() + _motion_root.name = "MotionOrientation" + add_child(_motion_root) + _motion_root.add_child(scene) if facing_flip: scene.rotation_degrees.y = 180.0 @@ -244,17 +265,22 @@ func load_model(path: String) -> void: _pose_mod.fingers = _rig_info.get("fingers", {}) _pose_mod.name = "ShooterPose" skeleton.add_child(_pose_mod) - # Dance AFTER the shooter pose, so an emote blends over the weapon hold - # instead of fighting it — the arms come off the gun as the dance weight - # rises and are handed back the same way. - _dance_mod = DanceModifier.new() - _dance_mod.name = "Dance" - _dance_mod.roles = _rig_info.get("roles", {}) - skeleton.add_child(_dance_mod) + var grapple_hand := _role_bone( + "hand.L", ["hand.L", "Left wrist", "Left hand"]) + var grapple_shoulder := _role_bone( + "upper_arm.L", ["upper_arm.L", "Left upper arm"]) + if grapple_hand >= 0: + _grapple_attachment = BoneAttachment3D.new() + _grapple_attachment.name = "GrappleHandSocket" + _grapple_attachment.bone_idx = grapple_hand + skeleton.add_child(_grapple_attachment) + if grapple_shoulder >= 0: + _grapple_shoulder_attachment = BoneAttachment3D.new() + _grapple_shoulder_attachment.name = "GrappleShoulderSocket" + _grapple_shoulder_attachment.bone_idx = grapple_shoulder + skeleton.add_child(_grapple_shoulder_attachment) # Cloth and hair last, so the springs react to the FINAL body pose — - # animation plus the shooter lean/slide layer, plus any dance. Hair and - # a skirt following a dance is entirely this ordering; nothing else is - # needed to make it happen. + # authored animation plus the weapon-mechanics layer. if not _rig_info.is_empty(): _spring_mod = SpringBones.new() _spring_mod.name = "SpringBones" @@ -267,7 +293,6 @@ func load_model(path: String) -> void: else: print("SkinnedPlayerModel: '%s' — %d cloth/hair bones on springs" % [path.get_file(), driven]) - # Cel-shaded look: toon shading over the imported textures... LevelMaterials.apply_toon_recursive(scene) # ...then the character-only pass, which is where the surface table earns @@ -291,7 +316,7 @@ func load_model(path: String) -> void: _set_shadows_recursive(self) loaded = true - _play_clip("Idle") + _play_ground_locomotion(0.0, false) ## Collision hulls for the cloth solver, taken from the MESH rather than from @@ -464,19 +489,73 @@ func _index_animations() -> void: ## Runtime blend tree: -## clips -> loco Transition -> TimeScale -> OneShot(upper filter) -> output -## The OneShot's filter holds every upper-body track, so reload/throw/hit -## replace arms+chest only while the locomotion clip keeps owning the legs. +## +## authored cycles -> direction/speed BlendSpaces \ +## authored traversal/action clips ----------------> state Transition +## -> TimeScale +## -> upper OneShot -> output +## +## The ground blend position is the measured horizontal velocity, so a speed +## change affects the legs on this frame. Cyclic sync keeps every active gait on +## the same foot phase while Idle/Walk/Run/Sprint and cardinal strafes blend. +## There is no delayed tier ladder and no procedural leg pose. func _setup_anim_tree(scene: Node) -> void: var bt := AnimationNodeBlendTree.new() - _loco_trans = AnimationNodeTransition.new() - _loco_trans.xfade_time = BLEND_TIME - _loco_trans.allow_transition_to_self = true # lets Land restart itself - bt.add_node("loco", _loco_trans, Vector2(-200, 0)) + _state_trans = AnimationNodeTransition.new() + _state_trans.xfade_time = BLEND_TIME + _state_trans.allow_transition_to_self = true # lets Land restart itself + bt.add_node("state", _state_trans, Vector2(-120, 0)) - # One input per unique clip, named by the clip so transition_request works. + _loco_blend = AnimationNodeBlendSpace2D.new() + _loco_blend.min_space = Vector2(-sprint_anim_reference_speed, + -sprint_anim_reference_speed) + _loco_blend.max_space = Vector2(sprint_anim_reference_speed, + sprint_anim_reference_speed) + _loco_blend.snap = Vector2(0.1, 0.1) + _loco_blend.sync_mode = AnimationNodeBlendSpace2D.SYNC_MODE_CYCLIC_MUTABLE + var ground_points := [ + ["Idle", Vector2.ZERO], + ["Walk", Vector2(0.0, walk_anim_reference_speed)], + ["StrafeWalkForward", Vector2(0.0, strafe_walk_anim_reference_speed)], + ["RunForward", Vector2(0.0, run_anim_reference_speed)], + ["Sprint", Vector2(0.0, sprint_anim_reference_speed)], + ["StrafeWalkBackward", Vector2(0.0, -strafe_walk_anim_reference_speed)], + ["RunBackward", Vector2(0.0, -strafe_run_anim_reference_speed)], + ["StrafeWalkLeft", Vector2(-strafe_walk_anim_reference_speed, 0.0)], + ["RunLeft", Vector2(-strafe_run_anim_reference_speed, 0.0)], + ["StrafeWalkRight", Vector2(strafe_walk_anim_reference_speed, 0.0)], + ["RunRight", Vector2(strafe_run_anim_reference_speed, 0.0)], + ] + for point in ground_points: + var canonical: String = point[0] + if not _resolved_clips.has(canonical): + continue + var anim_node := AnimationNodeAnimation.new() + anim_node.animation = _resolved_clips[canonical] + _loco_blend.add_blend_point(anim_node, point[1], -1, canonical) + bt.add_node("loco_blend", _loco_blend, Vector2(-520, -100)) + _state_trans.add_input("Locomotion") + bt.connect_node("state", 0, "loco_blend") + + _crouch_blend = AnimationNodeBlendSpace1D.new() + _crouch_blend.min_space = 0.0 + _crouch_blend.max_space = crouch_anim_reference_speed + _crouch_blend.snap = 0.1 + _crouch_blend.sync_mode = AnimationNodeBlendSpace1D.SYNC_MODE_CYCLIC_MUTABLE + for point in [["Crouch", 0.0], ["CrouchWalk", crouch_anim_reference_speed]]: + var canonical: String = point[0] + if not _resolved_clips.has(canonical): + continue + var anim_node := AnimationNodeAnimation.new() + anim_node.animation = _resolved_clips[canonical] + _crouch_blend.add_blend_point(anim_node, point[1], -1, canonical) + bt.add_node("crouch_blend", _crouch_blend, Vector2(-520, 80)) + _state_trans.add_input("CrouchLocomotion") + bt.connect_node("state", 1, "crouch_blend") + + # Direct inputs preserve authored traversal and debug clip playback. var seen := {} - var idx := 0 + var idx := 2 for canonical in _resolved_clips: var clip_name: String = _resolved_clips[canonical] if seen.has(clip_name): @@ -486,13 +565,13 @@ func _setup_anim_tree(scene: Node) -> void: an.animation = clip_name var node_id := "clip_%d" % idx bt.add_node(node_id, an, Vector2(-500, idx * 60)) - _loco_trans.add_input(clip_name) - bt.connect_node("loco", idx, node_id) + _state_trans.add_input(clip_name) + bt.connect_node("state", idx, node_id) idx += 1 var ts := AnimationNodeTimeScale.new() bt.add_node("loco_scale", ts, Vector2(0, 0)) - bt.connect_node("loco_scale", 0, "loco") + bt.connect_node("loco_scale", 0, "state") var upper := AnimationNodeOneShot.new() # The upper-body one-shot pops in and out over the locomotion clip, so its @@ -564,14 +643,8 @@ func _set_shadow_mode_recursive(node: Node, mode: int) -> void: var _prev_state: String = "" var _oneshot_lock: float = 0.0 # seconds left where a one-shot owns playback var _dancing: bool = false -## Which of DanceRoutines.ROUTINES is playing, and the eased 0..1 blend of the -## dance layer over everything below it. +## Which authored entry in DanceRoutines.ROUTINES is playing. var _dance_index: int = 0 -var _cur_dance: float = 0.0 -## How fast a dance takes the body and gives it back. Slower than the pose -## layer's other blends on purpose: an emote starting is not a reaction, and -## snapping into one looks like a bug rather than like a decision. -const DANCE_SMOOTH := 5.0 ## Play a one-shot clip over locomotion for `lock_time` seconds. @@ -597,15 +670,20 @@ func play_oneshot(canonical: String, lock_time: float = 0.35) -> void: ## Emote toggle (Dance). Shown while grounded and near-idle; any real ## movement breaks it (the controller clears the flag too). ## -## `which` selects one of the five routines in DanceRoutines. The base clip stays -## the library's single `Dance_Loop` underneath — the routine is layered over it -## by DanceModifier, which is what makes five distinct emotes out of one clip. +## `which` selects one of five separately authored library clips. No runtime +## bone oscillation or procedural emote modifier is involved. func set_dancing(on: bool, which: int = -1) -> void: _dancing = on if which >= 0: _dance_index = which - if _dance_mod: - _dance_mod.routine = DanceRoutines.get_routine(_dance_index) + # The authored emotes own the full body. Leaving the live weapon attached + # makes otherwise-correct gestures drive a rifle through the face and chest. + # Hide only the third-person prop; the gameplay loadout remains unchanged + # and comes back immediately when the emote ends. + if _weapon_attachment and _weapon_attachment.get_child_count() > 0: + var held_weapon := _weapon_attachment.get_child(0) as Node3D + if held_weapon: + held_weapon.visible = not on ## Play a named gameplay action (reload / throw / shoot) as a one-shot. @@ -630,7 +708,6 @@ func add_gun_recoil(strength: float = 1.0) -> void: ## Same contract as HumanoidModel.update_state(). Called by the movement ## controller each frame with either local or network-synced state. func update_state(state: String, speed: float, is_crouching: bool = false) -> void: - _speed = speed if not loaded or not animation_player: return @@ -653,61 +730,57 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo match state: "ground", "idle": if _dancing and speed < 0.5 and not is_crouching: - clip = "Dance" - elif is_crouching: - clip = "CrouchWalk" if speed > 0.5 else "Crouch" + clip = DanceRoutines.clip_of(_dance_index) else: - clip = _loco_clip(speed) - # Armed idle uses the plain Idle clip — the rifle-hold pose layer - # owns the arms, so the odd arms-crossed PistolIdle base reads worse. + _play_ground_locomotion(speed, is_crouching) + clip = "" "air": # Rising = jump, falling = the fall loop. clip = "Jump" if _vertical_speed() > 0.5 else "Fall" "slide": clip = "Slide" "wall_run": - clip = "WallRun" - "wall_cling", "wall_climb": + # Separate authored performances: the contact leg and balancing arm + # are different on each side. This is never a renamed ground run. + clip = "WallRunRight" if _target_wall > 0.0 else "WallRunLeft" + "wall_cling": clip = "WallCling" + "wall_climb": + clip = "WallClimb" "grapple": - # No canned clip — the zip pose is procedural (body aligned to the - # line, free arm reaching). Fall gives the legs a natural airborne - # base instead of the library's horizontal swim. - clip = "Fall" + clip = "Grapple" "dash": clip = "Dash" "death": clip = "Death" - _play_clip(clip) + if clip != "": + _play_clip(clip) if _pose_mod: _pose_mod.state = state - _pose_mod.weapon_held = is_holding_weapon + if state != "grapple": + _pose_mod.grapple_active = false - # Scale locomotion playback so feet keep up with actual movement speed. - if _anim_tree: - var s := 1.0 - var is_loco := false - match clip: - "Walk", "CrouchWalk": - s = clampf(speed / walk_anim_reference_speed, 0.7, 1.6) - is_loco = true - "Run", "Sprint", "WallRun": - s = clampf(speed / run_anim_reference_speed, 0.7, 1.8) - is_loco = true - # Backpedalling: run the cycle BACKWARDS rather than moon-walking with - # the forward clip. The shared library has no authored reverse run, and - # a reversed stride reads correctly for a backpedal. The hip yaw in - # _update_travel picks the regime and points the legs to match, so the - # two must agree — reading a different threshold here used to leave a - # band where the feet ran one way and pointed the other. - if is_loco and _travel_reverse: - s = -s - _anim_tree.set("parameters/loco_scale/scale", s) + # Traversal clips are authored loops too. Above their reference velocity, + # time-scale the whole cycle so their planted foot/hand cadence still tracks + # gameplay speed; never synthesize a different pose. + if _anim_tree and clip != "": + var scale := 1.0 + if clip == "Slide": + scale = clampf(speed / run_anim_reference_speed, 0.75, 1.8) + elif clip in ["WallRunLeft", "WallRunRight"]: + scale = clampf(speed / wall_run_anim_reference_speed, 0.85, 1.65) + elif clip == "Grapple": + # Grapple is a held, authored flight pose. Its cadence does not + # represent footsteps and must not accelerate with pull velocity. + scale = 1.0 + elif clip == "Dash": + scale = clampf(speed / 13.0, 0.75, 1.8) + _anim_tree.set("parameters/loco_scale/scale", scale) -## Drives the procedural pose layer. Called by the controller each frame. +## Drives the authored direction BlendSpace and live ADS mechanic. ## strafe: -1 (moving left) .. +1 (moving right), relative to facing ## fwd: -1 (moving back) .. +1 (moving forward), relative to facing ## ads: 0 (hip) .. 1 (aiming down sights) @@ -718,16 +791,79 @@ func set_locomotion(strafe: float, fwd: float, ads: float) -> void: ## Wall side during a wall run: -1 wall on left, +1 wall on right, 0 none. -## Drives a whole-body lean into the wall. +## Used only to release the wall-side weapon hand; the clip owns body motion. func set_wall_side(side: float) -> void: _target_wall = clampf(side, -1.0, 1.0) -## World-space point the grapple hook is anchored to. While the movement state -## is "grapple" the pose layer aligns the body along the line to this point -## and reaches the free hand toward it. -func set_grapple_target(point_world: Vector3) -> void: - _grapple_point_world = point_world +## Gameplay velocity for aligning the authored lateral wall-run performance. +## Uisco supplies the body lean, contact leg, arm balance, and cadence. This +## supplies only the wall-tangent coordinate frame that an offline clip cannot +## know. +func set_wall_run_motion(velocity_world: Vector3) -> void: + _wall_run_velocity_world = velocity_world + + +## Regression telemetry: the authored model's forward direction in world space. +func wall_run_forward_debug() -> Vector3: + if not is_instance_valid(_motion_root): + return Vector3.ZERO + var source_forward := Vector3.FORWARD if facing_flip else Vector3.BACK + return ( + global_transform.basis * _motion_root.transform.basis * source_forward + ).normalized() + + +## Live grapple mechanics layered over the authored airborne performance. +## The clip supplies weight and secondary motion; gameplay supplies the one +## constraint an authored fixed pose cannot know: the actual cable direction. +func set_grapple_target(point_world: Vector3, + velocity_world: Vector3 = Vector3.ZERO) -> void: + if not _pose_mod: + return + _pose_mod.grapple_target_world = point_world + _pose_mod.grapple_velocity_world = velocity_world + _pose_mod.grapple_active = true + + +## Cable socket after animation + grapple IK. BoneAttachment3D observes the +## final modified skeleton pose (unlike a raw pose query made after the +## modifier pass, which has already been restored for the next animation tick). +func get_grapple_hand_world_position() -> Vector3: + if is_instance_valid(_grapple_attachment): + return _grapple_attachment.global_position + if not skeleton: + return global_position + var hand := _role_bone("hand.L", ["hand.L", "Left wrist", "Left hand"]) + if hand < 0: + return global_position + return (skeleton.global_transform * skeleton.get_bone_global_pose(hand)).origin + + +## Test/lab telemetry: hand-to-line error in metres and the physical body +## alignment factor (0 hanging under gravity, 1 aligned into the rope). +func grapple_debug() -> Dictionary: + if not _pose_mod or not skeleton: + return {} + var shoulder_world := _grapple_shoulder_attachment.global_position \ + if is_instance_valid(_grapple_shoulder_attachment) \ + else global_position + var hand_world := get_grapple_hand_world_position() + var line := _pose_mod.grapple_target_world - shoulder_world + var reach := hand_world - shoulder_world + var error := 0.0 + if line.length_squared() > 0.000001: + error = reach.cross(line.normalized()).length() + return { + "line_error": error, + "alignment": _pose_mod.grapple_alignment, + "active": _pose_mod.grapple_active, + "body_angle": _pose_mod._grapple_body.angle_to(Quaternion.IDENTITY), + "body_rotation": _pose_mod._grapple_body, + "shoulder": shoulder_world, + "hand": hand_world, + "target": _pose_mod.grapple_target_world, + } ## How far away each cloth detail level starts, in metres. See SpringBones.lod — @@ -739,44 +875,16 @@ var _lod_timer: float = 0.0 func _process(delta: float) -> void: _update_cloth_lod(delta) + _update_motion_orientation(delta) if not _pose_mod: return _hold_weapon_still() var t := 1.0 - exp(-POSE_SMOOTH * delta) - # The body lean gets its own, much slower rate, and is scaled by how fast the - # character is ACTUALLY moving rather than by which key is held. - # - # The controller passes a normalised input direction, so `fwd` jumps 0 -> 1 - # the instant W is pressed. At the shared rate that planted the full forward - # lean in about a tenth of a second while the Idle->Run crossfade was still - # 0.4 s from finishing — the body snapped into a run posture ahead of the run - # cycle. Tying it to speed means the lean now grows as the character - # accelerates, and lands with the clip. - var lean_t := 1.0 - exp(-LEAN_SMOOTH * delta) - var drive: float = clampf(_speed / maxf(run_anim_reference_speed, 0.01), 0.0, 1.0) - _cur_strafe = lerpf(_cur_strafe, _target_strafe * drive, lean_t) - _cur_fwd = lerpf(_cur_fwd, _target_fwd * drive, lean_t) _cur_ads = lerpf(_cur_ads, _target_ads, t) - var slide_target := 1.0 if _pose_mod.state == "slide" else 0.0 - _cur_slide = lerpf(_cur_slide, slide_target, t) var wall_target := _target_wall if _pose_mod.state == "wall_run" else 0.0 - _cur_wall = lerpf(_cur_wall, wall_target, lean_t) - # The dance blend. Eased both ways, so an emote arrives and leaves rather - # than cutting — and so the arms come off the weapon smoothly. - if _dance_mod: - _cur_dance = lerpf(_cur_dance, 1.0 if _dancing else 0.0, - 1.0 - exp(-DANCE_SMOOTH * delta)) - _dance_mod.weight = _cur_dance - _update_travel(delta, drive) - _pose_mod.strafe = _cur_strafe - _pose_mod.fwd = _cur_fwd - _pose_mod.travel_yaw = _cur_travel + _cur_wall = lerpf(_cur_wall, wall_target, t) _pose_mod.ads = _cur_ads - _pose_mod.slide = _cur_slide - _pose_mod.wall = _cur_wall - # Grapple zip pose: blend in while grappling; feed the modifier the - # direction to the anchor in skeleton space. if _upper_lock > 0.0: _upper_lock -= delta if _upper_lock <= 0.0: @@ -788,16 +896,6 @@ func _process(delta: float) -> void: if _anim_tree: _anim_tree.set("parameters/upper/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FADE_OUT) - var grapple_target := 1.0 if _pose_mod.state == "grapple" else 0.0 - _cur_grapple = lerpf(_cur_grapple, grapple_target, t) - _pose_mod.grapple = _cur_grapple - if _cur_grapple > 0.01 and skeleton and _grapple_point_world != Vector3.ZERO: - var chest := skeleton.global_transform.origin + Vector3.UP * 1.2 - var dir_world := _grapple_point_world - chest - if dir_world.length_squared() > 0.01: - _pose_mod.grapple_dir = \ - (skeleton.global_transform.basis.inverse() * dir_world).normalized() - # Two-hand rifle hold: owns the arms whenever a weapon is held, EXCEPT when # a one-shot clip (reload/throw/hit) or a full-body moment (Land lock, # dance, death) needs the authored animation to read through. The upper @@ -845,54 +943,22 @@ func _process(delta: float) -> void: _pose_mod.reload_phase = rl_target -## Which way the LEGS should point, and whether the stride runs backwards. -## -## The shared clip library has one forward locomotion cycle and no strafe or -## backpedal clips, so a character sidestepping used to run forwards on the spot -## while sliding sideways — nothing in the animation said which way they were -## going, and a body lean was carrying the whole burden of telling the player. -## -## Turning the HIPS toward the travel direction is what actually says it, and it -## costs no new animation: the legs are children of the hips, so the whole stride -## turns with them, while the spine counter-rotates so the chest and the gun stay -## on the aim. It is the same split every third-person shooter uses, and the same -## one Hoyoverse's locked-on locomotion uses. -## -## Beyond about a right angle the hips cannot follow, so the cycle plays in -## REVERSE and the legs point the other way instead — a real backpedal rather -## than a moonwalk. Which regime is in force is hysteretic, and the yaw is eased -## rather than snapped, so switching between them reads as a pivot on the spot, -## which is what a person actually does there. -## The most the hips may turn away from where the character is facing. -## -## A right angle would point the legs exactly along a sidestep, but a person -## sidestepping does not stand with their hips square to their path — they open -## maybe half that and let the feet cross. Past this the silhouette stops reading -## as a shooter holding an aim and starts reading as someone who has turned round. -const MAX_TRAVEL_YAW := 0.95 -const TRAVEL_SMOOTH := 9.0 -const TRAVEL_REVERSE_IN := -0.35 -const TRAVEL_REVERSE_OUT := -0.12 - -func _update_travel(delta: float, drive: float) -> void: - var st: float = _target_strafe - var fw: float = _target_fwd - if _travel_reverse: - if fw > TRAVEL_REVERSE_OUT: - _travel_reverse = false - elif fw < TRAVEL_REVERSE_IN: - _travel_reverse = true - var want := 0.0 - if absf(st) > 0.01 or absf(fw) > 0.01: - # Skeleton space: the character faces +Z and character-right is -X, so a - # travel direction of (strafe right, forward) is (-strafe, 0, fwd). A yaw - # of `want` about +Y points the legs along it — or along the opposite of - # it when the stride is running backwards. - want = atan2(st, -fw) if _travel_reverse else atan2(-st, fw) - want = clampf(want, -MAX_TRAVEL_YAW, MAX_TRAVEL_YAW) * drive - # Shortest way round, so a pivot never takes the long route. - var d := wrapf(want - _cur_travel, -PI, PI) - _cur_travel += d * (1.0 - exp(-TRAVEL_SMOOTH * delta)) +func _update_motion_orientation(delta: float) -> void: + if not is_instance_valid(_motion_root): + return + var wanted_yaw := 0.0 + if _pose_mod and _pose_mod.state == "wall_run": + var local_velocity := global_transform.basis.inverse() \ + * _wall_run_velocity_world + local_velocity.y = 0.0 + if local_velocity.length_squared() > 0.01: + var source_forward := Vector3.FORWARD \ + if facing_flip else Vector3.BACK + wanted_yaw = source_forward.signed_angle_to( + local_velocity.normalized(), Vector3.UP) + var blend := 1.0 - exp(-14.0 * delta) + _motion_yaw = lerp_angle(_motion_yaw, wanted_yaw, blend) + _motion_root.rotation.y = _motion_yaw ## Pick the cloth solver's detail level from how far the camera is. @@ -918,41 +984,138 @@ func _update_cloth_lod(delta: float) -> void: _spring_mod.lod = want -## Locomotion clip for a ground speed, with HYSTERESIS. +## Drive authored ground cycles directly from the current velocity. ## -## The thresholds used to be a bare elif chain, so a character accelerating from -## a standstill crossed all three in under a second and each crossfade cut off -## the one before it — and any speed hovering on a boundary flickered between -## two clips forever. Dropping back down needs the speed to fall well under the -## threshold that promoted it, so a tier, once entered, is committed to. -func _loco_clip(speed: float) -> String: - _tier_age += get_process_delta_time() - var up := [0.5, walk_anim_reference_speed * 1.2, run_anim_reference_speed * 1.35] - var want := _loco_tier - while want < LOCO_TIERS.size() - 1 and speed > up[want]: - want += 1 - while want > 0 and speed < up[want - 1] * LOCO_HYSTERESIS: - want -= 1 - if want != _loco_tier and _tier_age >= MIN_TIER_DWELL: - _loco_tier = want - _tier_age = 0.0 - return LOCO_TIERS[_loco_tier] +## The old tier ladder could spend 0.34 s in Idle and another 0.34 s in Walk +## even though gameplay acceleration reached running speed in one physics frame. +## That is the visible "sliding, then slow walk, then run" failure. Here the +## BlendSpace position is physical metres/second on this call, so the feet react +## on the same frame as the body. +func _play_ground_locomotion(speed: float, crouched: bool) -> void: + if not _anim_tree: + return + var state_node := "CrouchLocomotion" if crouched else "Locomotion" + if _current_state_node != state_node: + _state_trans.xfade_time = 0.16 + _anim_tree.set("parameters/state/transition_request", state_node) + _current_state_node = state_node + + if crouched: + var authored_speed := minf(speed, crouch_anim_reference_speed) + _anim_tree.set("parameters/crouch_blend/blend_position", authored_speed) + _anim_tree.set("parameters/loco_scale/scale", + maxf(1.0, speed / maxf(crouch_anim_reference_speed, 0.01))) + _current_clip = _resolved_clips.get( + "CrouchWalk" if speed > 0.1 else "Crouch", "") + return + + var direction := Vector2(_target_strafe, _target_fwd) + if direction.length_squared() < 0.0001: + direction = Vector2(0.0, 1.0) + else: + direction = direction.normalized() + + # Each axis is positioned at its measured source velocity. The edge of the + # BlendSpace is therefore a velocity envelope, not an arbitrary circle: + # solve where this heading intersects the line between its two neighboring + # authored clips. Above that speed, TimeScale advances the authored cycle + # proportionally instead of letting the mesh slide over planted feet. + var forward_limit := sprint_anim_reference_speed \ + if direction.y > 0.0 else strafe_run_anim_reference_speed + var inverse_limit := ( + absf(direction.x) / maxf(strafe_run_anim_reference_speed, 0.01) + + absf(direction.y) / maxf(forward_limit, 0.01) + ) + var authored_limit := 1.0 / maxf(inverse_limit, 0.01) + var authored_speed := minf(speed, authored_limit) + _anim_tree.set("parameters/loco_blend/blend_position", + direction * authored_speed) + _anim_tree.set("parameters/loco_scale/scale", + maxf(1.0, speed / maxf(authored_limit, 0.01))) + + var canonical := "Idle" + if speed > 0.1: + if absf(direction.x) > absf(direction.y): + canonical = "RunRight" if direction.x > 0.0 else "RunLeft" + elif direction.y < 0.0: + canonical = "RunBackward" + elif speed < (walk_anim_reference_speed + run_anim_reference_speed) * 0.5: + canonical = "Walk" + elif speed < (run_anim_reference_speed + sprint_anim_reference_speed) * 0.5: + canonical = "RunForward" + else: + canonical = "Sprint" + _current_clip = _resolved_clips.get(canonical, "") func _play_clip(canonical: String, restart: bool = false) -> void: if not _anim_tree or not _resolved_clips.has(canonical): return var clip_name: String = _resolved_clips[canonical] - if not restart and _current_clip == clip_name: + if not restart and _current_state_node == clip_name: return - _loco_trans.xfade_time = BLEND_TIMES.get(canonical, BLEND_TIME) - _anim_tree.set("parameters/loco/transition_request", clip_name) + _state_trans.xfade_time = BLEND_TIMES.get(canonical, BLEND_TIME) + _anim_tree.set("parameters/state/transition_request", clip_name) + _current_state_node = clip_name _current_clip = clip_name -## Current smoothed forward lean, 0..1. For debug/transition_check.gd. +## Procedural locomotion lean was removed; kept for regression compatibility. func get_lean_debug() -> float: - return _cur_fwd + return 0.0 + + +func get_brake_debug() -> float: + return 0.0 + + +## Physical velocity currently driving the authored ground BlendSpace. +func locomotion_blend_debug() -> Vector2: + if not _anim_tree: + return Vector2.ZERO + var value = _anim_tree.get("parameters/loco_blend/blend_position") + return value if value is Vector2 else Vector2.ZERO + + +## Velocity represented by the authored stride after playback-rate calibration. +## The raw BlendSpace position stops at the source clip's measured velocity; +## TimeScale covers faster gameplay without changing the pose or foot phase. +func locomotion_effective_speed_debug() -> float: + if not _anim_tree: + return 0.0 + var scale_value = _anim_tree.get("parameters/loco_scale/scale") + var scale := float(scale_value) if scale_value != null else 1.0 + return locomotion_blend_debug().length() * scale + + +## Animated world-space anchors for the two shoulder and two foot jets. +## Offsets use the skeleton frame, not each limb's twisting frame, so the +## nozzles stay on the character's back/heels through every authored clip. +func get_jet_socket_world_positions() -> Array[Vector3]: + if not skeleton: + return [] + var roles_and_fallbacks := [ + ["shoulder.L", ["shoulder.L", "upper_arm.L", "Left shoulder"]], + ["shoulder.R", ["shoulder.R", "upper_arm.R", "Right shoulder"]], + ["foot.L", ["foot.L", "Left ankle", "Left foot"]], + ["foot.R", ["foot.R", "Right ankle", "Right foot"]], + ] + var sockets: Array[Vector3] = [] + for i in roles_and_fallbacks.size(): + var spec: Array = roles_and_fallbacks[i] + var bone := _role_bone(spec[0], spec[1]) + if bone < 0: + return [] + var world_pose := skeleton.global_transform * skeleton.get_bone_global_pose(bone) + # The shoulder pair sits just outside the back silhouette. This extra + # clearance matters for long-haired skins: mounting at the literal joint + # leaves both flames completely buried in the hair mesh. + var side := 0.10 if i == 0 else -0.10 + var offset := skeleton.global_transform.basis \ + * (Vector3(side, -0.03, -0.30) if i < 2 \ + else Vector3(0.0, 0.035, -0.045)) + sockets.append(world_pose.origin + offset) + return sockets ## Every canonical clip this character resolved to something real. @@ -1029,10 +1192,9 @@ func _reseat_weapon() -> void: _measure_weapon(w) -## Whether the locomotion cycle is running backwards, which points the stride -## the opposite way to the hips. For debug/travel_dir_check.gd. +## Compatibility probe: authored direction clips always play forward. func stride_reversed_debug() -> bool: - return _travel_reverse + return false ## Clip currently playing. For debug/transition_check.gd. @@ -1314,36 +1476,30 @@ func _set_shadows_recursive(node: Node) -> void: _set_shadows_recursive(child) -# ── Procedural shooter pose layer ───────────────────────────────────────────── +# ── Mechanical shooter pose layer ───────────────────────────────────────────── # -# Runs after the AnimationPlayer each frame and layers shooter-feel poses on top -# of the base clip: lean into the movement direction, a slide that leans back and -# looks forward, and an always-held weapon that raises to ADS. All rotations are -# authored in the skeleton's own space (forward = +Z, up = +Y, character-right = -# -X) and converted into each bone's local pose, so they read intuitively. +# Authored clips own body motion. This modifier handles only mechanics that must +# follow live gameplay data: aim pitch, weapon IK, cheek weld, finger grip, and +# recoil. class ShooterPoseModifier extends SkeletonModifier3D: # Inputs, written by the owning SkinnedPlayerModel each frame. - var strafe: float = 0.0 # -1 left .. +1 right - var fwd: float = 0.0 # -1 back .. +1 forward var ads: float = 0.0 # 0 hip .. 1 aiming - var slide: float = 0.0 # 0 .. 1 slide blend - var wall: float = 0.0 # -1 wall left .. +1 wall right (wall-run lean) - # Yaw of the LOWER body about the character's up axis, in radians — which way - # the legs are actually travelling. See SkinnedPlayerModel._update_travel. - var travel_yaw: float = 0.0 var aim_pitch: float = 0.0 # radians, up positive — upper body follows aim var recoil: float = 0.0 # decaying shot kick var state: String = "idle" - var weapon_held: bool = false - # Grapple zip: 0..1 blend + skeleton-space direction to the anchor point. - var grapple: float = 0.0 - var grapple_dir: Vector3 = Vector3.ZERO + # Grapple target/velocity arrive in world space from the movement state. + # They are converted to skeleton space at solve time, after the authored + # animation has placed the shoulders. + var grapple_active: bool = false + var grapple_target_world: Vector3 = Vector3.ZERO + var grapple_velocity_world: Vector3 = Vector3.ZERO + var grapple_alignment: float = 0.0 + var _grapple_body: Quaternion = Quaternion.IDENTITY # Per-arm rifle-hold weights (0 = clip owns the arm, 1 = hold pose owns it). var hold_r_target: float = 0.0 var hold_l_target: float = 0.0 var _hold_r: float = 0.0 var _hold_l: float = 0.0 - var _time: float = 0.0 # The attached gun's forward/up axes in hand-bone space (set on set_weapon). var gun_fwd_hand: Vector3 = Vector3.ZERO var gun_up_hand: Vector3 = Vector3.UP @@ -1366,23 +1522,6 @@ class ShooterPoseModifier extends SkeletonModifier3D: # [child_bone, helper_bone] pairs; see SkinJointHelper. var joint_helpers: Array = [] - # Tuning (radians). Positive pitch leans forward; positive roll leans right. - # The lean is the ONLY thing that tells a viewer which way this character is - # travelling — the library has one forward locomotion cycle and no strafe - # clips — so it has to be legible, not subtle. - ## How much of the hip yaw the spine takes back, so the chest, the head and - ## the gun stay pointed where the player is aiming. Not all of it: a real - ## torso does follow the hips a little, and countering the whole thing makes - ## the waist look broken. - const TRAVEL_COUNTER := 0.82 - const LEAN_ROLL := 0.42 - const LEAN_PITCH := 0.30 - const SLIDE_BACK := 0.75 # torso lean-back during slide - const SLIDE_HEAD_UP := 0.7 # head pitch to keep looking forward - const SLIDE_LEG_FWD := 0.95 # thighs swing forward so feet lead the slide - const SLIDE_KNEE := 0.55 # shins straighten against the crouch clip's bend - const WALL_PITCH := 0.2 # forward drive lean during a wall run - const WALL_ARM_OUT := 0.9 # inner arm reaches out to touch the wall const HOLD_SMOOTH := 8.0 # how fast the hold takes/releases the arms ## The library skeleton's spine, hips first. Shared with the dance layer via @@ -1466,9 +1605,8 @@ class ShooterPoseModifier extends SkeletonModifier3D: # The canonical names are the LIBRARY skeleton's, and a model that kept # its own rig names things differently — Taila's hips are DEF-spine, her # head is DEF-spine.006, and she has no bone with "neck" in its name at - # all. Unresolved, every lean, aim pitch and slide head-lift below - # silently did nothing. RigRoles maps them through the sidecar; the dance - # layer uses the same call rather than a second copy of it. + # all. RigRoles maps weapon IK and aim onto those authored names through + # the sidecar rather than imposing one skeleton naming convention. _idx = RigRoles.resolve(skel, roles) _resolve_hands(skel) _resolved = true @@ -1546,20 +1684,15 @@ class ShooterPoseModifier extends SkeletonModifier3D: _resolve() var delta := get_physics_process_delta_time() if Engine.is_in_physics_frame() \ else get_process_delta_time() - _time += delta var t := 1.0 - exp(-HOLD_SMOOTH * delta) _hold_r = lerpf(_hold_r, hold_r_target, t) _hold_l = lerpf(_hold_l, hold_l_target, t) - _apply_lean(skel) - if absf(aim_pitch) > 0.01: + # The authored clip owns cadence and weight. Grappling adds the physical + # pendulum frame before the arm constraint is solved. + _apply_grapple_body(skel, delta) + if absf(aim_pitch) > 0.01 and not grapple_active: _apply_aim_pitch(skel) - if slide > 0.01: - _apply_slide(skel) - if absf(wall) > 0.01: - _apply_wall_lean(skel) - if grapple > 0.01 and grapple_dir != Vector3.ZERO: - _apply_grapple(skel) if _hold_r > 0.01 or _hold_l > 0.01: _apply_rifle_hold(skel) if absf(cheek) > 0.01: @@ -1568,6 +1701,8 @@ class ShooterPoseModifier extends SkeletonModifier3D: if recoil > 0.01: _apply_recoil(skel) recoil = lerpf(recoil, 0.0, 0.25) + if grapple_active: + _apply_grapple_arm(skel) # Joints LAST, and inside the modification pass: each helper has to track # whatever final rotation its child bone ended up with, or it deforms the @@ -1576,6 +1711,108 @@ class ShooterPoseModifier extends SkeletonModifier3D: SkinJointHelper.update(skel, joint_helpers) + ## Rotate the visual body as a pendulum without touching the gameplay + ## CharacterBody/capsule. At low energy the pilot hangs upright under + ## gravity. Speed and angular velocity progressively align the body's long + ## axis with the rope, while tangential velocity controls facing around it. + func _apply_grapple_body(skel: Skeleton3D, delta: float) -> void: + var wanted := Quaternion.IDENTITY + grapple_alignment = 0.0 + if grapple_active: + var ua: int = _idx.get("DEF-upper_arm.L", -1) + if ua >= 0: + var inv := skel.global_transform.affine_inverse() + var target := inv * grapple_target_world + var shoulder := skel.get_bone_global_pose(ua).origin + var rope := target - shoulder + var rope_length := rope.length() + if rope_length > 0.01: + var rope_dir := rope / rope_length + var velocity := skel.global_transform.basis.inverse() \ + * grapple_velocity_world + var tangent := velocity - rope_dir * velocity.dot(rope_dir) + var angular_speed := tangent.length() / rope_length + # Whole-body cable alignment is a high-energy silhouette. + # Below 11 m/s the authored hanging pose remains dominant; + # angular motion can shape a fast swing but cannot bypass + # the speed gate by itself. + var speed_factor := smoothstep( + 11.0, 28.0, velocity.length()) + var angular_factor := smoothstep( + 0.8, 2.8, angular_speed) + grapple_alignment = speed_factor * lerpf( + 0.55, 1.0, angular_factor) + + var up := Vector3.UP.slerp( + rope_dir, grapple_alignment).normalized() + var tilt := Quaternion(Vector3.UP, up) + var forward := tangent - up * tangent.dot(up) + if forward.length_squared() < 0.0001: + forward = tilt * Vector3(0.0, 0.0, 1.0) + else: + forward = forward.normalized() + var base_forward := tilt * Vector3(0.0, 0.0, 1.0) + base_forward = ( + base_forward - up * base_forward.dot(up) + ).normalized() + var twist := base_forward.signed_angle_to(forward, up) + wanted = Quaternion(up, twist) * tilt + + var blend := 1.0 - exp(-7.0 * delta) + _grapple_body = _grapple_body.slerp(wanted.normalized(), blend) + if _grapple_body.angle_to(Quaternion.IDENTITY) > 0.001: + _add_space( + skel, _idx.get("DEF-hips", -1), _grapple_body) + + + ## Exact live two-bone constraint for the grapple arm. The target hand point + ## is the farthest reachable point on the cable ray, so shoulder, hand and + ## hook are collinear even when the hook is many metres away. + func _apply_grapple_arm(skel: Skeleton3D) -> void: + var ua: int = _idx.get("DEF-upper_arm.L", -1) + var hand: int = _idx.get("DEF-hand.L", -1) + if ua < 0 or hand < 0: + return + var inv := skel.global_transform.affine_inverse() + var target := inv * grapple_target_world + var shoulder := skel.get_bone_global_pose(ua).origin + var line := target - shoulder + if line.length_squared() < 0.0001: + return + var along := line.normalized() + var reach := _arm_reach( + skel, "DEF-upper_arm.L", "DEF-forearm.L", "DEF-hand.L") + var hand_target := shoulder + along * reach * 0.985 + var gravity_down := skel.global_transform.basis.inverse() * Vector3.DOWN + var pole := gravity_down + Vector3(0.35, 0.0, 0.0) + var g_fore := _ik_arm( + skel, + "DEF-upper_arm.L", + "DEF-forearm.L", + "DEF-hand.L", + hand_target, + pole.normalized(), + 1.0, + ) + + # Continue the hand itself down the same ray. This puts the palm socket, + # not merely the wrist joint, on the cable line. + if g_fore != Quaternion.IDENTITY and _hand_frame.has("L"): + var palm := gravity_down - along * gravity_down.dot(along) + if palm.length_squared() < 0.0001: + palm = Vector3.RIGHT - along * Vector3.RIGHT.dot(along) + palm = palm.normalized() + var curl := along.cross(palm).normalized() + var want := Basis(along, palm, curl) + var rest: Basis = _hand_frame["L"] + var g_hand := ( + want * rest.inverse() + ).get_rotation_quaternion() \ + * skel.get_bone_global_rest(hand).basis.get_rotation_quaternion() + _set_global_rot(skel, hand, g_fore, g_hand.normalized(), 1.0) + _close_hand(skel, "L", 1.0) + + # Upper body follows the camera pitch: distributed over spine/neck/head # so looking up/down reads on the whole silhouette, not just the head. func _apply_aim_pitch(skel: Skeleton3D) -> void: @@ -1599,126 +1836,6 @@ class ShooterPoseModifier extends SkeletonModifier3D: for n in ["DEF-spine.002", "DEF-spine.003"]: _add_space(skel, _idx.get(n, -1), back) - # Turn the lower body to face the way the character is travelling, and lean - # along that direction rather than along the facing. - # - # The legs hang off the hips, so yawing the hips turns the whole stride — the - # one thing that makes a sidestep look like a sidestep when the clip library - # has only a forward run. The spine takes most of it back so the chest and the - # gun stay on the aim. - # - # The LEAN has to move into the travel frame with it. Leaning "forward" along - # the character's facing while the legs run off to one side leans them - # sideways relative to their own stride, which is exactly the sensation of a - # character being dragged rather than running. - func _apply_lean(skel: Skeleton3D) -> void: - var yaw := travel_yaw * (1.0 - slide) - # How hard the character is driving, regardless of which way. Signed - # `fwd` is wrong now that the legs turn: a sidestep has fwd near zero and - # should still lean into its own stride. - var effort := clampf(Vector2(strafe, fwd).length(), 0.0, 1.0) - var pitch := effort * LEAN_PITCH * (1.0 - slide) - if absf(pitch) < 0.001 and absf(yaw) < 0.001: - return - var turn := Quaternion(Vector3(0, 1, 0), yaw) - # Pitch about the axis ACROSS the direction of travel, not across the - # facing — see the comment above the function. - var lean := Quaternion((turn * Vector3(1, 0, 0)).normalized(), pitch) - # The hips carry the yaw, and a little of the lean. - _add_space(skel, _idx.get("DEF-hips", -1), - turn * Quaternion.IDENTITY.slerp(lean, 0.25)) - # The torso unwinds the yaw and takes the rest of the lean. - var torso: Array = SPINE.slice(1) - var n := maxf(torso.size(), 1) - var back := Quaternion(Vector3(0, 1, 0), -yaw * TRAVEL_COUNTER) - var per_yaw := Quaternion.IDENTITY.slerp(back, 1.0 / n) - var per_lean := Quaternion.IDENTITY.slerp(lean, 0.75 / n) - for b in torso: - _add_space(skel, _idx.get(b, -1), per_yaw * per_lean) - - # Grapple zip: the whole body pivots to fly along the line to the anchor, - # legs trail behind, and the FREE (left) hand reaches up the rope — the - # right hand keeps the rifle (the hold layer runs after this and owns it). - func _apply_grapple(skel: Skeleton3D) -> void: - var d := grapple_dir.normalized() - var fwd := Vector3(0, 0, 1) - if d.dot(fwd) > 0.999: - d = (d + Vector3(0.001, 0.001, 0)).normalized() - var arc := Quaternion(fwd, d) - # Align a good chunk of the body with the line, capped so a grapple - # point straight overhead doesn't fold the character in half. - var frac := 0.65 - var ang := arc.get_angle() - if ang * frac > 1.15: - frac = 1.15 / ang - var per := Quaternion.IDENTITY.slerp(arc, grapple * frac / SPINE.size()) - for n in SPINE: - _add_space(skel, _idx.get(n, -1), per) - # Head keeps sighting the anchor point. - var head_q := Quaternion.IDENTITY.slerp(arc, grapple * 0.18) - _add_space(skel, _idx.get("DEF-neck", -1), head_q) - _add_space(skel, _idx.get("DEF-head", -1), head_q) - # Legs trail behind the flight line (Superman zip, not a swim). - var trail := Quaternion(Vector3(1, 0, 0), 0.45 * grapple) - _add_space(skel, _idx.get("DEF-thigh.R", -1), trail) - _add_space(skel, _idx.get("DEF-thigh.L", -1), - Quaternion(Vector3(1, 0, 0), 0.32 * grapple)) - var straighten := Quaternion(Vector3(1, 0, 0), -0.25 * grapple) - _add_space(skel, _idx.get("DEF-shin.R", -1), straighten) - _add_space(skel, _idx.get("DEF-shin.L", -1), straighten) - # Free arm reaches along the rope toward the anchor. - var ua_dir := (d + Vector3(0.28, 0.1, 0.0)).normalized() - var g_fa := _aim_chain(skel, "DEF-upper_arm.L", "DEF-forearm.L", - ua_dir, d, -0.2, grapple) - var hand_l: int = _idx.get("DEF-hand.L", -1) - var fa_l: int = _idx.get("DEF-forearm.L", -1) - if hand_l >= 0 and fa_l >= 0 and g_fa != Quaternion.IDENTITY: - var hand_rest_q := skel.get_bone_global_rest(hand_l).basis.get_rotation_quaternion() - var fa_o := skel.get_bone_global_rest(fa_l).origin - var hand_o := skel.get_bone_global_rest(hand_l).origin - var fa_rest_dir := (hand_o - fa_o).normalized() - var g_hand := Quaternion(d, 0.4) * Quaternion(fa_rest_dir, d) * hand_rest_q - _set_global_rot(skel, hand_l, g_fa, g_hand, grapple) - - # Wall run: roll into the wall, drive forward, inner arm reaches the wall. - func _apply_wall_lean(skel: Skeleton3D) -> void: - var q := Quaternion(Vector3(0, 0, 1), wall * 0.35) \ - * Quaternion(Vector3(1, 0, 0), WALL_PITCH * absf(wall)) - var per := Quaternion.IDENTITY.slerp(q, 1.0 / SPINE.size()) - for n in SPINE: - _add_space(skel, _idx.get(n, -1), per) - # Reach the wall-side arm out sideways (character-right = -X, so a - # negative Z rotation swings the down arm toward the right side). - var reach := Quaternion(Vector3(0, 0, 1), -WALL_ARM_OUT * wall) - if wall > 0.0: - _add_space(skel, _idx.get("DEF-upper_arm.R", -1), reach) - else: - _add_space(skel, _idx.get("DEF-upper_arm.L", -1), reach) - - # Slide: torso leans back, head looks forward, legs kick out in front so - # it reads feet-first instead of "sitting in a crouch". - func _apply_slide(skel: Skeleton3D) -> void: - var back := Quaternion(Vector3(1, 0, 0), -SLIDE_BACK * slide) - var per := Quaternion.IDENTITY.slerp(back, 1.0 / SPINE.size()) - for n in SPINE: - _add_space(skel, _idx.get(n, -1), per) - var up := Quaternion(Vector3(1, 0, 0), SLIDE_HEAD_UP * slide) - _add_space(skel, _idx.get("DEF-neck", -1), Quaternion.IDENTITY.slerp(up, 0.5)) - _add_space(skel, _idx.get("DEF-head", -1), Quaternion.IDENTITY.slerp(up, 0.5)) - # Legs: thighs swing forward (lead leg further), knees straighten. - var lead := Quaternion(Vector3(1, 0, 0), -SLIDE_LEG_FWD * slide) - var trail := Quaternion(Vector3(1, 0, 0), -SLIDE_LEG_FWD * 0.7 * slide) - _add_space(skel, _idx.get("DEF-thigh.R", -1), lead) - _add_space(skel, _idx.get("DEF-thigh.L", -1), trail) - var straighten := Quaternion(Vector3(1, 0, 0), SLIDE_KNEE * slide) - _add_space(skel, _idx.get("DEF-shin.R", -1), straighten) - _add_space(skel, _idx.get("DEF-shin.L", -1), straighten) - # Trailing arm braces back-and-down against the ground for balance. - var brace := Quaternion(Vector3(1, 0, 0), 0.9 * slide) \ - * Quaternion(Vector3(0, 0, 1), -0.5 * slide) - _add_space(skel, _idx.get("DEF-upper_arm.L", -1), brace) - _add_space(skel, _idx.get("DEF-forearm.L", -1), Quaternion(Vector3(1, 0, 0), 0.35 * slide)) - # ── Two-hand rifle hold ────────────────────────────────────────────────── # The gun is parented to the RIGHT HAND bone, so where the hands go decides # where the gun goes. We therefore place the WEAPON first — stock in the @@ -1753,7 +1870,6 @@ class ShooterPoseModifier extends SkeletonModifier3D: var ua_l: int = _idx.get("DEF-upper_arm.L", -1) if ua_r < 0: return - var breathe := sin(_time * 2.2) * 0.012 + fwd * 0.02 # ~7 degrees of muzzle rise per shot, stacking a little on full auto. var kick := recoil * 0.12 @@ -1762,7 +1878,8 @@ class ShooterPoseModifier extends SkeletonModifier3D: # # `pitch` is a low-ready knob only, and deliberately: down the sights # the muzzle follows the CAMERA, so there is nothing there to tune. - var gun_pitch := lerpf(_t("pitch_hip", GUN_PITCH_HIP), -aim_pitch, ads) - kick + breathe + var gun_pitch := lerpf( + _t("pitch_hip", GUN_PITCH_HIP), -aim_pitch, ads) - kick var aim_dir: Vector3 = (Quaternion(Vector3(1, 0, 0), gun_pitch) \ * Vector3(0, 0, 1)).normalized() # Gun's "up" — perpendicular to the barrel in the vertical plane, so the diff --git a/debug/acoustics_test.gd b/debug/acoustics_test.gd index 82d558b..013e2f8 100644 --- a/debug/acoustics_test.gd +++ b/debug/acoustics_test.gd @@ -106,6 +106,22 @@ func _process(_delta: float) -> bool: _check(Acoustics.classify(crate) == "wood", "name-based classify: crate -> wood") crate.free() + # An UNKNOWN acoustic_material must degrade to concrete, not throw. + # Sakura Crossing tagged its ballast "gravel" before gravel existed in + # MATERIALS, and every hitscan impact on it took down occlusion() from + # inside the audio path — a level builder's typo must never be able to + # stop the game making sound. + var odd := StaticBody3D.new() + odd.name = "OddSurface" + odd.set_meta("acoustic_material", "unobtainium") + _check(Acoustics.classify(odd) == "concrete", + "unknown acoustic_material falls back to concrete") + odd.free() + + # And every material the level builders actually use must resolve. + for m in ["concrete", "wood", "metal", "glass", "brick", "gravel", "grass"]: + _check(Acoustics.MATERIALS.has(m), "MATERIALS defines '%s'" % m) + print("=== Acoustics results: %s ===" % ("ALL PASSED" if _fails == 0 else "%d FAILED" % _fails)) return true return false diff --git a/debug/akiba_detail_capture.gd b/debug/akiba_detail_capture.gd new file mode 100644 index 0000000..a148dbf --- /dev/null +++ b/debug/akiba_detail_capture.gd @@ -0,0 +1,65 @@ +extends SceneTree + +## Close-up visual regression capture for Akiba's repaired assets. +## godot --path . --windowed --resolution 1280x720 \ +## -s res://debug/akiba_detail_capture.gd -- + +var _frames := 0 +var _out_dir := "." +var _camera: Camera3D + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if not args.is_empty(): + _out_dir = args[0] + change_scene_to_file("res://scenes/maps/neon_alley/neon_alley.tscn") + + +func _process(_delta: float) -> bool: + _frames += 1 + if _frames == 60: + _camera = Camera3D.new() + _camera.far = 1200.0 + current_scene.add_child(_camera) + _camera.current = true + var poles := root.find_children( + "UtilityPole_*", "StaticBody3D", true, false) + if not poles.is_empty(): + var pole := poles[0] as StaticBody3D + _camera.look_at_from_position( + pole.global_position + Vector3(-8.0, 3.2, -9.0), + pole.global_position + Vector3(0.6, 5.8, 0), Vector3.UP) + elif _frames == 90: + _snap("powerlines") + var cars := root.find_children("Car_*", "StaticBody3D", true, false) + if not cars.is_empty(): + var car := cars[0] as StaticBody3D + _camera.look_at_from_position( + car.global_position + Vector3(6.5, 3.1, 7.5), + car.global_position + Vector3(0, 1.0, 0), Vector3.UP) + elif _frames == 120: + _snap("parked_car") + var landings := root.find_children( + "EscLanding_*", "StaticBody3D", true, false) + if not landings.is_empty(): + var landing := landings[0] as StaticBody3D + _camera.look_at_from_position( + landing.global_position + Vector3(0, 3.2, -10.0), + landing.global_position + Vector3(0, 2.0, 0), Vector3.UP) + elif _frames == 150: + _snap("supported_escape") + unload_current_scene() + elif _frames >= 240: + return true + return false + + +func _snap(tag: String) -> void: + var image := root.get_viewport().get_texture().get_image() + var path := _out_dir.path_join("akiba_%s.png" % tag) + var error := image.save_png(path) + if error == OK: + print("akiba_detail_capture: saved ", path) + else: + printerr("akiba_detail_capture: save failed ", error) diff --git a/debug/akiba_detail_capture.gd.uid b/debug/akiba_detail_capture.gd.uid new file mode 100644 index 0000000..9434a5c --- /dev/null +++ b/debug/akiba_detail_capture.gd.uid @@ -0,0 +1 @@ +uid://bmuejgnrchb1g diff --git a/debug/akiba_integrity_probe.gd b/debug/akiba_integrity_probe.gd new file mode 100644 index 0000000..7dccd6e --- /dev/null +++ b/debug/akiba_integrity_probe.gd @@ -0,0 +1,243 @@ +extends SceneTree + +## Geometry/material regression probe for Akiba Crossing. +## +## This deliberately verifies the mechanisms that prove the requested fixes: +## imported art has one transformed triangle collider per rendered mesh (not +## an enclosing AABB), every supported prop sits on its declared plane, every +## utility curve terminates at a pole/building terminal, and cable interiors +## do not intersect static geometry. + +var _frames := 0 +var _result_ready := false +var _exit_code := 0 + + +func _initialize() -> void: + change_scene_to_file("res://scenes/maps/neon_alley/neon_alley.tscn") + + +func _process(_delta: float) -> bool: + _frames += 1 + if _result_ready: + if _frames >= 45: + quit(_exit_code) + return false + if _frames < 90: + return false + if current_scene == null: + printerr("AKIBA_INTEGRITY no current scene") + quit(1) + return false + + var exact_bodies := 0 + var exact_meshes := 0 + var exact_shapes := 0 + var collision_mismatches := 0 + var fallbacks := 0 + var untextured_cars := 0 + var out_of_district := 0 + for candidate in root.find_children("*", "StaticBody3D", true, false): + var body := candidate as StaticBody3D + if body.has_meta("exact_collision_fallback"): + fallbacks += 1 + if not body.has_meta("mesh_exact_collision"): + continue + exact_bodies += 1 + var meshes := body.find_children("*", "MeshInstance3D", true, false) + var shapes := body.find_children( + "ExactMeshCollision_*", "CollisionShape3D", false, false) + exact_meshes += meshes.size() + exact_shapes += shapes.size() + if meshes.size() != shapes.size(): + collision_mismatches += 1 + _report_collision_issue(body, + "mesh_count=%d shape_count=%d" % [meshes.size(), shapes.size()]) + for shape_candidate in shapes: + var shape := shape_candidate as CollisionShape3D + if not shape.shape is ConcavePolygonShape3D \ + or (shape.shape as ConcavePolygonShape3D).get_faces().is_empty(): + collision_mismatches += 1 + _report_collision_issue(body, "%s is not triangle collision" % shape.name) + continue + var source_path: String = str(shape.get_meta("source_mesh_path", "")) + var source := root.get_node_or_null(NodePath(source_path)) as MeshInstance3D + if source == null: + collision_mismatches += 1 + _report_collision_issue(body, "%s has no source mesh" % shape.name) + continue + var expected := body.global_transform.affine_inverse() * source.global_transform + if not shape.transform.is_equal_approx(expected): + collision_mismatches += 1 + _report_collision_issue(body, "%s transform differs from %s" % [ + shape.name, source.name]) + if body.name.begins_with("Car_") and not _has_textured_surface(body): + untextured_cars += 1 + + for child in root.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := child as MeshInstance3D + if mesh_instance.mesh == null: + continue + var global_bounds := _transform_aabb( + mesh_instance.get_aabb(), mesh_instance.global_transform) + var center := global_bounds.get_center() + if absf(center.x) > 336.0 or absf(center.z) > 336.0: + out_of_district += 1 + if out_of_district <= 8: + print("AKIBA_OUT_OF_DISTRICT %s center=%s" % [ + mesh_instance.get_path(), center]) + + var support_checked := 0 + var unsupported := 0 + var road_lamps := 0 + var road_intrusions := 0 + for candidate in root.find_children("*", "Node3D", true, false): + var node := candidate as Node3D + if node.has_meta("support_fitted"): + support_checked += 1 + var plane := float(node.get_meta("support_plane_y")) + var bounds := _global_visual_bounds(node) + if bounds.size == Vector3.ZERO or absf(bounds.position.y - plane) > 0.025: + unsupported += 1 + if unsupported <= 8: + print("AKIBA_UNSUPPORTED_PROP %s bottom=%.4f plane=%.4f" % [ + node.get_path(), bounds.position.y, plane]) + var source_path: String = str(node.get_meta("source_asset_path", "")) + if source_path.contains("light-curved"): + road_lamps += 1 + var p := node.global_position + var central_ns := absf(p.x) < 20.0 + var central_ew := absf(p.z) < 20.0 + if (central_ns and absf(p.x) < 7.0) \ + or (central_ew and absf(p.z) < 7.0): + road_intrusions += 1 + print("AKIBA_ROAD_LAMP %s position=%s" % [node.get_path(), p]) + + var terminals: Array[Vector3] = [] + for marker in root.find_children("UtilityTerminal_*", "Node3D", true, false): + terminals.append((marker as Node3D).global_position) + var curves: Dictionary = {} + var cable_intersections := 0 + var space: PhysicsDirectSpaceState3D = \ + current_scene.get_world_3d().direct_space_state + for candidate in root.find_children("PowerCable_*", "MeshInstance3D", true, false): + var cable := candidate as MeshInstance3D + var curve_id := int(cable.get_meta("curve_id", -1)) + var segment_index := int(cable.get_meta("segment_index", -1)) + var segment_count := int(cable.get_meta("segment_count", 0)) + if not curves.has(curve_id): + curves[curve_id] = { + "from": cable.get_meta("cable_from"), + "to": cable.get_meta("cable_to"), + "first": segment_index, + "last": segment_index, + } + else: + var record: Dictionary = curves[curve_id] + if segment_index < int(record["first"]): + record["first"] = segment_index + record["from"] = cable.get_meta("cable_from") + if segment_index > int(record["last"]): + record["last"] = segment_index + record["to"] = cable.get_meta("cable_to") + curves[curve_id] = record + var from: Vector3 = cable.get_meta("cable_from") + var to: Vector3 = cable.get_meta("cable_to") + var query := PhysicsRayQueryParameters3D.create( + from.lerp(to, 0.16), from.lerp(to, 0.84)) + query.collide_with_areas = false + var hit: Dictionary = space.intersect_ray(query) + if not hit.is_empty(): + cable_intersections += 1 + if cable_intersections <= 8: + print("AKIBA_CABLE_INTERSECTION curve=%d segment=%d/%d collider=%s" % [ + curve_id, segment_index, segment_count, + (hit["collider"] as Node).get_path()]) + + var unterminated_curves := 0 + for curve_id in curves: + var record: Dictionary = curves[curve_id] + if not _near_terminal(record["from"], terminals) \ + or not _near_terminal(record["to"], terminals): + unterminated_curves += 1 + if unterminated_curves <= 8: + print("AKIBA_UNTERMINATED_CURVE id=%s from=%s to=%s" % [ + curve_id, record["from"], record["to"]]) + + var power_cables := root.find_children( + "PowerCable_*", "MeshInstance3D", true, false).size() + var utility_poles := root.find_children( + "UtilityPole_*", "StaticBody3D", true, false).size() + var failures := collision_mismatches + fallbacks + untextured_cars \ + + out_of_district + unsupported + road_intrusions \ + + cable_intersections + unterminated_curves + print("AKIBA_INTEGRITY exact_bodies=%d exact_meshes=%d exact_shapes=%d collision_mismatches=%d fallbacks=%d untextured_cars=%d out_of_district=%d support_checked=%d unsupported=%d road_lamps=%d road_intrusions=%d cables=%d curves=%d poles=%d terminals=%d cable_intersections=%d unterminated_curves=%d failures=%d" % [ + exact_bodies, exact_meshes, exact_shapes, collision_mismatches, + fallbacks, untextured_cars, out_of_district, support_checked, + unsupported, road_lamps, road_intrusions, power_cables, + curves.size(), utility_poles, terminals.size(), cable_intersections, + unterminated_curves, failures]) + if failures > 0: + printerr("AKIBA_INTEGRITY FAILED") + _exit_code = 1 + else: + print("AKIBA_INTEGRITY PASSED") + _exit_code = 0 + unload_current_scene() + _result_ready = true + _frames = 0 + return false + + +func _report_collision_issue(body: StaticBody3D, detail: String) -> void: + print("AKIBA_COLLISION_MISMATCH %s %s" % [body.get_path(), detail]) + + +func _near_terminal(point: Vector3, terminals: Array[Vector3]) -> bool: + for terminal in terminals: + if point.distance_to(terminal) <= 0.035: + return true + return false + + +func _global_visual_bounds(node: Node) -> AABB: + var result := AABB() + var has_bounds := false + for child in node.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := child as MeshInstance3D + if mesh_instance.mesh == null: + continue + var transformed := _transform_aabb( + mesh_instance.get_aabb(), mesh_instance.global_transform) + result = result.merge(transformed) if has_bounds else transformed + has_bounds = true + return result if has_bounds else AABB() + + +func _transform_aabb(bounds: AABB, xform: Transform3D) -> AABB: + var result := AABB(xform * bounds.position, Vector3.ZERO) + for x in 2: + for y in 2: + for z in 2: + var corner := bounds.position + Vector3( + bounds.size.x * float(x), + bounds.size.y * float(y), + bounds.size.z * float(z)) + result = result.expand(xform * corner) + return result + + +func _has_textured_surface(body: Node) -> bool: + for child in body.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := child as MeshInstance3D + if mesh_instance.mesh == null: + continue + for surface in mesh_instance.mesh.get_surface_count(): + var material := mesh_instance.get_active_material(surface) + if material is ShaderMaterial \ + and (material.get_shader_parameter("has_texture") == true \ + or material.get_shader_parameter("use_vertex_color") == true): + return true + if material is BaseMaterial3D and material.albedo_texture != null: + return true + return false diff --git a/debug/akiba_integrity_probe.gd.uid b/debug/akiba_integrity_probe.gd.uid new file mode 100644 index 0000000..1b3626e --- /dev/null +++ b/debug/akiba_integrity_probe.gd.uid @@ -0,0 +1 @@ +uid://cgjnu7mwndf28 diff --git a/debug/all_maps_smoke_test.gd b/debug/all_maps_smoke_test.gd new file mode 100644 index 0000000..2d1a599 --- /dev/null +++ b/debug/all_maps_smoke_test.gd @@ -0,0 +1,150 @@ +extends SceneTree + +## Loads every menu-selectable level through an active single-player match, +## waits for its player/viewmodel to initialize, then advances. This is the +## release gate for scene-load, shader, and teardown errors. + +## The map list is DISCOVERED, not written down. +## +## It used to be a hardcoded array, and a newly added level (Sakura Crossing) +## loaded, shipped and never once went through this gate — the run still said +## "PASS: all 5 levels" while there were six. A release gate that has to be +## edited by hand every time it gains something to test is a gate that will be +## wrong exactly when it matters. +## +## `scenes/maps/*/map_meta.cfg` is already the definition of "menu-selectable", +## since that is what the main menu and map_preview_capture both scan. +const MAPS_DIR := "res://scenes/maps/" + +var MAPS: Array[String] = [] + + +func _discover_maps() -> Array[String]: + var out: Array[String] = [] + var dir := DirAccess.open(MAPS_DIR) + if dir == null: + return out + dir.list_dir_begin() + var folder := dir.get_next() + while folder != "": + if dir.current_is_dir() and not folder.begins_with("."): + var cfg := ConfigFile.new() + if cfg.load(MAPS_DIR + folder + "/map_meta.cfg") == OK: + var scene_path: String = cfg.get_value("map", "scene_path", "") + if scene_path != "" and ResourceLoader.exists(scene_path): + out.append(scene_path) + folder = dir.get_next() + out.sort() + return out + + +var _mode := "boot" +var _frames := 0 +var _map_index := -1 +var _failures: Array[String] = [] +var _loaded_maps := 0 +var _skin_override := "" + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if not args.is_empty(): + _skin_override = args[0] + MAPS = _discover_maps() + print("MAP_SMOKE discovered %d levels: %s" % [MAPS.size(), ", ".join(MAPS)]) + change_scene_to_file("res://ui/main_menu/main_menu.tscn") + + +func _process(_delta: float) -> bool: + _frames += 1 + if _mode == "boot": + if _frames == 45: + if not _skin_override.is_empty(): + var skin_manager = root.get_node_or_null("SkinManager") + if skin_manager and skin_manager.skins.has(_skin_override): + # Test-only selection: direct assignment avoids overwriting + # the player's saved preference in user://. + skin_manager.active_skin_id = _skin_override + print("MAP_SMOKE skin override: ", _skin_override) + else: + _fail("unknown skin override: " + _skin_override) + var network = root.get_node_or_null("NetworkManager") + if network and network.has_method("start_singleplayer_match"): + network.start_singleplayer_match(GameMode.DEATHMATCH) + _load_next() + return false + + if _mode == "map": + if _frames == 150: + var players := root.find_children("*", "CharacterBody3D", true, false) + var local_player = null + for player in players: + if player.has_method("get_visual_model") \ + and player.is_multiplayer_authority(): + local_player = player + break + if local_player == null: + _fail("missing local player: " + MAPS[_map_index]) + else: + var managers: Array[Node] = local_player.find_children( + "WeaponManager", "WeaponManager", true, false) + if managers.is_empty(): + _fail("missing WeaponManager: " + MAPS[_map_index]) + else: + print("MAP_SMOKE loaded ", MAPS[_map_index]) + _loaded_maps += 1 + _audit_null_materials() + elif _frames >= 180: + _load_next() + return false + + if _mode == "drain": + if _frames >= 180: + if _failures.is_empty() and _loaded_maps == MAPS.size(): + print("MAP_SMOKE PASS: all %d levels loaded cleanly" % MAPS.size()) + quit(0) + else: + for failure in _failures: + push_error("MAP_SMOKE FAIL: " + failure) + quit(1) + return true + return false + return false + + +func _load_next() -> void: + _map_index += 1 + _frames = 0 + if _map_index >= MAPS.size(): + _mode = "drain" + unload_current_scene() + return + _mode = "map" + var error := change_scene_to_file(MAPS[_map_index]) + if error != OK: + _fail("could not load %s (error %d)" % [MAPS[_map_index], error]) + + +func _fail(message: String) -> void: + _failures.append(message) + printerr("MAP_SMOKE ", message) + + +func _audit_null_materials() -> void: + var null_surfaces: Array[String] = [] + for candidate in root.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := candidate as MeshInstance3D + if mesh_instance.mesh == null: + continue + for surface in mesh_instance.mesh.get_surface_count(): + if mesh_instance.get_active_material(surface) == null: + null_surfaces.append( + "%s[%d]" % [mesh_instance.get_path(), surface]) + if null_surfaces.is_empty(): + print("MAP_SMOKE material audit: 0 null surfaces") + else: + _fail("%d null mesh materials in %s: %s" % [ + null_surfaces.size(), + MAPS[_map_index], + ", ".join(null_surfaces.slice(0, 12)), + ]) diff --git a/debug/all_maps_smoke_test.gd.uid b/debug/all_maps_smoke_test.gd.uid new file mode 100644 index 0000000..08e0aa6 --- /dev/null +++ b/debug/all_maps_smoke_test.gd.uid @@ -0,0 +1 @@ +uid://o46rju6s0mug diff --git a/debug/anchor_shift_check.gd b/debug/anchor_shift_check.gd index 514cb78..b289d04 100644 --- a/debug/anchor_shift_check.gd +++ b/debug/anchor_shift_check.gd @@ -17,13 +17,10 @@ extends SceneTree ## different places in the two poses, and a skeleton-space implementation ## passes check 1 happily. ## -## MEASURED IN THE GUN'S FRAME, and it has to be. The hold BREATHES — there is a -## `sin(_time * 2.2) * 0.012` on the muzzle pitch — so no anchor is ever at the -## same world position twice, and the first version of this check compared -## absolute positions and reported a 3.5 mm error that was just the character -## inhaling. Taking each anchor relative to the one it hangs off and rotating -## into the current gun basis cancels the breathing, the ADS blend and the -## recoil kick exactly, because all three move the basis and the anchor together. +## MEASURED IN THE GUN'S FRAME, and it has to be. Taking each anchor relative to +## the one it hangs off and rotating into the current gun basis cancels the ADS +## blend and recoil kick exactly, because both move the basis and anchor +## together. ## ## godot --headless --path . -s res://debug/anchor_shift_check.gd diff --git a/debug/anim_capture.gd b/debug/anim_capture.gd index 9e6a1da..88394a8 100644 --- a/debug/anim_capture.gd +++ b/debug/anim_capture.gd @@ -2,30 +2,46 @@ extends SceneTree ## Dev tool: screenshot every animation state on the active GLB skin. ## Run: -## godot --path . --windowed --resolution 1280x720 -s res://debug/anim_capture.gd -- [skin_id] +## godot --path . --windowed --resolution 1280x720 -s res://debug/anim_capture.gd -- [skin_id] [unarmed] [tag,tag,...] ## Saves anim_.png per state plus mid-action shots for reload/throw. var _frames := 0 var _out_dir := "." var _skin := "miku" +var _unarmed := false var _player: Node = null var _model: Node = null var _cam: Camera3D = null -# [tag, state, speed, crouch, hold_frames] +# [tag, state, speed, crouch, hold_frames, strafe, forward, emote_index] var _states := [ - ["idle", "idle", 0.0, false, 50], - ["walk", "ground", 2.2, false, 50], - ["run", "ground", 6.5, false, 50], - ["sprint", "ground", 12.0, false, 50], - ["crouch", "ground", 0.0, true, 50], - ["crouchwalk", "ground", 2.0, true, 50], - ["fall", "air", 4.0, false, 50], - ["slide", "slide", 10.0, true, 50], - ["dash", "dash", 14.0, false, 30], - ["wallrun", "wall_run", 9.0, false, 50], - ["grapple", "grapple", 10.0, false, 50], - ["dance", "idle", 0.0, false, 60], + ["idle", "idle", 0.0, false, 50, 0.0, 0.0, -1], + ["walk", "ground", 0.975, false, 50, 0.0, 1.0, -1], + ["run", "ground", 5.26148, false, 50, 0.0, 1.0, -1], + ["sprint", "ground", 8.25, false, 50, 0.0, 1.0, -1], + ["sprint_game_speed", "ground", 12.0, false, 50, 0.0, 1.0, -1], + ["strafe_walk_back", "ground", 3.0, false, 50, 0.0, -1.0, -1], + ["strafe_walk_left", "ground", 3.0, false, 50, -1.0, 0.0, -1], + ["strafe_walk_right", "ground", 3.0, false, 50, 1.0, 0.0, -1], + ["run_back", "ground", 8.25, false, 50, 0.0, -1.0, -1], + ["run_left", "ground", 8.25, false, 50, -1.0, 0.0, -1], + ["run_right", "ground", 8.25, false, 50, 1.0, 0.0, -1], + ["run_left_game_speed", "ground", 11.0, false, 50, -1.0, 0.0, -1], + ["run_right_game_speed", "ground", 11.0, false, 50, 1.0, 0.0, -1], + ["crouch", "ground", 0.0, true, 50, 0.0, 0.0, -1], + ["crouchwalk", "ground", 0.75, true, 50, 0.0, 1.0, -1], + ["fall", "air", 4.0, false, 50, 0.0, 1.0, -1], + ["slide", "slide", 10.0, true, 50, 0.0, 1.0, -1], + ["dash", "dash", 14.0, false, 30, 0.0, 1.0, -1], + ["wallrun", "wall_run", 13.0, false, 50, 0.0, 1.0, -1], + ["wallcling", "wall_cling", 0.0, false, 50, 0.0, 0.0, -1], + ["wallclimb", "wall_climb", 2.0, false, 50, 0.0, 1.0, -1], + ["grapple", "grapple", 10.0, false, 50, 0.0, 1.0, -1], + ["emote_dance", "idle", 0.0, false, 60, 0.0, 0.0, 0], + ["emote_stretch", "idle", 0.0, false, 60, 0.0, 0.0, 1], + ["emote_call", "idle", 0.0, false, 60, 0.0, 0.0, 2], + ["emote_yes", "idle", 0.0, false, 60, 0.0, 0.0, 3], + ["emote_no", "idle", 0.0, false, 60, 0.0, 0.0, 4], ] var _phase := 0 # index into _states, then actions after var _phase_frame := 0 @@ -40,6 +56,20 @@ func _initialize() -> void: _out_dir = args[0] if args.size() > 1: _skin = args[1] + if args.size() > 2: + _unarmed = args[2].to_lower() == "unarmed" + if args.size() > 3 and args[3] != "": + var wanted := args[3].split(",", false) + _states = _states.filter(func(state: Array) -> bool: + return String(state[0]) in wanted) + if not _out_dir.is_absolute_path(): + _out_dir = ProjectSettings.globalize_path(_out_dir) + var mkdir_error := DirAccess.make_dir_recursive_absolute(_out_dir) + if mkdir_error != OK: + printerr("anim_capture: could not create '%s' (error %d)" % [ + _out_dir, mkdir_error]) + quit(mkdir_error) + return change_scene_to_file("res://ui/main_menu/main_menu.tscn") @@ -75,9 +105,10 @@ func _process(_delta: float) -> bool: _player.global_position = Vector3(0, 1.2, 14) _player.rotation = Vector3.ZERO _model.set_owner_visible(true) - # Give the model a weapon so armed poses read. + # Optional unarmed pass exposes the authored arm silhouettes used by + # idle/dance/emotes instead of covering them with weapon IK. if _model.has_method("set_weapon"): - _model.set_weapon("res://weapons/ak47.gd") + _model.set_weapon("" if _unarmed else "res://weapons/ak47.gd") # Camera: repositioned per shot (front + side). Model faces -Z. _cam = Camera3D.new() _cam.cull_mask &= ~(1 << 19) # don't render the FP viewmodel layer @@ -91,8 +122,14 @@ func _process(_delta: float) -> bool: # hop needs a real rendered frame in between). if _mode == "states": var s: Array = _states[_phase] - if s[0] == "dance" and _model.has_method("set_dancing"): - _model.set_dancing(true) + if _model.has_method("set_locomotion"): + _model.set_locomotion(float(s[5]), float(s[6]), 0.0) + if s[0] == "wallrun" and _model.has_method("set_wall_side"): + _model.set_wall_side(-1.0) + if _model.has_method("set_wall_run_motion"): + _model.set_wall_run_motion(Vector3(13.0, 0.0, 0.0)) + if int(s[7]) >= 0 and _model.has_method("set_dancing"): + _model.set_dancing(true, int(s[7])) _model.update_state(s[1], s[2], s[3]) _phase_frame += 1 var hold: int = int(s[4]) @@ -102,7 +139,7 @@ func _process(_delta: float) -> bool: elif _phase_frame >= hold: _snap("anim_" + String(s[0]) + "_s") _cam_to(Vector3(0.4, 0.9, -2.4)) - if s[0] == "dance" and _model.has_method("set_dancing"): + if int(s[7]) >= 0 and _model.has_method("set_dancing"): _model.set_dancing(false) if s[0] == "idle": _debug_gun() @@ -188,7 +225,10 @@ func _process(_delta: float) -> bool: _model.update_state("grapple", 12.0, false) if _model.has_method("set_grapple_target"): # Anchor up and ahead of the player (player faces -Z world). - _model.set_grapple_target(_player.global_position + Vector3(1.5, 7.0, -7.0)) + _model.set_grapple_target( + _player.global_position + Vector3(1.5, 7.0, -7.0), + Vector3(14.0, 2.0, -4.0), + ) _phase_frame += 1 if _phase_frame == 48: _snap("anim_grapple_zip_f") @@ -222,5 +262,9 @@ func _cam_to(offset: Vector3) -> void: func _snap(tag: String) -> void: var img := root.get_viewport().get_texture().get_image() var path := _out_dir + "/" + tag + ".png" - img.save_png(path) - print("anim_capture: saved ", path) + var save_error := img.save_png(path) + if save_error == OK: + print("anim_capture: saved ", path) + else: + printerr("anim_capture: could not save '%s' (error %d)" % [ + path, save_error]) diff --git a/debug/audio_fidelity_test.gd b/debug/audio_fidelity_test.gd new file mode 100644 index 0000000..599b671 --- /dev/null +++ b/debug/audio_fidelity_test.gd @@ -0,0 +1,257 @@ +extends SceneTree + +## Runtime release gate for the premium audio pass. +## +## Uses a real Forward+ match so it verifies autoload ordering, imported audio +## resources, a spawned player's movement players, and a live weapon loadout. + +var _frames := 0 +var _phase := "menu" +var _failures: Array[String] = [] + +const REQUIRED_EVENTS := [ + "ak47_fire", "m4_fire", "mp7_fire", "dmr_fire", "awp_fire", + "shotgun_fire", "nailgun_fire", "plasma_fire", "rocket_fire", + "swarm_fire", "mortar_fire", "knife_swing", "shotgun_reload", + "explosion", "bullet_impact", + "footstep_concrete", "footstep_metal", "footstep_wood", "footstep_glass", + "wind", "slide", "wallrun", "dash", "jump", "double_jump", "vault", "land", + "grapple_launch", "grapple_latch", "grapple_reel", + "ui_hover", "ui_click", "ui_confirm", "ui_error", "ui_equip", +] + + +func _initialize() -> void: + change_scene_to_file("res://ui/main_menu/main_menu.tscn") + + +func _process(_delta: float) -> bool: + _frames += 1 + if _phase == "menu" and _frames == 50: + _verify_menu_music() + _verify_manager() + var network = root.get_node_or_null("NetworkManager") + if network and network.has_method("start_singleplayer_match"): + network.start_singleplayer_match(GameMode.DEATHMATCH) + else: + _fail("NetworkManager cannot start the runtime audio match") + change_scene_to_file("res://scenes/maps/test_level/test_level.tscn") + _phase = "match" + _frames = 0 + elif _phase == "match" and _frames == 180: + _verify_live_player() + _phase = "drain" + _frames = 0 + unload_current_scene() + elif _phase == "drain" and _frames >= 90: + _finish() + return false + + +func _verify_menu_music() -> void: + var found := false + if current_scene: + for candidate in current_scene.find_children("*", "AudioStreamPlayer", true, false): + var player := candidate as AudioStreamPlayer + if player.bus == "Music": + found = true + _check(player.playing, "menu music is playing") + _check(player.stream is AudioStreamOggVorbis, + "menu music is an imported Ogg stream") + if player.stream is AudioStreamOggVorbis: + _check((player.stream as AudioStreamOggVorbis).loop, + "menu music loops") + break + _check(found, "main menu owns a Music-bus player") + + +func _verify_manager() -> void: + var am = root.get_node_or_null("AudioManager") + _check(am != null, "AudioManager autoload exists") + if am == null: + return + for event in REQUIRED_EVENTS: + _check(am.has_sound(event), "event resolves: %s" % event) + _check(am.weapon_sound_id("Double Barrel") == "shotgun_fire", + "multiplayer shotgun broadcast resolves to its fire family") + + var expected_sends := { + "SFX": "Master", "Weapons": "SFX", "Explosions": "SFX", + "Impacts": "SFX", "Footsteps": "SFX", "Movement": "SFX", + "Ambience": "SFX", "UI": "SFX", "Wind": "SFX", "Music": "Master", + } + for bus_name in expected_sends: + var bus := AudioServer.get_bus_index(bus_name) + _check(bus >= 0, "bus exists: %s" % bus_name) + if bus >= 0: + _check(AudioServer.get_bus_send(bus) == expected_sends[bus_name], + "%s routes to %s" % [bus_name, expected_sends[bus_name]]) + + var master := AudioServer.get_bus_index("Master") + _check(_bus_has_effect(master, "AudioEffectHardLimiter"), + "Master has a hard limiter") + _check(_bus_has_effect(AudioServer.get_bus_index("Weapons"), + "AudioEffectCompressor"), "Weapons has transient compression") + _check(_bus_has_effect(AudioServer.get_bus_index("Explosions"), + "AudioEffectCompressor"), "Explosions has body compression") + + var loop_stream: AudioStream = am.stream_for("wind") + _check(loop_stream is AudioStreamRandomizer, "wind resolves through variation stream") + if loop_stream is AudioStreamRandomizer: + var randomizer := loop_stream as AudioStreamRandomizer + _check(randomizer.streams_count >= 1, "wind has a source stream") + var child := randomizer.get_stream(0) + _check(child is AudioStreamWAV, "wind source is WAV") + if child is AudioStreamWAV: + var wav := child as AudioStreamWAV + _check(wav.loop_mode == AudioStreamWAV.LOOP_FORWARD, + "wind source has forward looping enabled") + _check(wav.loop_end > 0, "wind source has an explicit loop end") + + var first = am.play_ui("ui_hover") + var suppressed = am.play_ui("ui_hover") + _check(first != null, "UI hover plays") + _check(suppressed == null, "UI hover chatter is rate-limited") + var click = am.play_ui("ui_click") + _check(click != null and click.bus == "UI", "UI click plays on the UI bus") + var explosion = am.play_3d("explosion", Vector3.ZERO) + _check(explosion != null and explosion.bus == "Explosions", + "explosion plays on the Explosions bus") + + for material in ["concrete", "metal", "wood", "glass"]: + var surface := Node.new() + surface.set_meta("acoustic_material", material) + _check(Acoustics.classify(surface) == material, + "surface classifier preserves %s" % material) + surface.free() + + +func _verify_live_player() -> void: + var ambience := current_scene.get_node_or_null("AmbientBed") \ + if current_scene else null + _check(ambience == null, "level has no always-on broadband ambience") + + var player = null + for candidate in root.find_children("*", "CharacterBody3D", true, false): + if candidate.has_method("get_visual_model") \ + and candidate.is_multiplayer_authority(): + player = candidate + break + _check(player != null, "single-player match spawned the local player") + if player == null: + return + + _check(player.has_method("surface_event"), "player exposes material event lookup") + var foot_event: String = player.surface_event("footstep") + _check(foot_event in [ + "footstep_concrete", "footstep_metal", "footstep_wood", "footstep_glass"], + "live floor resolves to a footstep family (%s)" % foot_event) + player.play_footstep(8.0) + _check(player.footstep_player.bus == "Footsteps", + "local footsteps use the Footsteps bus") + _check(player.footstep_player.playing, "local material footstep plays") + + player.update_slide_audio(14.0) + _check(player.slide_player.bus == "Movement", "slide uses the Movement bus") + _check(player.slide_player.stream is AudioStreamRandomizer, + "slide uses the mastered looping family") + player.update_wallrun_audio(14.0, Vector3.LEFT) + _check(player.wallrun_player.bus == "Movement", "wall-run uses the Movement bus") + _check(player.wind_player.bus == "Wind", "speed wind uses the independent Wind bus") + _check(player.grapple_shoot_player.bus == "Movement", + "grapple launch uses the Movement bus") + _check(player.grapple_latch_player.bus == "Movement", + "grapple latch uses the Movement bus") + _check(player.grapple_swing_player.bus == "Movement", + "grapple reel uses the Movement bus") + _check(player.jump_player.stream != player.double_jump_player.stream, + "ground jump and powered double jump use distinct masters") + var grapple_streams := [ + player.grapple_shoot_player.stream, + player.grapple_latch_player.stream, + player.grapple_swing_player.stream, + ] + _check(grapple_streams[0] != grapple_streams[1] + and grapple_streams[0] != grapple_streams[2] + and grapple_streams[1] != grapple_streams[2], + "grapple launch, latch and reel use three distinct masters") + _check(player.jump_player.stream not in grapple_streams + and player.double_jump_player.stream not in grapple_streams + and player.dash_player.stream not in grapple_streams, + "grapple stages do not reuse jump or jet-thrust masters") + _check(_randomizer_has_loop(player.grapple_swing_player.stream), + "grapple reel resolves to an explicitly looping cable stream") + player._on_movement_event("grapple_shoot", {}) + _check(player.grapple_shoot_player.playing, + "grapple shoot event plays the cable launcher") + player._on_movement_event("grapple_latch", {}) + _check(player.grapple_latch_player.playing, + "grapple latch event plays the hook impact") + _check(player._jet_vfx != null and player._jet_vfx.get_flame_count() == 4, + "live player owns exactly four jet flames") + var jet_before: float = player._jet_vfx._burst_left + player._on_movement_event("jump", {}) + _check(is_equal_approx(player._jet_vfx._burst_left, jet_before), + "ordinary jump does not ignite the jets") + player._on_movement_event("double_jump", {}) + _check(player._jet_vfx._burst_left > 0.0, + "double jump ignites the four-jet burst") + + var managers: Array[Node] = player.find_children( + "WeaponManager", "WeaponManager", true, false) + _check(not managers.is_empty(), "live player has a WeaponManager") + if not managers.is_empty(): + var manager = managers[0] + var checked_weapons := 0 + for weapon in manager.weapons.values(): + if weapon and "fire_sound" in weapon and weapon.fire_sound: + checked_weapons += 1 + _check(weapon.fire_sound.bus == "Weapons", + "%s fire routes to Weapons" % weapon.weapon_name) + _check(weapon.fire_sound.stream is AudioStreamRandomizer, + "%s uses the mastered variation family" % weapon.weapon_name) + _check(weapon.fire_sound.max_polyphony >= 4, + "%s shots keep overlapping body/tails" % weapon.weapon_name) + _check(checked_weapons >= 3, "at least three live weapon players verified") + + +func _randomizer_has_loop(stream: AudioStream) -> bool: + if not stream is AudioStreamRandomizer: + return false + var randomizer := stream as AudioStreamRandomizer + if randomizer.streams_count < 1: + return false + var child := randomizer.get_stream(0) + return child is AudioStreamWAV \ + and (child as AudioStreamWAV).loop_mode == AudioStreamWAV.LOOP_FORWARD + + +func _bus_has_effect(bus: int, effect_class: String) -> bool: + if bus < 0: + return false + for i in AudioServer.get_bus_effect_count(bus): + var effect := AudioServer.get_bus_effect(bus, i) + if effect and effect.is_class(effect_class): + return true + return false + + +func _check(condition: bool, label: String) -> void: + if condition: + print(" PASS: ", label) + else: + _fail(label) + + +func _fail(label: String) -> void: + _failures.append(label) + printerr("AUDIO_FIDELITY FAIL: ", label) + + +func _finish() -> void: + if _failures.is_empty(): + print("AUDIO_FIDELITY PASSED") + quit(0) + else: + printerr("AUDIO_FIDELITY failures=", _failures.size()) + quit(1) diff --git a/debug/audio_fidelity_test.gd.uid b/debug/audio_fidelity_test.gd.uid new file mode 100644 index 0000000..718e36e --- /dev/null +++ b/debug/audio_fidelity_test.gd.uid @@ -0,0 +1 @@ +uid://c2ob7ppq88shf diff --git a/debug/cloth_allow_check.gd b/debug/cloth_allow_check.gd index 7b6e1df..68555a1 100644 --- a/debug/cloth_allow_check.gd +++ b/debug/cloth_allow_check.gd @@ -1,5 +1,7 @@ extends SceneTree +const CLOTH_CLASSES := ["skirt", "cloth", "cape", "coat", "scarf", "ribbon", "sleeve"] + ## Dev tool: how much room does the collision solver actually HAVE? ## ## godot --headless --path . -s res://debug/cloth_allow_check.gd -- [skin_glb] @@ -51,7 +53,7 @@ func _initialize() -> void: print(" BLIND = limb radius here - the allowance the rest-clearance cap gives\n") var rows: Array = [] for ch in info.get("chains", []): - if String(ch.get("class", "")) not in SpringBones.DRAPE_CLASSES: + if String(ch.get("class", "")) not in CLOTH_CLASSES: continue var names: Array = ch.get("bones", []) var tips: Array = ch.get("tips", []) diff --git a/debug/cloud_sun_response_capture.gd b/debug/cloud_sun_response_capture.gd new file mode 100644 index 0000000..120f5d1 --- /dev/null +++ b/debug/cloud_sun_response_capture.gd @@ -0,0 +1,63 @@ +extends SceneTree + +## Regression capture for directional cloud lighting. +## +## Runs the same Akiba street camera twice with the sun rotated 180 degrees. +## The bright cloud rim and colored lee-side band should visibly swap sides. +## +## godot --path . -s res://debug/cloud_sun_response_capture.gd -- + +var _frame := 0 +var _out_dir := "." +var _sun: DirectionalLight3D + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if not args.is_empty(): + _out_dir = args[0] + if not _out_dir.is_absolute_path(): + _out_dir = ProjectSettings.globalize_path(_out_dir) + var mkdir_error := DirAccess.make_dir_recursive_absolute(_out_dir) + if mkdir_error != OK: + printerr("cloud_sun_capture: could not create output directory") + quit(mkdir_error) + return + change_scene_to_file("res://ui/main_menu/main_menu.tscn") + + +func _process(_delta: float) -> bool: + _frame += 1 + if _frame == 60: + var network = root.get_node_or_null("NetworkManager") + if network and network.has_method("start_singleplayer_match"): + network.start_singleplayer_match(GameMode.DEATHMATCH) + change_scene_to_file("res://scenes/maps/neon_alley/neon_alley.tscn") + elif _frame == 240: + _sun = current_scene.get_node_or_null("Sun") as DirectionalLight3D + var camera := root.get_camera_3d() + if _sun == null or camera == null: + printerr("cloud_sun_capture: missing sun or camera") + return true + camera.global_position = Vector3(3, 2.2, 20) + camera.look_at(Vector3(-8, 4, -18), Vector3.UP) + _sun.rotation_degrees = Vector3(-28, -65, 0) + elif _frame == 280: + _shot("sun_west") + _sun.rotation_degrees = Vector3(-28, 115, 0) + elif _frame == 320: + _shot("sun_east") + unload_current_scene() + elif _frame == 440: + return true + return false + + +func _shot(tag: String) -> void: + var image := root.get_viewport().get_texture().get_image() + var path := _out_dir.path_join(tag + ".png") + var error := image.save_png(path) + if error == OK: + print("cloud_sun_capture: saved ", path) + else: + printerr("cloud_sun_capture: save failed with error ", error) diff --git a/debug/cloud_sun_response_capture.gd.uid b/debug/cloud_sun_response_capture.gd.uid new file mode 100644 index 0000000..864c63e --- /dev/null +++ b/debug/cloud_sun_response_capture.gd.uid @@ -0,0 +1 @@ +uid://hhe274y22xvp diff --git a/debug/dance_check.gd b/debug/dance_check.gd index c5c50e1..15e3e50 100644 --- a/debug/dance_check.gd +++ b/debug/dance_check.gd @@ -1,63 +1,28 @@ extends SceneTree -## Do the five emotes move the character, differ from each other, and OVERLAP? +## Regression gate for the authored emote catalog. ## -## godot --path . -s res://debug/dance_check.gd -## -## Three properties, and the third is the one worth checking. "It moves" and -## "they are different" are easy to satisfy by accident — five sine waves at five -## frequencies would pass both and would still look like programmer animation. -## What separates a dance from an oscillation is that the body moves as a CHAIN: -## the hips lead and the head arrives later. That is measurable, so it is. -## -## Sampled from inside the modifier pass, like every other pose check here. -## Outside it Godot restores the local poses and what gets measured is the -## animation clip alone — every routine would report identical motion whether or -## not the dance layer exists at all. +## The project used to synthesize five routines by adding sine-wave bone +## offsets to one dance clip. That made every emote look like programmer +## animation. The new contract is deliberately simpler: every wheel entry must +## select its own imported clip, the clip must move, and no DanceModifier may +## exist in the live character. -const CAPTURE_BEATS := 4.0 -const SAMPLES := 90 +const SETTLE_FRAMES := 24 +const SAMPLE_FRAMES := 36 +const MIN_FRAME_MOTION := 0.01 -## A routine has to move the character at least this far, in metres of total -## head travel over the sample window. Below this it is not an emote. -const MIN_TRAVEL := 0.05 -## Two routines must differ by at least this, comparing their per-frame pose -## trajectories. -const MIN_DISTINCT := 0.02 - -var _fails := 0 -var _probe: Probe = null - - -class Probe extends SkeletonModifier3D: - var pose: Array = [] - ## Each bone's OWN local pose rotation, which is what a phase measurement - ## needs. A bone's GLOBAL rotation contains every ancestor's rotation too, so - ## the head's global carries the hips' un-lagged swing as a large component - ## and correlates with it at a lag of zero no matter how much the head itself - ## is delayed. Measuring globals reported Two-Step as having no overlap at - ## all when its head is delayed by five links. - var local: Array = [] - - func _process_modification() -> void: - var skel := get_skeleton() - if skel == null: - return - pose.resize(skel.get_bone_count()) - local.resize(skel.get_bone_count()) - for i in skel.get_bone_count(): - pose[i] = skel.get_bone_global_pose(i) - local[i] = skel.get_bone_pose_rotation(i) +var _failures := 0 func _init() -> void: await process_frame - var mgr = root.get_node_or_null("SkinManager") - var skin = mgr.get_skin("taila") if mgr else null + var manager = root.get_node_or_null("SkinManager") + var skin = manager.get_skin("taila") if manager else null if skin == null or skin.model_path == "": - print("dance_check: no rigged skin to test with") - quit(1) + _expect(false, "Taila is available for the authored-emote test") + _done() return var model := SkinnedPlayerModel.new() @@ -68,368 +33,81 @@ func _init() -> void: model.update_state("idle", 0.0, false) await process_frame - if model._dance_mod == null: - _expect(false, "the model built a dance layer") - _done() - return - - var skel: Skeleton3D = model.skeleton - _probe = Probe.new() - skel.add_child(_probe) - skel.move_child(_probe, skel.get_child_count() - 1) - + _expect(model.loaded and model.skeleton != null, + "the authored character and skeleton load") _expect(DanceRoutines.count() == 5, - "there are five emotes (%d)" % DanceRoutines.count()) + "the wheel exposes five authored emotes (%d)" % DanceRoutines.count()) + _expect(model.find_children("*", "DanceModifier", true, false).is_empty(), + "no procedural DanceModifier is present") - var tracks := {} - for i in DanceRoutines.count(): - tracks[i] = await _sample(model, skel, i) + var resolved_clips: Array[String] = [] + for index in DanceRoutines.count(): + var canonical := DanceRoutines.clip_of(index) + var resolved: String = String(model._resolved_clips.get(canonical, "")) + _expect(resolved != "", + "%s resolves to an imported clip" % DanceRoutines.name_of(index)) + _expect(not resolved_clips.has(resolved), + "%s uses its own authored clip" % DanceRoutines.name_of(index)) + resolved_clips.append(resolved) - _report(tracks) - _compare(tracks) + model.set_dancing(true, index) + for _i in SETTLE_FRAMES: + model.update_state("idle", 0.0, false) + await process_frame + + _expect(model._current_clip == resolved, + "%s selects %s" % [DanceRoutines.name_of(index), canonical]) + + var previous := _pose(model.skeleton) + var greatest_motion := 0.0 + for _i in SAMPLE_FRAMES: + model.update_state("idle", 0.0, false) + await process_frame + var current := _pose(model.skeleton) + greatest_motion = maxf(greatest_motion, _pose_distance(previous, current)) + previous = current + _expect(greatest_motion >= MIN_FRAME_MOTION, + "%s visibly animates the skeleton (%.4f rad/frame)" + % [DanceRoutines.name_of(index), greatest_motion]) + + model.set_dancing(false) + for _i in 12: + model.update_state("idle", 0.0, false) + await process_frame + + model.queue_free() _done() -## One routine's trajectory: the hips' and head's positions, per frame, in the -## character's own space, plus the elbow angles for the joint-limit check. -func _sample(model, skel: Skeleton3D, index: int) -> Dictionary: - model.set_dancing(true, index) - # Let the blend arrive fully before recording, or the first routine sampled - # reports smaller motion than the rest purely because it was still fading in. - for _i in 40: - model.update_state("idle", 0.0, false) - await process_frame - - var mod = model._pose_mod - var hips: int = mod._idx.get("DEF-hips", -1) - var head: int = mod._idx.get("DEF-head", -1) - # Overlap is measured between two links of the SAME chain, not between the - # hips and the head. - # - # Every spine link is driven by the same channels (`spine_roll`, `spine_yaw`, - # `spine_pitch`) at `beat - lag * i`, so the only difference between them IS - # the lag. The hips and the head are driven by DIFFERENT channels, often at - # different periods — Two-Step's hips roll on a two-beat cycle while its head - # bobs on a one-beat one — so correlating those two compares signals that do - # not have a phase relationship to find. - var link_a: int = mod._idx.get("DEF-spine.001", -1) - var link_b: int = mod._idx.get("DEF-spine.003", -1) - var fa_r: int = mod._idx.get("DEF-forearm.R", -1) - var ua_r: int = mod._idx.get("DEF-upper_arm.R", -1) - var hand_r: int = mod._idx.get("DEF-hand.R", -1) - - var hip_track: Array = [] - var head_track: Array = [] - # The SAME quantity at two points in the chain: how far each bone has been - # rotated away from its own rest pose, signed. Correlating the hips' world - # TRANSLATION against the head's position RELATIVE to the hips was comparing - # two different physical quantities driven by different channels at different - # periods, and the peak landed anywhere — it reported the Robot, whose lag is - # zero by construction, as the most overlapped routine in the set. - var hip_rot: Array = [] - var head_rot: Array = [] - var worst_elbow := 180.0 - - for _i in SAMPLES: - model.update_state("idle", 0.0, false) - await process_frame - var pose: Array = _probe.pose - if pose.size() <= maxi(hips, head) or hips < 0 or head < 0: - continue - var origin: Vector3 = pose[hips].origin - hip_track.append(origin) - head_track.append(pose[head].origin - origin) - hip_rot.append(_twist(skel, _probe.local, link_a)) - head_rot.append(_twist(skel, _probe.local, link_b)) - # The elbow must never open past straight. A signed wave on a forearm - # bends it backwards through the joint on half of every cycle, which is - # the single most obvious tell in procedural animation. - if ua_r >= 0 and fa_r >= 0 and hand_r >= 0 and pose.size() > hand_r: - var upper: Vector3 = (pose[ua_r].origin - pose[fa_r].origin).normalized() - var lower: Vector3 = (pose[hand_r].origin - pose[fa_r].origin).normalized() - worst_elbow = minf(worst_elbow, rad_to_deg(acos(clampf( - upper.dot(lower), -1.0, 1.0)))) - - model.set_dancing(false) - for _i in 30: - model.update_state("idle", 0.0, false) - await process_frame - - return {"hips": hip_track, "head": head_track, "elbow": worst_elbow, - "hip_rot": hip_rot, "head_rot": head_rot} +func _pose(skeleton: Skeleton3D) -> Array[Quaternion]: + var result: Array[Quaternion] = [] + result.resize(skeleton.get_bone_count()) + for bone in skeleton.get_bone_count(): + result[bone] = skeleton.get_bone_pose_rotation(bone) + return result -func _report(tracks: Dictionary) -> void: - print("\n=== EMOTES ===") - for i in tracks: - var t: Dictionary = tracks[i] - var travel := _travel(t["head"]) - var hip_travel := _travel(t["hips"]) - var lag := _lag(t["hip_rot"], t["head_rot"]) - print(" %-12s head %.3f m hips %.3f m upper spine lags lower by %d frames min elbow %.0f deg" - % [DanceRoutines.name_of(i), travel, hip_travel, lag, t["elbow"]]) - - -func _compare(tracks: Dictionary) -> void: - for i in tracks: - var t: Dictionary = tracks[i] - var nm := DanceRoutines.name_of(i) - _expect(_travel(t["head"]) >= MIN_TRAVEL, - "%s actually moves the character (%.3f m)" % [nm, _travel(t["head"])]) - # 8 degrees of slack: the IK and the idle clip underneath both contribute, - # and an elbow that never quite straightens is correct anyway. - _expect(t["elbow"] >= 8.0, - "%s never hyperextends the elbow (min %.0f deg)" % [nm, t["elbow"]]) - - var ids: Array = tracks.keys() - for i in ids.size(): - for j in range(i + 1, ids.size()): - var d := _difference(tracks[ids[i]]["head"], tracks[ids[j]]["head"]) - _expect(d >= MIN_DISTINCT, - "%s and %s are different dances (%.3f)" - % [DanceRoutines.name_of(ids[i]), DanceRoutines.name_of(ids[j]), d]) - - # OVERLAP. The head must trail the hips, because the body is a chain — this - # is the property that separates a dance from five bones oscillating in - # phase, and it is the whole reason `lag` exists in the routine data. - # - # The robot is exempt and deliberately so: its lag is zero on purpose, which - # is what makes it read as mechanical against the other four. - for i in tracks: - var rid: String = String(DanceRoutines.get_routine(i).get("id", "")) - # Robot: lag zero by construction, which is the point of it. - # Spin: the head SPOTS — it holds its heading against the turn and whips - # round to catch up, so it is deliberately not a delayed copy of the - # hips. Asserting that it follows them would be asserting the opposite of - # the technique. - if rid == "robot" or rid == "spin": - continue - var lag := _lag(tracks[i]["hip_rot"], tracks[i]["head_rot"]) - _expect(lag > 0, - "%s moves as a chain — the upper spine trails the lower by %d frames" - % [DanceRoutines.name_of(i), lag]) - # The Robot's own property is that its motion is QUANTISED: it holds a pose - # and jumps, where the others move continuously. That is what `steps` in the - # routine data produces and what makes it read as mechanical against the - # other four. - # - # Its LAG is deliberately not asserted. Zero lag ought to correlate perfectly - # at shift 0, but the signal is a staircase with 16-frame plateaus, so many - # shifts score nearly identically and the measured peak wanders — it reported - # 21 frames. Asserting a number the measurement cannot resolve would be - # asserting noise; the hold fraction below is the property that is actually - # there. - var robot := DanceRoutines.index_of("robot") - var robot_step := _step_size(tracks[robot]["head_rot"]) - for i in tracks: - if i == robot: - continue - var other := _step_size(tracks[i]["head_rot"]) - _expect(robot_step > other * 1.5, - "Robot JUMPS between poses where %s flows (%.2f vs %.2f of range per frame)" - % [DanceRoutines.name_of(i), robot_step, other]) - - -## The largest single-frame change, as a fraction of the track's whole range. -## -## This is what quantised motion looks like from the outside: long flat stretches -## punctuated by one big jump. A smooth wave never moves more than a few percent -## of its range in a frame however punchy its easing. -## -## Measured as a JUMP rather than as time-spent-still, which was the first -## attempt and does not separate them: a shaped wave hangs at its extremes by -## design, so Two-Step scored the same 0.97 "holding" as the Robot did. The -## routines differ in HOW THEY LEAVE a pose, not in how long they sit in one. -func _step_size(rot_track: Array) -> float: - var track := _project(rot_track) - var n := track.size() - if n < 4: +func _pose_distance(a: Array, b: Array) -> float: + var count := mini(a.size(), b.size()) + if count == 0: return 0.0 - var lo := 1e30 - var hi := -1e30 - for v in track: - lo = minf(lo, v) - hi = maxf(hi, v) - var span: float = hi - lo - if span < 0.000001: - return 0.0 - var biggest := 0.0 - for i in range(1, n): - biggest = maxf(biggest, absf(track[i] - track[i - 1])) - return biggest / span + var greatest := 0.0 + for index in count: + var qa: Quaternion = a[index] + var qb: Quaternion = b[index] + greatest = maxf(greatest, qa.angle_to(qb)) + return greatest -## Total path length of a track. -func _travel(track: Array) -> float: - var sum := 0.0 - for i in range(1, track.size()): - sum += (track[i] as Vector3).distance_to(track[i - 1]) - return sum - - -## Mean per-frame distance between two tracks, after removing each one's own -## average position — otherwise two identical dances at different heights would -## read as different, and two different dances at the same height as the same. -func _difference(a: Array, b: Array) -> float: - var n := mini(a.size(), b.size()) - if n == 0: - return 0.0 - var ca := Vector3.ZERO - var cb := Vector3.ZERO - for i in n: - ca += a[i] - cb += b[i] - ca /= float(n) - cb /= float(n) - var sum := 0.0 - for i in n: - sum += ((a[i] - ca) - (b[i] - cb)).length() - return sum / float(n) - - -## How many frames the head's rotation trails the hips', by NORMALISED -## cross-correlation. -## -## Both signals are the same quantity — a bone's rotation away from its own rest -## pose — sampled at two ends of the same chain, so the only thing that can -## differ between them is timing. That is the whole point: an unnormalised -## correlation between two DIFFERENT quantities peaks wherever their amplitudes -## happen to line up, which reported the Robot (lag zero by construction) as the -## most overlapped routine in the set. -## -## Pearson, so amplitude cannot influence where the peak falls — a head that -## moves further than the hips must not read as a head that moves later. -func _lag(hips: Array, head: Array) -> int: - # BOTH ends projected onto the HIPS' axis, not each onto its own. - # - # Overlap is "the same motion, later", so the measurement has to be of the - # same motion. Projecting each end onto its own dominant axis compares - # whatever channel happens to dominate at that end, and routines drive - # different channels at the two ends: Two-Step's hips are dominated by a - # two-beat roll while its head is dominated by a one-beat bob, so the - # correlation was between signals of different PERIOD and peaked wherever. - var axis := _dominant_axis(hips) - var a := _centre(_project(hips, axis)) - var b := _centre(_project(head, axis)) - var n := mini(a.size(), b.size()) - if n < 16: - return 0 - var best := 0 - var best_score := -1e30 - # Out to half the window. The correlation of a periodic signal repeats every - # period, so the search must stay inside one; Body Wave has the largest lag - # in the set by design (0.13 s per link over five links, most of a beat at - # 88 bpm) and a short window could not see it at all. - for shift in range(0, n / 2): - var sum := 0.0 - var na := 0.0 - var nb := 0.0 - for i in range(0, n - shift): - sum += a[i] * b[i + shift] - na += a[i] * a[i] - nb += b[i + shift] * b[i + shift] - if na < 0.000001 or nb < 0.000001: - continue - var score: float = sum / sqrt(na * nb) - if score > best_score: - best_score = score - best = shift - return best - - -## Mean-removed copy of a scalar track. -func _centre(track: Array) -> Array: - var n := track.size() - if n == 0: - return [] - var mean := 0.0 - for v in track: - mean += v - mean /= float(n) - var out: Array = [] - for v in track: - out.append(v - mean) - return out - - -## How far a bone has been rotated away from its rest pose, as a ROTATION VECTOR -## (axis times angle). -## -## A vector, not a signed scalar. The first version returned `angle * sign of the -## axis's largest component`, and that is discontinuous: as a rocking bone passes -## back through its rest pose the angle goes to zero and the axis FLIPS, so the -## signal jumped the full width of its range in a single frame. Two-Step measured -## a per-frame step of 0.99 of its own range — which looked exactly like the -## quantised motion the Robot is supposed to have exclusively, on a routine that -## is perfectly smooth. -## -## The rotation vector passes through zero and comes out the other side pointing -## the opposite way, which is continuous, and projecting it onto a fixed axis -## afterwards gives the signed wave the analysis actually wants. -func _twist(skel: Skeleton3D, local: Array, idx: int) -> Vector3: - if idx < 0 or idx >= local.size(): - return Vector3.ZERO - # The bone's OWN rotation away from its rest — not its global, which carries - # every ancestor's along with it. See Probe.local. - var rest: Quaternion = skel.get_bone_rest(idx).basis.get_rotation_quaternion() - var d := (rest.inverse() * (local[idx] as Quaternion)).normalized() - # Shortest arc, so a rotation just past 180 degrees does not read as one just - # under -180. - if d.w < 0.0: - d = Quaternion(-d.x, -d.y, -d.z, -d.w) - var ang := d.get_angle() - if ang < 0.000001: - return Vector3.ZERO - return d.get_axis() * ang - - -## The axis a track of rotation vectors varies most about. -func _dominant_axis(track: Array) -> Vector3: - var n := track.size() - if n == 0: - return Vector3.ZERO - var mean := Vector3.ZERO - for v in track: - mean += v - mean /= float(n) - var axis := Vector3.ZERO - var best := 0.0 - for v in track: - var d: Vector3 = v - mean - if d.length() > best: - best = d.length() - axis = d - return axis.normalized() if axis.length() > 0.000001 else Vector3.ZERO - - -## A track of rotation vectors flattened to one signed scalar per frame, along -## `axis` — or along the track's own dominant axis if none is given. -func _project(track: Array, axis: Vector3 = Vector3.ZERO) -> Array: - var n := track.size() - if n == 0: - return [] - var use := axis if axis.length() > 0.000001 else _dominant_axis(track) - if use.length() < 0.000001: - return [] - var mean := Vector3.ZERO - for v in track: - mean += v - mean /= float(n) - var out: Array = [] - for v in track: - out.append((v - mean).dot(use)) - return out - - -func _expect(ok: bool, what: String) -> void: +func _expect(ok: bool, description: String) -> void: if ok: - print(" OK: ", what) + print(" OK: ", description) else: - print(" FAIL: ", what) - _fails += 1 + printerr(" FAIL: ", description) + _failures += 1 func _done() -> void: - print("\n=== DANCE SUMMARY ===") - print("Failures: %d" % _fails) - quit(1 if _fails > 0 else 0) + print("\n=== AUTHORED EMOTE SUMMARY ===") + print("Failures: %d" % _failures) + quit(1 if _failures > 0 else 0) diff --git a/debug/dust2_level_builder.gd b/debug/dust2_level_builder.gd index 62bb6d3..5d6ba23 100644 --- a/debug/dust2_level_builder.gd +++ b/debug/dust2_level_builder.gd @@ -1,5 +1,11 @@ extends TestLevelBuilder +func _build_environment() -> void: + # Dust 2 uses the warm sunset palette. Selecting it through the shared + # environment updates both the sky and the fullscreen ink pass. + LevelEnvironment.add_to(self, "sunset") + + func _build_geometry() -> void: # Build environment setup _build_environment() @@ -10,10 +16,9 @@ func _build_geometry() -> void: # (keep the shared linear tonemap — ACES crushes the cel bands) if environment.sky and environment.sky.sky_material: var sky_mat = environment.sky.sky_material - sky_mat.sky_top_color = Color(0.3, 0.5, 0.8) - sky_mat.sky_horizon_color = Color(0.8, 0.7, 0.5) - sky_mat.ground_bottom_color = Color(0.2, 0.15, 0.1) - sky_mat.ground_horizon_color = Color(0.8, 0.7, 0.5) + sky_mat.set_shader_parameter("top_color", Color(0.3, 0.5, 0.8)) + sky_mat.set_shader_parameter("horizon_color", Color(0.8, 0.7, 0.5)) + sky_mat.set_shader_parameter("ground_color", Color(0.2, 0.15, 0.1)) _build_lighting() var sun = get_node_or_null("Sun") @@ -192,17 +197,10 @@ func _build_dust2_layout() -> void: _build_dust2_dummies() # Combat Area Bounds - var combat_area = load("res://entities/combat_area.gd").new() - combat_area.name = "CombatArea" - var area_shape = CollisionShape3D.new() - var cbox = BoxShape3D.new() - # Size the combat area to envelop the entire playable space (width and depth of 120, height of 20) - cbox.size = Vector3(120, 20, 120) - area_shape.shape = cbox - combat_area.add_child(area_shape) - # Center it on the map so it fits perfectly around the CSG boundaries - combat_area.position = Vector3(0, 10, 0) - add_child(combat_area) + # Envelops the playable space; centred so it fits around the CSG boundaries. + # Deeper than the old 20 m box, which sat with its floor at y = 0 and so + # never actually contained a player who had fallen below the map. + CombatArea.add_to(self, Vector3(120, 70, 120), Vector3(0, 25, 0)) func _build_dust2_dummies() -> void: # Do not spawn dummies in real multiplayer matches diff --git a/debug/fidelity_probe.gd b/debug/fidelity_probe.gd new file mode 100644 index 0000000..6c47b23 --- /dev/null +++ b/debug/fidelity_probe.gd @@ -0,0 +1,147 @@ +extends SceneTree + +## Deterministic set-piece for judging the LOOK, not the level. +## +## visual_capture.gd shoots the real game, which is the right tool for "does +## this still work" and the wrong one for "did that shader change help": the +## player spawns at a random spawn point, weapons idle-sway, and characters +## animate, so two runs of the same build produce different frames. Measuring +## those with tools/levels.py gives numbers that bounce by 15 and hide every +## delta smaller than the noise. +## +## This builds a fixed scene instead — known primitives on a known ground plane, +## a fixed sun angle, a fixed camera — so the ONLY thing that can move a +## statistic is the change under test. The shapes are chosen to exercise each +## part of the cel look: +## +## sphere / cylinder curved terminator (the band-edge anti-aliasing case) +## box flat faces meeting at hard angles (crisp break case) +## the low slab casts across the ground plane (cast-shadow case) +## the overhang deep occlusion right above a surface (contact/AO case) +## the stack object-on-object contact, the "does it float" case +## +## Run: +## godot --path . --windowed --resolution 1280x720 \ +## -s res://debug/fidelity_probe.gd -- +## +## Writes probe_key.png (three-quarter hero angle) and probe_graze.png (low +## raking angle, where terminator crawl and outline weight show worst). + +var _frames := 0 +var _out_dir := "." +var _root: Node3D +var _cam: Camera3D + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if args.size() > 0: + _out_dir = args[0] + _build() + + +func _build() -> void: + _root = Node3D.new() + _root.name = "FidelityProbe" + root.add_child(_root) + + LevelEnvironment.add_to(_root, "day") + # Pin the sun rather than take the shared default, so a change to the + # shared default cannot silently move the probe's baseline. Raking enough + # that everything throws a long, legible shadow across the plane. + var sun: DirectionalLight3D = _root.get_node("Sun") + sun.rotation_degrees = Vector3(-34, 42, 0) + sun.light_energy = 2.2 + sun.directional_shadow_max_distance = 80.0 + + # Ground: one large plane, deliberately untextured flat cel colour. This is + # the surface the `detail` statistic is measured against — if a surface + # detail law is doing anything, it shows up here first. + var ground := MeshInstance3D.new() + var plane := PlaneMesh.new() + plane.size = Vector2(60, 60) + ground.mesh = plane + ground.material_override = LevelMaterials.flat(Color(0.52, 0.50, 0.58), "ground") + _root.add_child(ground) + + var wall_col := Color(0.62, 0.58, 0.66) + var prop_col := Color(0.80, 0.78, 0.84) + + # Back wall — a big flat expanse, the "dead%" case. + _box(Vector3(0, 4, -9), Vector3(30, 8, 0.6), wall_col, "wall") + # Low slab: throws a long shadow across open ground. + _box(Vector3(-6, 0.5, 2), Vector3(4, 1, 1.2), prop_col, "trim") + # Overhang: a lid on two legs, deep occlusion directly above the ground. + _box(Vector3(5, 2.6, 0), Vector3(5, 0.4, 4), prop_col, "panel") + _box(Vector3(3.2, 1.3, -1.4), Vector3(0.4, 2.2, 0.4), prop_col) + _box(Vector3(6.8, 1.3, 1.4), Vector3(0.4, 2.2, 0.4), prop_col) + # Stack: object resting on object, the classic "does it float" read. + _box(Vector3(0, 0.6, 3.5), Vector3(2.4, 1.2, 2.4), prop_col, "panel") + _box(Vector3(0, 1.6, 3.5), Vector3(1.2, 0.8, 1.2), Color(0.86, 0.52, 0.34)) + + # Curved surfaces: the terminator that crawls if the band edge is not + # anti-aliased in screen space. + _shape(SphereMesh.new(), Vector3(-2.5, 1.2, 0.5), prop_col, 1.2) + var cyl := CylinderMesh.new() + cyl.top_radius = 0.5 + cyl.bottom_radius = 0.5 + cyl.height = 3.4 + _shape(cyl, Vector3(-4.5, 1.7, -3.0), Color(0.42, 0.62, 0.72), 1.0) + + _cam = Camera3D.new() + _root.add_child(_cam) + _cam.current = true + _aim_key() + + +func _box(pos: Vector3, size: Vector3, col: Color, law: String = "") -> void: + var mi := MeshInstance3D.new() + var m := BoxMesh.new() + m.size = size + mi.mesh = m + mi.position = pos + mi.material_override = LevelMaterials.flat(col, law) + _root.add_child(mi) + + +func _shape(mesh: Mesh, pos: Vector3, col: Color, scale: float) -> void: + var mi := MeshInstance3D.new() + if mesh is SphereMesh: + mesh.radius = 1.0 + mesh.height = 2.0 + mi.mesh = mesh + mi.position = pos + mi.scale = Vector3.ONE * scale + mi.material_override = LevelMaterials.flat(col) + _root.add_child(mi) + + +## look_at_from_position, not position-then-look_at: _initialize() runs before +## the node is actually in the tree, and look_at needs a global transform. +func _aim_key() -> void: + _cam.look_at_from_position(Vector3(7.5, 5.2, 12.0), Vector3(0.0, 1.0, 0.5), Vector3.UP) + + +func _aim_graze() -> void: + _cam.look_at_from_position(Vector3(-9.0, 1.05, 7.5), Vector3(2.0, 1.4, -2.0), Vector3.UP) + + +func _process(_delta: float) -> bool: + _frames += 1 + # 90 frames of settle: shaders compile on first use and the first frames + # render with the fallback material, which would show up as a huge and + # entirely fake improvement in every statistic. + if _frames == 90: + _shot("key") + _aim_graze() + elif _frames == 120: + _shot("graze") + return true + return false + + +func _shot(tag: String) -> void: + var img := root.get_viewport().get_texture().get_image() + var path := _out_dir + "/probe_" + tag + ".png" + img.save_png(path) + print("fidelity_probe: saved ", path) diff --git a/debug/fidelity_probe.gd.uid b/debug/fidelity_probe.gd.uid new file mode 100644 index 0000000..4db9a6f --- /dev/null +++ b/debug/fidelity_probe.gd.uid @@ -0,0 +1 @@ +uid://coy68t4v8bnk0 diff --git a/debug/fp_movement_capture.gd b/debug/fp_movement_capture.gd new file mode 100644 index 0000000..9c26647 --- /dev/null +++ b/debug/fp_movement_capture.gd @@ -0,0 +1,141 @@ +extends SceneTree + +## Visual and structural gate for first-person movement handling. +## +## Run: +## godot --path . --resolution 1280x720 \ +## --script res://debug/fp_movement_capture.gd -- + +const DT := 1.0 / 60.0 +const SETTLE_FRAMES := 42 + +var _out_dir := "." +var _frames := 0 +var _booted := false +var _player: CharacterBody3D = null +var _machine: Node = null +var _manager: Node = null +var _case_index := 0 +var _case_frame := 0 +var _failures: Array[String] = [] + +# tag, state, velocity, wall side, crouching +var _cases := [ + ["idle", "idle", Vector3.ZERO, 0.0, false], + ["jump", "air", Vector3(0.0, 9.0, -6.0), 0.0, false], + ["fall", "air", Vector3(0.0, -11.0, -7.0), 0.0, false], + ["dash", "dash", Vector3(0.0, 0.0, -18.0), 0.0, false], + ["slide", "slide", Vector3(0.0, 0.0, -13.0), 0.0, true], + ["wallrun_left", "wall_run", Vector3(0.0, 1.0, -11.0), -1.0, false], + ["wallrun_right", "wall_run", Vector3(0.0, 1.0, -11.0), 1.0, false], + ["wall_cling", "wall_cling", Vector3(0.0, 0.0, -2.0), -1.0, false], + ["grapple", "grapple", Vector3(4.0, 5.0, -15.0), 0.0, false], +] + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if not args.is_empty(): + _out_dir = args[0] + if not _out_dir.is_absolute_path(): + _out_dir = ProjectSettings.globalize_path(_out_dir) + var mkdir_error := DirAccess.make_dir_recursive_absolute(_out_dir) + if mkdir_error != OK: + printerr("FP_MOVEMENT could not create '%s' (error %d)" % [ + _out_dir, mkdir_error]) + quit(mkdir_error) + return + change_scene_to_file("res://ui/main_menu/main_menu.tscn") + + +func _process(_delta: float) -> bool: + _frames += 1 + if not _booted and _frames == 40: + _booted = true + var network = root.get_node_or_null("NetworkManager") + if network and network.has_method("start_singleplayer_match"): + network.start_singleplayer_match(GameMode.DEATHMATCH) + change_scene_to_file("res://scenes/maps/test_level/test_level.tscn") + return false + if _frames < 160: + return false + if _player == null: + if not _setup(): + _finish() + return true + + var entry: Array = _cases[_case_index] + _machine.current_state = entry[1] + _machine.wall_side = entry[3] + _machine.input_crouch = entry[4] + _player.velocity = entry[2] + _manager._process(DT) + _case_frame += 1 + if _case_frame >= SETTLE_FRAMES: + _snap("fp_move_" + String(entry[0])) + var pose: Dictionary = _manager.movement_pose_debug() + var position: Vector3 = pose.get("position", Vector3.ZERO) + var rotation: Vector3 = pose.get("rotation", Vector3.ZERO) + if entry[0] != "idle" and position.length() < 0.012 \ + and rotation.length() < 0.025: + _failures.append("%s did not produce a readable movement pose" % entry[0]) + print("FP_MOVEMENT pose %s position=%s rotation_deg=%s" % [ + entry[0], position, rotation * (180.0 / PI)]) + _case_index += 1 + _case_frame = 0 + if _case_index >= _cases.size(): + _finish() + return true + return false + + +func _setup() -> bool: + for candidate in root.find_children("*", "CharacterBody3D", true, false): + if candidate.has_method("get_visual_model") \ + and candidate.is_multiplayer_authority(): + _player = candidate + break + if _player == null: + _failures.append("local player was not created") + return false + _machine = _player.get_node_or_null("MovementStateMachine") + _manager = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager") + if _machine == null: + _failures.append("MovementStateMachine is missing") + if _manager == null: + _failures.append("WeaponManager is missing") + if not _failures.is_empty(): + return false + _player.set_physics_process(false) + _machine.set_physics_process(false) + _manager.set_process(false) + _player.global_position = Vector3(0.0, 1.2, 14.0) + + var obsolete := root.find_children( + "FirstPersonBody", "Node", true, false) + if not obsolete.is_empty(): + _failures.append("obsolete first-person body/legs still exist") + if _manager.weapons.is_empty(): + _failures.append("viewmodel weapon loadout is empty") + return _failures.is_empty() + + +func _snap(tag: String) -> void: + var image := root.get_viewport().get_texture().get_image() + var path := _out_dir.path_join(tag + ".png") + var error := image.save_png(path) + if error == OK: + print("FP_MOVEMENT saved ", path) + else: + _failures.append("could not save %s (error %d)" % [path, error]) + + +func _finish() -> void: + if _failures.is_empty() and _case_index >= _cases.size(): + print("FP_MOVEMENT PASS: %d authored poses, no first-person legs" \ + % _cases.size()) + quit(0) + else: + for failure in _failures: + push_error("FP_MOVEMENT FAIL: " + failure) + quit(1) diff --git a/debug/fp_movement_capture.gd.uid b/debug/fp_movement_capture.gd.uid new file mode 100644 index 0000000..a4feff0 --- /dev/null +++ b/debug/fp_movement_capture.gd.uid @@ -0,0 +1 @@ +uid://c183tdow8n6dn diff --git a/debug/fp_weapon_capture.gd b/debug/fp_weapon_capture.gd index a3162ce..0ca676d 100644 --- a/debug/fp_weapon_capture.gd +++ b/debug/fp_weapon_capture.gd @@ -7,6 +7,8 @@ extends SceneTree var _frames := 0 var _out_dir := "." +var _skin_id := "" +var _quick := false var _player: Node = null var _wman: Node = null var _mode := "boot" @@ -32,6 +34,10 @@ func _initialize() -> void: var args := OS.get_cmdline_user_args() if args.size() > 0: _out_dir = args[0] + if args.size() > 1: + _skin_id = args[1] + if args.size() > 2: + _quick = args[2] == "quick" change_scene_to_file("res://ui/main_menu/main_menu.tscn") @@ -39,6 +45,12 @@ func _process(_delta: float) -> bool: _frames += 1 if _mode == "boot": if _frames == 40: + var skin_manager = root.get_node_or_null("SkinManager") + if not _skin_id.is_empty() and skin_manager \ + and skin_manager.skins.has(_skin_id): + # Capture-only override: do not persist over the player's actual + # skin selection in user://. + skin_manager.active_skin_id = _skin_id var nm = root.get_node_or_null("NetworkManager") if nm and nm.has_method("start_singleplayer_match"): nm.start_singleplayer_match(GameMode.DEATHMATCH) @@ -62,7 +74,30 @@ func _process(_delta: float) -> bool: return true _player.set_physics_process(false) _player.global_position = Vector3(0, 1.2, 14) - _mode = "spawn" + _mode = "quick" if _quick else "spawn" + _phase_frame = 0 + return false + + if _mode == "quick": + _phase_frame += 1 + if _phase_frame == 15: + if not _skin_id.is_empty(): + _verify_skin(_skin_id) + _snap("fp_quick_idle") + var quick_weapon = _wman.weapons.get(_wman.active_slot) + if quick_weapon and quick_weapon.has_method("_start_reload"): + quick_weapon._start_reload() + elif _phase_frame == 32: + _snap("fp_quick_reload") + elif _phase_frame == 38: + var skin_manager = root.get_node_or_null("SkinManager") + if skin_manager: + skin_manager.skin_changed.emit("aria") + elif _phase_frame == 48: + _verify_skin("aria") + _snap("fp_quick_skin_swap") + elif _phase_frame >= 55: + return true return false if _mode == "spawn": @@ -121,7 +156,9 @@ func _process(_delta: float) -> bool: if _mode == "knife_swing": _phase_frame += 1 - if _phase_frame == 20: + if _phase_frame == 10: + _snap("fp_knife_guard") + elif _phase_frame == 20: var knife = _wman.weapons.get(4) if knife and knife.has_method("_swing"): knife._swing() @@ -154,3 +191,47 @@ func _snap(tag: String) -> void: var path := _out_dir + "/" + tag + ".png" img.save_png(path) print("fp_capture: saved ", path) + + +func _verify_skin(skin_id: String) -> void: + var skin_manager = root.get_node_or_null("SkinManager") + var weapon = _wman.weapons.get(_wman.active_slot) if _wman else null + var sleeve = weapon.get_node_or_null("ArmR/Sleeve") if weapon else null + var hand = weapon.get_node_or_null("ArmR/Hand") if weapon else null + if skin_manager == null or sleeve == null or hand == null: + printerr("fp_capture: skin swap verification missing arm") + return + var material = sleeve.get_active_material(0) + var actual = material.get_shader_parameter("albedo_color") \ + if material is ShaderMaterial else null + var expected: Color = skin_manager.get_skin(skin_id).viewmodel_sleeve_color + var selected_skin: PlayerSkin = skin_manager.get_skin(skin_id) + var palm := hand.get_node_or_null("Palm") as MeshInstance3D + var palm_material := palm.get_active_material(0) if palm else null + var palm_actual = palm_material.get_shader_parameter("albedo_color") \ + if palm_material is ShaderMaterial else null + var palm_expected: Color + match selected_skin.viewmodel_hand_style: + "bare": + palm_expected = selected_skin.viewmodel_skin_color + "android": + palm_expected = selected_skin.viewmodel_accent_color + _: + palm_expected = selected_skin.viewmodel_glove_color + var finger_segments: int = hand.find_children( + "Finger*", "MeshInstance3D", true, false).size() + var thumb_segments: int = hand.find_children( + "Thumb*", "MeshInstance3D", true, false).size() + if not (actual is Color and actual.is_equal_approx(expected)): + printerr("fp_capture: skin swap mismatch actual=%s expected=%s" % [ + actual, expected]) + elif not (palm_actual is Color and palm_actual.is_equal_approx(palm_expected)): + printerr("fp_capture: hand palette mismatch actual=%s expected=%s" % [ + palm_actual, palm_expected]) + elif finger_segments != 8 or thumb_segments != 2: + printerr("fp_capture: incomplete anime hand fingers=%d thumb=%d" % [ + finger_segments, thumb_segments]) + else: + print("fp_capture: live skin swap verified %s style=%s fingers=%d thumb=%d" % [ + skin_id, selected_skin.viewmodel_hand_style, + finger_segments, thumb_segments]) diff --git a/debug/jet_pose_capture.gd b/debug/jet_pose_capture.gd new file mode 100644 index 0000000..1a82dbc --- /dev/null +++ b/debug/jet_pose_capture.gd @@ -0,0 +1,132 @@ +extends SceneTree + +## Rendered QA for the movement-fidelity pass. +## Captures rear views of both thrust modes and side views of reverse/braking. + +var _frames := 0 +var _out_dir := "." +var _player: Node = null +var _model: Node = null +var _jets: PlayerJetVFX = null +var _camera: Camera3D = null +var _phase := 0 +var _phase_frame := 0 + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if not args.is_empty(): + _out_dir = args[0] + if not _out_dir.is_absolute_path(): + _out_dir = ProjectSettings.globalize_path(_out_dir) + var err := DirAccess.make_dir_recursive_absolute(_out_dir) + if err != OK: + printerr("jet_pose_capture: could not create output directory") + quit(err) + return + change_scene_to_file("res://ui/main_menu/main_menu.tscn") + + +func _process(_delta: float) -> bool: + _frames += 1 + if _frames == 35: + var skins = root.get_node_or_null("SkinManager") + if skins: + skins.set_active_skin("miku") + var network = root.get_node_or_null("NetworkManager") + if network and network.has_method("start_singleplayer_match"): + network.start_singleplayer_match(GameMode.DEATHMATCH) + change_scene_to_file("res://scenes/maps/test_level/test_level.tscn") + return false + if _frames < 145: + return false + if _frames == 145: + for candidate in root.find_children("*", "CharacterBody3D", true, false): + if candidate.has_method("get_visual_model") \ + and candidate.is_multiplayer_authority(): + _player = candidate + break + if not _player: + printerr("jet_pose_capture: player not found") + return true + _player.set_third_person(true) + _model = _player.get_visual_model() + _jets = _player.get_node_or_null("JetVFX") + if not _model or not _jets: + printerr("jet_pose_capture: model or jet rig not found") + return true + _player.set_physics_process(false) + _player.set_process(false) + _player.global_position = Vector3(0, 1.2, 14) + _player.rotation = Vector3.ZERO + _model.set_owner_visible(true) + _jets.set_render_enabled(true) + _jets.set_visual_model(_model) + _camera = Camera3D.new() + _camera.cull_mask &= ~(1 << 19) + current_scene.add_child(_camera) + _camera.current = true + _side_camera() + return false + + _phase_frame += 1 + match _phase: + 0: + _model.update_state("ground", 8.0, false) + _model.set_locomotion(0.0, -1.0, 0.0) + if _phase_frame == 48: + _snap("backward_lean") + _next_phase() + 1: + _model.set_locomotion(0.0, 1.0, 0.0) + _model.update_state("ground", 12.0 if _phase_frame < 12 else 5.0, false) + if _phase_frame == 18: + _snap("brake_pose") + _next_phase() + _rear_camera() + 2: + _model.update_state("dash", 14.0, false) + _model.set_locomotion(0.0, 1.0, 0.0) + _jets.set_dash(true, Vector2(0.0, 1.0)) + if _phase_frame == 32: + _snap("dash_jets") + _next_phase() + 3: + _model.update_state("air", 5.0, false) + _model.set_locomotion(0.0, 1.0, 0.0) + _jets.set_dash(false, Vector2.ZERO) + if _phase_frame == 1: + _jets.burst_double_jump() + if _phase_frame == 7: + _snap("double_jump_jets") + unload_current_scene() + _next_phase() + _: + if _phase_frame > 60: + print("JET_POSE_CAPTURE PASS") + return true + return false + + +func _next_phase() -> void: + _phase += 1 + _phase_frame = 0 + + +func _side_camera() -> void: + _camera.global_position = _player.global_position + Vector3(2.7, 0.9, 0.0) + _camera.look_at(_player.global_position + Vector3(0.0, 0.75, 0.0), Vector3.UP) + + +func _rear_camera() -> void: + _camera.global_position = _player.global_position + Vector3(2.15, 1.0, 2.8) + _camera.look_at(_player.global_position + Vector3(0.0, 0.72, 0.0), Vector3.UP) + + +func _snap(tag: String) -> void: + var path := _out_dir.path_join(tag + ".png") + var err := root.get_viewport().get_texture().get_image().save_png(path) + if err == OK: + print("jet_pose_capture: saved ", path) + else: + printerr("jet_pose_capture: failed to save ", path) diff --git a/debug/jet_pose_capture.gd.uid b/debug/jet_pose_capture.gd.uid new file mode 100644 index 0000000..3ac88ba --- /dev/null +++ b/debug/jet_pose_capture.gd.uid @@ -0,0 +1 @@ +uid://d3kckdjoe77uu diff --git a/debug/jet_vfx_test.gd b/debug/jet_vfx_test.gd new file mode 100644 index 0000000..f5757bb --- /dev/null +++ b/debug/jet_vfx_test.gd @@ -0,0 +1,56 @@ +extends SceneTree + +var _failures: Array[String] = [] + + +func _initialize() -> void: + call_deferred("_run") + + +func _expect(condition: bool, message: String) -> void: + if not condition: + _failures.append(message) + + +func _run() -> void: + var host := Node3D.new() + root.add_child(host) + var jets := PlayerJetVFX.new() + host.add_child(jets) + await process_frame + + _expect(jets.get_flame_count() == 4, "jet rig must contain exactly four flames") + jets.burst_double_jump() + jets._process(0.016) + var up_exhaust := jets.get_exhaust_direction() + _expect(up_exhaust.y < -0.95, "double-jump exhaust must point mostly down") + _expect(up_exhaust.z > 0.0, "double-jump exhaust needs a slight rear cant") + _expect(jets.visible, "double-jump burst must reveal the flames") + + jets.set_dash(true, Vector2(0.0, 1.0)) + jets._process(0.016) + var dash_exhaust := jets.get_exhaust_direction() + _expect(dash_exhaust.z > 0.90, "forward dash exhaust must point backward") + _expect(dash_exhaust.y < 0.0, "dash exhaust keeps a slight downward cant") + + var humanoid := HumanoidModel.new() + host.add_child(humanoid) + await process_frame + humanoid.set_locomotion(0.0, -1.0, 0.0) + humanoid.update_state("ground", 9.0) + humanoid._process(0.1) + _expect(humanoid.root_pivot.rotation.x > 0.0, + "fallback model must lean backward while backpedalling") + humanoid.update_state("ground", 5.0) + humanoid._process(0.1) + _expect(humanoid._brake_left > 0.0, "deceleration must arm the braking pose") + _expect(absf(humanoid.thigh_r_pivot.rotation.x) > 0.05, + "braking pose must plant a lead foot") + + if _failures.is_empty(): + print("JET_POSE_TEST PASS: four flames, directional thrust, reverse lean, brake pose") + quit(0) + else: + for failure in _failures: + push_error("JET_POSE_TEST FAIL: " + failure) + quit(1) diff --git a/debug/jet_vfx_test.gd.uid b/debug/jet_vfx_test.gd.uid new file mode 100644 index 0000000..6b68776 --- /dev/null +++ b/debug/jet_vfx_test.gd.uid @@ -0,0 +1 @@ +uid://dryddw6j3udbo diff --git a/debug/locomotion_stop_capture.gd b/debug/locomotion_stop_capture.gd new file mode 100644 index 0000000..abd9982 --- /dev/null +++ b/debug/locomotion_stop_capture.gd @@ -0,0 +1,155 @@ +extends SceneTree + +## Deterministic third-person run-to-stop capture. +## +## Run: +## godot --path . --resolution 1280x720 \ +## --script res://debug/locomotion_stop_capture.gd -- [skin_id] + +const DT := 1.0 / 60.0 +const RUN_FRAMES := 90 + +var _out_dir := "." +var _skin_id := "miku" +var _frames := 0 +var _player: CharacterBody3D = null +var _model: SkinnedPlayerModel = null +var _camera: Camera3D = null +var _motion_frame := 0 +var _stop_frame := -1 +var _worst_run_stride_error := 0.0 +var _worst_stop_stride := 0.0 +var _failures: Array[String] = [] + + +func _initialize() -> void: + Engine.max_fps = 60 + var args := OS.get_cmdline_user_args() + if args.size() > 0: + _out_dir = args[0] + if args.size() > 1: + _skin_id = args[1] + if not _out_dir.is_absolute_path(): + _out_dir = ProjectSettings.globalize_path(_out_dir) + var mkdir_error := DirAccess.make_dir_recursive_absolute(_out_dir) + if mkdir_error != OK: + printerr("LOCO_STOP could not create '%s' (error %d)" % [ + _out_dir, mkdir_error]) + quit(mkdir_error) + return + change_scene_to_file("res://ui/main_menu/main_menu.tscn") + + +func _process(_delta: float) -> bool: + _frames += 1 + if _frames == 40: + var skin_manager = root.get_node_or_null("SkinManager") + if skin_manager: + skin_manager.set_active_skin(_skin_id) + var network = root.get_node_or_null("NetworkManager") + if network and network.has_method("start_singleplayer_match"): + network.start_singleplayer_match(GameMode.DEATHMATCH) + change_scene_to_file("res://scenes/maps/test_level/test_level.tscn") + return false + if _frames < 160: + return false + if _player == null: + if not _setup(): + _finish() + return true + + _motion_frame += 1 + if _motion_frame <= RUN_FRAMES: + _model.set_locomotion(0.0, 1.0, 0.0) + _model.update_state("ground", 9.0, false) + _worst_run_stride_error = maxf(_worst_run_stride_error, + absf(_model.locomotion_effective_speed_debug() - 9.0)) + if _motion_frame == RUN_FRAMES - 1: + _snap("loco_stop_00_run") + else: + if _stop_frame < 0: + _stop_frame = 0 + else: + _stop_frame += 1 + _model.set_locomotion(0.0, 0.0, 0.0) + _model.update_state("ground", 0.0, false) + _worst_stop_stride = maxf(_worst_stop_stride, + _model.locomotion_effective_speed_debug()) + _model._process(DT) + + if _stop_frame >= 0: + match _stop_frame: + 3: + _snap("loco_stop_01_idle_blend") + 9: + _snap("loco_stop_02_idle_arrival") + 20: + _snap("loco_stop_03_idle") + 50: + _snap("loco_stop_04_settled") + if _worst_run_stride_error > 0.02: + _failures.append("run stride diverged from physical speed") + if _worst_stop_stride > 0.02: + _failures.append("authored stride did not stop immediately") + if absf(_model.get_brake_debug()) > 0.001: + _failures.append("procedural brake pose is still active") + _finish() + return true + return false + + +func _setup() -> bool: + for candidate in root.find_children("*", "CharacterBody3D", true, false): + if candidate.has_method("get_visual_model") \ + and candidate.is_multiplayer_authority(): + _player = candidate + break + if _player == null: + _failures.append("local player was not created") + return false + _model = _player.get_node_or_null("SkinnedModel") + if _model == null or not _model.loaded: + _failures.append("skinned model '%s' was not ready" % _skin_id) + return false + _player.set_physics_process(false) + _player.set_process(false) + _model.set_process(false) + _player.global_position = Vector3(0.0, 1.2, 14.0) + _player.rotation = Vector3.ZERO + _model.set_owner_visible(true) + if _model.has_method("set_weapon"): + _model.set_weapon("res://weapons/ak47.gd") + # The match HUD and the isolated first-person SubViewport are separate + # CanvasLayers, so a cull mask alone cannot keep them out of this shot. + for layer in root.find_children("*", "CanvasLayer", true, false): + layer.visible = false + + _camera = Camera3D.new() + _camera.cull_mask &= ~(1 << 19) + current_scene.add_child(_camera) + _camera.global_position = _player.global_position + Vector3(1.8, 0.85, -1.9) + _camera.look_at(_player.global_position + Vector3(0.0, 0.45, 0.0), Vector3.UP) + _camera.current = true + return true + + +func _snap(tag: String) -> void: + var image := root.get_viewport().get_texture().get_image() + var path := _out_dir.path_join(tag + ".png") + var error := image.save_png(path) + if error == OK: + print("LOCO_STOP saved ", path) + else: + _failures.append("could not save %s (error %d)" % [path, error]) + + +func _finish() -> void: + if _failures.is_empty() and _stop_frame >= 50: + print("LOCO_STOP PASS: run error %.5f, stop stride %.5f, procedural brake %.5f" % [ + _worst_run_stride_error, _worst_stop_stride, + _model.get_brake_debug()]) + quit(0) + else: + for failure in _failures: + push_error("LOCO_STOP FAIL: " + failure) + quit(1) diff --git a/debug/locomotion_stop_capture.gd.uid b/debug/locomotion_stop_capture.gd.uid new file mode 100644 index 0000000..6b102f3 --- /dev/null +++ b/debug/locomotion_stop_capture.gd.uid @@ -0,0 +1 @@ +uid://bhlv24bt2dymi diff --git a/debug/map_preview_capture.gd b/debug/map_preview_capture.gd index 4a9b73f..35e7114 100644 --- a/debug/map_preview_capture.gd +++ b/debug/map_preview_capture.gd @@ -192,6 +192,14 @@ func _bounds(node: Node) -> AABB: var box := vi.get_aabb() if box.size.length() > 100000.0 or box.size.length() < 0.001: continue + # Skip declared BACKDROP: distant hills, ridges and anything else that + # is painted scenery rather than level. Sakura Crossing's far ridge sits + # 380 m out, which inflated the merged bounds to about a kilometre and + # put every fallback vantage in orbit — the map photographed as a + # diorama floating in an empty sky, which is the exact failure this + # tool's framing was rewritten to avoid. + if vi.has_meta("preview_backdrop"): + continue box = vi.global_transform * box if not any: out = box diff --git a/debug/out_of_bounds_test.gd b/debug/out_of_bounds_test.gd new file mode 100644 index 0000000..a8bf82c --- /dev/null +++ b/debug/out_of_bounds_test.gd @@ -0,0 +1,153 @@ +extends SceneTree + +## Verifies the out-of-bounds contract on a real map, in a real match: +## +## 1. leaving the play volume registers the player and starts the countdown +## 2. the warning UI appears, with a timer +## 3. the red overlay deepens as the timer runs down +## 4. the timer is FIVE seconds +## 5. reaching zero kills the player +## 6. coming back inside before zero cancels it +## +## godot --path . --windowed --resolution 800x600 \ +## -s res://debug/out_of_bounds_test.gd +## +## It runs windowed rather than headless because the overlay is a real Control +## and the check on its alpha is the only way to know the screen actually reds +## out rather than merely that a number is counting down somewhere. + +## Defaults to Sakura Crossing; pass a scene path to check another map, e.g. +## -s res://debug/out_of_bounds_test.gd -- res://scenes/maps/neon_alley/neon_alley.tscn +var MAP := "res://scenes/maps/sakura_crossing/sakura_crossing.tscn" +## How far outside to teleport. Must clear the widest play volume in the game — +## Neon Alley's is 666 m across. +const FAR := 2000.0 + +var _frames := 0 +var _fails := 0 +var _phase := "boot" +var _player: Node3D = null +var _area: CombatArea = null +var _alpha_early := -1.0 +var _alpha_late := -1.0 +var _t_start := 0.0 + + +## By TYPE, not by node name — the overlay is created in code and its name is +## whatever Godot assigns. +func _ui_layer() -> CanvasLayer: + if _area == null: + return null + for c in _area.get_children(): + if c is CanvasLayer: + return c + return null + + +func _check(ok: bool, msg: String) -> void: + if ok: + print(" OK: ", msg) + else: + _fails += 1 + print(" FAIL: ", msg) + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if args.size() > 0: + MAP = args[0] + print("OOB: testing ", MAP) + change_scene_to_file("res://ui/main_menu/main_menu.tscn") + + +func _find() -> bool: + if current_scene == null: + return false + var areas := current_scene.find_children("*", "Area3D", true, false) + for a in areas: + if a is CombatArea: + _area = a + for p in current_scene.find_children("*", "CharacterBody3D", true, false): + if str(p.name).is_valid_int(): + _player = p + return _area != null and _player != null + + +func _process(delta: float) -> bool: + _frames += 1 + match _phase: + "boot": + if _frames == 60: + var nm = root.get_node_or_null("NetworkManager") + if nm and nm.has_method("start_singleplayer_match"): + nm.start_singleplayer_match(GameMode.DEATHMATCH) + change_scene_to_file(MAP) + _phase = "settle" + "settle": + if _frames > 200: + if not _find(): + print("OOB: could not find CombatArea or player") + _fails += 1 + return _done() + _check(true, "map has a CombatArea and a player") + # Sideways, not upward. Teleporting straight up puts the player + # back INSIDE the volume a few seconds later as they fall, which + # cancels the countdown and makes the test look like a bug in + # the countdown. Far out on X they stay outside however far they + # drop — which is also the real case being tested. + _player.global_position = Vector3(FAR, 30, 0) + var mp := _area.multiplayer + print("OOB: peer=%s uid=%d authority=%d is_server=%s" % [ + mp.has_multiplayer_peer(), mp.get_unique_id(), + _player.get_multiplayer_authority(), mp.is_server()]) + _phase = "left" + _t_start = 0.0 + "left": + _t_start += delta + if _t_start > 0.4: + _check(_area.out_of_bounds_players.has(_player), + "leaving the volume registers the player") + var t: float = 0.0 + if _area.out_of_bounds_players.has(_player): + t = _area.out_of_bounds_players[_player].time_left + _check(t > 4.0 and t <= 5.0, + "countdown starts at five seconds (%.2f left after %.1fs)" % [t, _t_start]) + var lay := _ui_layer() + _check(lay != null and lay.visible, "warning UI is on screen") + if lay: + for c in lay.get_children(): + if c is Label: + _check(c.text.contains("WARNING"), + "warning text reads '%s'" % c.text.split("\n")[0]) + if c is ColorRect: + _alpha_early = c.color.a + _phase = "reddening" + "reddening": + _t_start += delta + if _t_start > 3.6: + var lay := _ui_layer() + if lay: + for c in lay.get_children(): + if c is ColorRect: + _alpha_late = c.color.a + _check(_alpha_late > _alpha_early, + "screen reddens as the timer runs down (%.2f -> %.2f)" + % [_alpha_early, _alpha_late]) + _phase = "dying" + "dying": + _t_start += delta + if _t_start > 6.5: + var dead: bool = "is_dead" in _player and _player.is_dead + var hurt: bool = "health" in _player and float(_player.health) <= 0.0 + _check(dead or hurt, + "player is killed by the environment at zero (dead=%s)" % dead) + _check(not _area.out_of_bounds_players.has(_player), + "player is dropped from the countdown once dead") + return _done() + return false + + +func _done() -> bool: + print("=== OUT OF BOUNDS: %s ===" % + ("ALL PASSED" if _fails == 0 else "%d FAILED" % _fails)) + return true diff --git a/debug/out_of_bounds_test.gd.uid b/debug/out_of_bounds_test.gd.uid new file mode 100644 index 0000000..c8c96f1 --- /dev/null +++ b/debug/out_of_bounds_test.gd.uid @@ -0,0 +1 @@ +uid://dduxrlh05tup3 diff --git a/debug/prop_placement_audit.gd b/debug/prop_placement_audit.gd new file mode 100644 index 0000000..fea14ed --- /dev/null +++ b/debug/prop_placement_audit.gd @@ -0,0 +1,169 @@ +extends SceneTree + +## Is any piece of street furniture standing somewhere it should not be? +## +## godot --headless --path . -s res://debug/prop_placement_audit.gd +## +## Every "there is a bus stop in the middle of the road" bug in this map came +## from a hand-written coordinate that was right when it was written and wrong +## after something moved. Screenshots catch them one at a time, from whichever +## angle happens to be pointed at them; this checks all of them at once. +## +## It re-derives the corridors from the same constants the builder uses rather +## than reading them out of it, so a prop that has drifted onto a carriageway +## fails here even if the builder thinks it placed it correctly. + +const MAP := "res://scenes/maps/sakura_crossing/sakura_crossing.tscn" + +# Mirrors of the builder's layout constants. +const ROAD_HALF := 4.5 +const PAVE_W := 2.6 +const RAIL_HALF := 7.0 +const SHOP_ST_Z := -30.0 +const SHOP_ST_HALF := 4.5 +const SHOP_ST_X1 := 52.0 +const SOUTH_ST_Z := 20.0 +const SOUTH_ST_HALF := 4.5 +const EAST_ST_X := 64.0 +const EAST_ST_Z := 54.0 +const EAST_ST_HALF := 4.5 +const HALF_X := 118.0 +const CURVE_START_X := 62.0 +const CURVE_RADIUS := 60.0 +const CURVE_SWEEP := 55.0 + +## Bodies whose placement is hand-authored and therefore worth checking. +const WATCHED := ["Van", "Truck", "KeiVan", "KeiCar", "Hedge", "BlockWall", "LinesideHut", + "Hoarding", "Vending", "Cherry", "Mound"] + +var _fails := 0 +var _checked := 0 +var _n := 0 +var _lv: Node + + +func _initialize() -> void: + _lv = (load(MAP) as PackedScene).instantiate() + root.add_child(_lv) + + +## The running line, as a polyline, recomputed here. +func _rail_points() -> Array: + var pts: Array = [] + var x := -HALF_X - 40.0 + while x < CURVE_START_X: + pts.append(Vector2(x, 0.0)) + x += 2.0 + var c := Vector2(CURVE_START_X, -CURVE_RADIUS) + var sweep := deg_to_rad(CURVE_SWEEP) + for i in range(40): + var th := sweep * float(i) / 39.0 + pts.append(c + Vector2(sin(th), cos(th)) * CURVE_RADIUS) + var ep: Vector2 = pts[pts.size() - 1] + var et := Vector2(cos(sweep), -sin(sweep)) + for i in range(60): + pts.append(ep + et * (float(i) * 2.0)) + return pts + + +## Roads a prop must never stand in. +## +## Every one is BOUNDED along its own length. Treating the main street as an +## infinite strip at |x| < 4.5 flagged a hillside three hundred and fifty metres +## out, where the road has not existed for two hundred of them. +const ROAD_OUT := 170.0 # how far a road runs past the wall +const HALF_Z := 88.0 + +func _road_hit(p: Vector2) -> String: + if absf(p.x) < ROAD_HALF and absf(p.y) < HALF_Z + ROAD_OUT: + return "main street" + if absf(p.y - SOUTH_ST_Z) < SOUTH_ST_HALF and absf(p.x) < HALF_X + ROAD_OUT: + return "south street" + if p.x < SHOP_ST_X1 and p.x > -HALF_X - ROAD_OUT \ + and absf(p.y - SHOP_ST_Z) < SHOP_ST_HALF: + return "shopping street" + if p.y > SOUTH_ST_Z and p.y < 80.0 and absf(p.x - EAST_ST_X) < EAST_ST_HALF: + return "east district n-s street" + if p.x > 10.0 and p.x < 108.0 and absf(p.y - EAST_ST_Z) < EAST_ST_HALF: + return "east district e-w street" + return "" + + +## Distance from a point on a carriageway to its nearest kerb. +func _kerb_gap(p: Vector2) -> float: + var best := 999.0 + if absf(p.x) < ROAD_HALF: + best = minf(best, ROAD_HALF - absf(p.x)) + if absf(p.y - SOUTH_ST_Z) < SOUTH_ST_HALF: + best = minf(best, SOUTH_ST_HALF - absf(p.y - SOUTH_ST_Z)) + if p.x < SHOP_ST_X1 and absf(p.y - SHOP_ST_Z) < SHOP_ST_HALF: + best = minf(best, SHOP_ST_HALF - absf(p.y - SHOP_ST_Z)) + if absf(p.x - EAST_ST_X) < EAST_ST_HALF: + best = minf(best, EAST_ST_HALF - absf(p.x - EAST_ST_X)) + if absf(p.y - EAST_ST_Z) < EAST_ST_HALF: + best = minf(best, EAST_ST_HALF - absf(p.y - EAST_ST_Z)) + return best + + +func _rail_hit(p: Vector2, margin: float) -> bool: + for q in _rail_points(): + if absf(q.x - p.x) > margin or absf(q.y - p.y) > margin: + continue + if q.distance_to(p) < margin: + return true + return false + + +func _process(_d: float) -> bool: + _n += 1 + if _n < 120: + return false + + var seen := {} + for body in _lv.find_children("*", "StaticBody3D", true, false): + # Godot suffixes duplicate node names with digits, so an unstripped + # "Cherry2" matched nothing and most of the map went unchecked. + var base := String(body.name) + while base.length() > 0 and base[base.length() - 1].is_valid_int(): + base = base.substr(0, base.length() - 1) + if not WATCHED.has(base): + continue + var g: Vector3 = body.global_position + var p := Vector2(g.x, g.z) + # One report per object, not per collision box. + var key := "%s|%.1f|%.1f" % [base, g.x, g.z] + if seen.has(key): + continue + seen[key] = true + _checked += 1 + + var road := _road_hit(p) + if road != "": + # Vehicles are SUPPOSED to be on the road — parked against the + # kerb. What is wrong is one stranded out in the running lane. + if base == "Van" or base == "Truck" or base == "KeiVan" or base == "KeiCar": + if _kerb_gap(p) > 2.5: + print("AUDIT: FAIL %-12s at (%.1f, %.1f) is parked %.1f m " + % [base, g.x, g.z, _kerb_gap(p)] + + "from the kerb of the %s" % road) + _fails += 1 + else: + print("AUDIT: FAIL %-12s at (%.1f, %.1f) is in the %s carriageway" + % [base, g.x, g.z, road]) + _fails += 1 + continue + # Nothing but the railway's own furniture belongs within the four-foot. + # 3.9 m clears the sleeper ends (3.75) without flagging lineside kit. + if base != "LinesideHut" and _rail_hit(p, 3.9): + print("AUDIT: FAIL %-12s at (%.1f, %.1f) is on the running line" + % [base, g.x, g.z]) + _fails += 1 + elif base == "LinesideHut" and _rail_hit(p, 3.9): + print("AUDIT: FAIL %-12s at (%.1f, %.1f) fouls the running line" + % [base, g.x, g.z]) + _fails += 1 + + print("AUDIT: checked %d placed props" % _checked) + print("=== PROP PLACEMENT: %s ===" % + ("ALL CLEAR" if _fails == 0 else "%d MISPLACED" % _fails)) + return true diff --git a/debug/prop_placement_audit.gd.uid b/debug/prop_placement_audit.gd.uid new file mode 100644 index 0000000..229c8d0 --- /dev/null +++ b/debug/prop_placement_audit.gd.uid @@ -0,0 +1 @@ +uid://bl1ks6hnbv6qp diff --git a/debug/roster_capture.gd b/debug/roster_capture.gd index 59bfe27..73b9f91 100644 --- a/debug/roster_capture.gd +++ b/debug/roster_capture.gd @@ -28,14 +28,25 @@ func _initialize() -> void: var args := OS.get_cmdline_user_args() if args.size() > 0: _out_dir = args[0] - var nm = root.get_node_or_null("NetworkManager") - if nm and nm.has_method("start_singleplayer_match"): - nm.start_singleplayer_match(GameMode.DEATHMATCH) - change_scene_to_file("res://scenes/maps/test_level/test_level.tscn") + if not _out_dir.is_absolute_path(): + _out_dir = ProjectSettings.globalize_path(_out_dir) + var mkdir_error := DirAccess.make_dir_recursive_absolute(_out_dir) + if mkdir_error != OK: + printerr("roster_capture: could not create '%s' (error %d)" % [ + _out_dir, mkdir_error]) + quit(mkdir_error) + return + change_scene_to_file("res://ui/main_menu/main_menu.tscn") func _process(_delta: float) -> bool: _frames += 1 + if _frames == 40: + var nm = root.get_node_or_null("NetworkManager") + if nm and nm.has_method("start_singleplayer_match"): + nm.start_singleplayer_match(GameMode.DEATHMATCH) + change_scene_to_file("res://scenes/maps/test_level/test_level.tscn") + return false if _frames < 150: return false if _menu == null: @@ -80,5 +91,9 @@ func _shoot() -> void: img = img.get_region(Rect2i(int(w * 0.44), int(h * 0.15), int(w * 0.32), int(h * 0.68))) var path := "%s/roster_%s.png" % [_out_dir, id] - img.save_png(path) - print("wrote %s" % path) + var save_error := img.save_png(path) + if save_error == OK: + print("wrote %s" % path) + else: + printerr("roster_capture: could not save '%s' (error %d)" % [ + path, save_error]) diff --git a/debug/sakura_boundary_audit.gd b/debug/sakura_boundary_audit.gd new file mode 100644 index 0000000..574be7f --- /dev/null +++ b/debug/sakura_boundary_audit.gd @@ -0,0 +1,81 @@ +extends SceneTree + +## Structural checks for the map-cohesion pass. +## +## godot --headless --path . -s res://debug/sakura_boundary_audit.gd + +const MAP := "res://scenes/maps/sakura_crossing/sakura_crossing.tscn" +const EXPECTED_AREA := Vector3(236.0, 60.0, 176.0) + +var _level: Node +var _frames := 0 +var _fails := 0 + + +func _initialize() -> void: + _level = (load(MAP) as PackedScene).instantiate() + root.add_child(_level) + + +func _check(ok: bool, message: String) -> void: + print("BOUNDARY_AUDIT %s %s" % ["OK" if ok else "FAIL", message]) + if not ok: + _fails += 1 + + +func _process(_delta: float) -> bool: + _frames += 1 + if _frames < 8: + return false + + var area: CombatArea = null + for candidate in _level.find_children("*", "CombatArea", true, false): + area = candidate as CombatArea + break + _check(area != null, "combat area exists") + if area != null: + var collision := area.get_child(0) as CollisionShape3D + var box := collision.shape as BoxShape3D if collision != null else null + _check(box != null and box.size.is_equal_approx(EXPECTED_AREA), + "combat area matches the visible wall rectangle") + + var gates: Array[Node] = [] + for body in _level.find_children("*", "StaticBody3D", true, false): + if body.has_meta("boundary_gate"): + gates.append(body) + _check(gates.size() == 60, "six road closures have ten beam segments each") + var kei_cars := _level.find_children("KeiCar*", "Node3D", true, false) + _check(not kei_cars.is_empty(), "kei hatchbacks are present") + + var house_meshes: Array[MeshInstance3D] = [] + for body in _level.find_children("HouseBody*", "StaticBody3D", true, false): + var mesh := body.get_child(1) as MeshInstance3D + if mesh != null: + house_meshes.append(mesh) + var overlaps := 0 + for i in range(house_meshes.size()): + for j in range(i + 1, house_meshes.size()): + var overlap := _global_aabb(house_meshes[i]).intersection(_global_aabb(house_meshes[j])) + if overlap.size.x > 0.02 and overlap.size.y > 0.02 and overlap.size.z > 0.02: + overlaps += 1 + if overlaps <= 8: + print("BOUNDARY_AUDIT HOUSE_OVERLAP %s %s size=%s" % [ + house_meshes[i].get_path(), house_meshes[j].get_path(), overlap.size]) + _check(overlaps == 0, "detached house bodies do not overlap") + + print("=== SAKURA BOUNDARY AUDIT: %s ===" % + ("ALL CLEAR" if _fails == 0 else "%d FAILED" % _fails)) + quit(0 if _fails == 0 else 1) + return true + + +func _global_aabb(mesh: MeshInstance3D) -> AABB: + var local := mesh.get_aabb() + var result := AABB(mesh.global_transform * local.position, Vector3.ZERO) + for x in 2: + for y in 2: + for z in 2: + var corner := local.position + Vector3( + local.size.x * float(x), local.size.y * float(y), local.size.z * float(z)) + result = result.expand(mesh.global_transform * corner) + return result diff --git a/debug/sakura_boundary_audit.gd.uid b/debug/sakura_boundary_audit.gd.uid new file mode 100644 index 0000000..522e66c --- /dev/null +++ b/debug/sakura_boundary_audit.gd.uid @@ -0,0 +1 @@ +uid://p6fiv1r1hhef diff --git a/debug/sakura_detail_capture.gd b/debug/sakura_detail_capture.gd new file mode 100644 index 0000000..60e2042 --- /dev/null +++ b/debug/sakura_detail_capture.gd @@ -0,0 +1,96 @@ +extends SceneTree + +## Photograph Sakura Crossing from a fixed set of authored viewpoints. +## +## godot --path . --windowed --resolution 1600x900 \ +## -s res://debug/sakura_detail_capture.gd -- +## +## `visual_capture.gd` shoots the real game through a real match, which is the +## right regression tool but the wrong review tool: it spawns the player at +## random, so half its frames are of a rooftop or a wall and no two runs compare. +## Reviewing DETAIL needs the opposite — the same handful of angles every time, +## chosen because each one is where a particular piece of work has to hold up. + +const SCENE := "res://scenes/maps/sakura_crossing/sakura_crossing.tscn" +const SETTLE_FRAMES := 120 + +## [name, eye, look_at, fov] +const VIEWS := [ + ["crossing", Vector3(0, 1.7, 20.0), Vector3(0, 2.2, -16.0), 70.0], + ["shopfront", Vector3(-30.0, 1.7, -29.0), Vector3(-24.0, 3.2, -22.0), 62.0], + ["shopping_street", Vector3(-4.0, 1.7, -30.0), Vector3(46.0, 4.0, -30.0), 68.0], + ["station", Vector3(30.0, 2.6, 14.0), Vector3(46.0, 2.4, 9.0), 66.0], + ["platform", Vector3(52.0, 2.2, 8.8), Vector3(30.0, 2.6, 8.4), 66.0], + ["torii", Vector3(-22.0, 1.7, 34.0), Vector3(-22.0, 3.0, 56.0), 68.0], + ["alley", Vector3(-52.0, 1.7, -11.2), Vector3(10.0, 2.6, -11.2), 70.0], + ["footbridge", Vector3(-40.0, 7.6, 8.0), Vector3(30.0, 3.0, -1.0), 72.0], + ["road_paint", Vector3(0.0, 1.7, 16.0), Vector3(0.0, 0.0, 6.0), 66.0], + ["houses", Vector3(-8.0, 1.7, 26.0), Vector3(-24.0, 3.0, 33.0), 66.0], + ["aerial", Vector3(-30.0, 150.0, 150.0), Vector3(20.0, 0.0, -10.0), 62.0], + ["rail_curve", Vector3(40.0, 18.0, 30.0), Vector3(95.0, 2.0, -22.0), 66.0], + ["east_district", Vector3(64.0, 1.8, 30.0), Vector3(64.0, 3.0, 78.0), 68.0], + ["east_street", Vector3(20.0, 1.8, 54.0), Vector3(104.0, 4.0, 54.0), 66.0], + # Looking OUT from inside the map, over the boundary, which is the only + # angle that tells you whether the world continues past the wall. + ["beyond_west", Vector3(-104.0, 3.0, 20.0), Vector3(-200.0, 10.0, 20.0), 70.0], + ["beyond_south", Vector3(0.0, 3.0, 74.0), Vector3(0.0, 12.0, 200.0), 70.0], + ["catenary", Vector3(20.0, 3.0, 12.0), Vector3(-30.0, 6.5, 0.0), 60.0], + ["vehicles", Vector3(-30.0, 2.2, 27.0), Vector3(-8.0, 1.4, 22.0), 55.0], + ["rail_east_wall", Vector3(96.0, 9.0, -6.0), Vector3(126.0, 2.0, -38.0), 66.0], + ["north_park", Vector3(-30.0, 3.0, -78.0), Vector3(30.0, 4.0, -60.0), 70.0], + ["north_park_overview", Vector3(-34.0, 32.0, -45.0), Vector3(-20.0, 0.0, -70.0), 68.0], + ["pond_bridge", Vector3(-58.0, 2.5, -64.0), Vector3(-34.0, 1.0, -73.0), 66.0], + ["park_road", Vector3(60.0, 6.0, -22.0), Vector3(100.0, 1.0, -60.0), 68.0], + ["shrine_precinct", Vector3(-22.0, 2.0, 58.0), Vector3(-22.0, 4.0, 84.0), 70.0], + ["shrine_lawn", Vector3(-70.0, 8.0, 42.0), Vector3(-25.0, 2.0, 53.0), 68.0], + ["playground", Vector3(-72.0, 2.0, 36.0), Vector3(-72.0, 1.4, 52.0), 70.0], + ["playground_reverse", Vector3(-92.0, 5.0, 63.0), Vector3(-72.0, 1.5, 51.0), 68.0], + ["shrine_intersection", Vector3(-2.0, 2.0, 54.0), Vector3(20.0, 1.0, 54.0), 66.0], + ["sidewalk_corner", Vector3(13.0, 10.0, 7.0), Vector3(5.9, 0.2, 14.1), 52.0], + ["street_sign", Vector3(3.5, 1.8, -18.0), Vector3(8.15, 2.0, -21.85), 46.0], + ["bikes", Vector3(20.0, 2.0, 8.0), Vector3(32.0, 1.2, 13.0), 55.0], +] + +var _out_dir := "." + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if args.size() > 0: + _out_dir = args[0] + if not _out_dir.is_absolute_path(): + _out_dir = ProjectSettings.globalize_path(_out_dir) + DirAccess.make_dir_recursive_absolute(_out_dir) + _run() + + +func _run() -> void: + await process_frame + var packed: PackedScene = load(SCENE) + var level: Node = packed.instantiate() + root.add_child(level) + for _i in SETTLE_FRAMES: + await process_frame + + var cam := Camera3D.new() + cam.far = 1200.0 + level.add_child(cam) + cam.current = true + + for v in VIEWS: + cam.fov = float(v[3]) + cam.global_position = v[1] + cam.look_at(v[2], Vector3.UP) + # Three frames: one for the transform, one for the frame drawn with it, + # and one because the ink pass reads a screen copy of the frame before. + for _i in 3: + await process_frame + var img := root.get_texture().get_image() + var path := "%s/%s.png" % [_out_dir, v[0]] + img.save_png(path) + print("sakura_detail: saved ", path) + + level.queue_free() + for _i in 6: + await process_frame + quit(0) diff --git a/debug/sakura_detail_capture.gd.uid b/debug/sakura_detail_capture.gd.uid new file mode 100644 index 0000000..7583f40 --- /dev/null +++ b/debug/sakura_detail_capture.gd.uid @@ -0,0 +1 @@ +uid://5hx0k7ohj27h diff --git a/debug/sakura_followup_audit.gd b/debug/sakura_followup_audit.gd new file mode 100644 index 0000000..56c0006 --- /dev/null +++ b/debug/sakura_followup_audit.gd @@ -0,0 +1,231 @@ +extends SceneTree + +## Requirement-level audit for the park/shrine/intersection follow-up pass. + +const MAP := "res://scenes/maps/sakura_crossing/sakura_crossing.tscn" +const ROAD_HALF := 4.5 +const PAVE_W := 2.6 +const SHOP_ST_Z := -30.0 +const SOUTH_ST_Z := 20.0 +const EAST_ST_X := 64.0 +const EAST_ST_Z := 54.0 +const STREET_HALF := 4.5 + +var _level: Node +var _frames := 0 +var _fails := 0 + + +func _initialize() -> void: + _level = (load(MAP) as PackedScene).instantiate() + root.add_child(_level) + + +func _ok(condition: bool, message: String) -> void: + if condition: + print("FOLLOWUP_AUDIT OK ", message) + else: + print("FOLLOWUP_AUDIT FAIL ", message) + _fails += 1 + + +func _all_nodes() -> Array[Node]: + var result: Array[Node] = [] + var queue: Array[Node] = [_level] + while not queue.is_empty(): + var node: Node = queue.pop_front() + result.append(node) + for child in node.get_children(): + queue.append(child) + return result + + +func _base_name(node: Node) -> String: + var value := String(node.name) + while value.length() > 0 and value[value.length() - 1].is_valid_int(): + value = value.substr(0, value.length() - 1) + return value + + +func _named(name: String) -> Array[Node]: + var result: Array[Node] = [] + for node in _all_nodes(): + if _base_name(node) == name: + result.append(node) + return result + + +func _box_size(node: Node) -> Vector3: + var collisions := node.find_children("*", "CollisionShape3D", true, false) + if collisions.is_empty(): + return Vector3.ZERO + var collision := collisions[0] as CollisionShape3D + if collision == null or not collision.shape is BoxShape3D: + return Vector3.ZERO + return (collision.shape as BoxShape3D).size + + +func _on_road_or_pavement(p: Vector2) -> bool: + var paved_half := ROAD_HALF + PAVE_W + 0.30 + if absf(p.x) <= paved_half: + return true + if absf(p.y - SOUTH_ST_Z) <= paved_half: + return true + if p.x <= 52.0 and absf(p.y - SHOP_ST_Z) <= paved_half: + return true + if p.x >= ROAD_HALF - 0.2 and p.x <= 108.0 \ + and absf(p.y - EAST_ST_Z) <= paved_half: + return true + if p.y >= SOUTH_ST_Z and p.y <= 80.0 \ + and absf(p.x - EAST_ST_X) <= paved_half: + return true + return false + + +func _audit_signs(nodes: Array[Node]) -> void: + var signs := 0 + for node in nodes: + if not node.has_meta("ground_sign"): + continue + signs += 1 + var g := (node as Node3D).global_position + _ok(not _on_road_or_pavement(Vector2(g.x, g.z)), + "ground sign %s is clear of roads, crosswalks and pavements" % node.name) + _ok(signs == 6, "all six freestanding sign posts are tagged and checked") + + +func _audit_sidewalk_connectors(nodes: Array[Node]) -> void: + var connectors := 0 + var full_size := true + for node in nodes: + if not node.has_meta("sidewalk_connector"): + continue + connectors += 1 + var collisions := node.find_children("*", "CollisionShape3D", true, false) + var collision: CollisionShape3D = collisions[0] if not collisions.is_empty() else null + if collision == null or not collision.shape is BoxShape3D: + full_size = false + continue + var size := (collision.shape as BoxShape3D).size + if size.x < PAVE_W + 0.59 or size.z < PAVE_W + 0.59: + full_size = false + _ok(connectors == 10, "eight main-junction and two shrine-side corner connectors exist") + _ok(full_size, "every tactile apron reaches the adjoining pavement on both axes") + + +func _audit_east_link(nodes: Array[Node]) -> void: + var links: Array[Node] = [] + for node in nodes: + if node.has_meta("east_street_link"): + links.append(node) + _ok(links.size() == 1, "the shrine-side neighbourhood road has one continuous link slab") + if links.size() == 1: + var body := links[0] as StaticBody3D + var collisions := body.find_children("*", "CollisionShape3D", true, false) + var collision: CollisionShape3D = collisions[0] if not collisions.is_empty() else null + var size := (collision.shape as BoxShape3D).size + var left_edge := body.global_position.x - size.x * 0.5 + _ok(left_edge <= ROAD_HALF, "the neighbourhood branch physically reaches the main road") + var query := PhysicsRayQueryParameters3D.create( + Vector3(6.0, 2.0, EAST_ST_Z), Vector3(6.0, -1.0, EAST_ST_Z)) + var hit: Dictionary = _level.get_world_3d().direct_space_state.intersect_ray(query) + var collider: Node = hit.get("collider") as Node + _ok(not hit.is_empty() and collider != null and _base_name(collider) == "EastStreetLink", + "the former gap is road surface, not the old sidewalk barrier") + + +func _audit_park_and_playground() -> void: + _ok(_named("PondBridge").size() == 1, "north park has a connected pond-loop bridge") + _ok(_named("PicnicTable").size() >= 3, "park system has multiple social/picnic nodes") + _ok(_named("ParkLamp").size() >= 8, "park paths have a consistent lighting rhythm") + _ok(_named("ParkBin").size() >= 4, "park entrances and activity nodes have service assets") + _ok(_named("SwingBeam").size() == 1 and _named("SwingSeat").size() == 2 \ + and _named("SwingChain").size() == 4, + "two swing seats align to four chains and one overhead beam") + var beams := _named("SwingBeam") + var seats := _named("SwingSeat") + var chains := _named("SwingChain") + if beams.size() == 1 and seats.size() == 2 and chains.size() == 4: + var beam := beams[0] as Node3D + var beam_size := _box_size(beam) + var beam_left := beam.global_position.x - beam_size.x * 0.5 + var beam_right := beam.global_position.x + beam_size.x * 0.5 + var chains_under_beam := true + for chain in chains: + var cp := (chain as Node3D).global_position + chains_under_beam = chains_under_beam \ + and cp.x >= beam_left and cp.x <= beam_right \ + and absf(cp.z - beam.global_position.z) < 0.01 \ + and absf((cp.y + 1.55 * 0.5) - (beam.global_position.y - beam_size.y * 0.5)) < 0.08 + _ok(chains_under_beam, "every swing chain lands under and touches the common top beam") + var seats_have_pairs := true + for seat in seats: + var sp := (seat as Node3D).global_position + var matching := 0 + for chain in chains: + var cp := (chain as Node3D).global_position + if absf(cp.x - sp.x) <= 0.55 and absf(cp.z - sp.z) < 0.01: + matching += 1 + seats_have_pairs = seats_have_pairs and matching == 2 + _ok(seats_have_pairs, "each swing seat is centered beneath exactly two chains") + _ok(_named("SlidePlatform").size() == 1 and _named("SlideSlope").size() == 1, + "slide platform and slope are both present") + var platforms := _named("SlidePlatform") + var slopes := _named("SlideSlope") + if platforms.size() == 1 and slopes.size() == 1: + var platform := platforms[0] as Node3D + var slope := slopes[0] as Node3D + var platform_size := _box_size(platform) + var slope_size := _box_size(slope) + var high_end := slope.global_transform * Vector3(0.0, 0.0, slope_size.z * 0.5) + var platform_edge := Vector3(platform.global_position.x, + platform.global_position.y, platform.global_position.z - platform_size.z * 0.5) + _ok(Vector2(high_end.y, high_end.z).distance_to( + Vector2(platform_edge.y, platform_edge.z)) < 0.16, + "slide incline meets the platform edge at the same height and z datum") + _ok(_named("Sandpit").size() == 1 and _named("Seesaw").size() == 1 \ + and _named("ClimbingFrame").size() == 1, + "playground contains distinct sand, seesaw and climbing zones") + + var cherries: Array[Node] = [] + var cedars: Array[Node] = [] + for node in _named("Cherry"): + var p := (node as Node3D).global_position + if p.x >= -96.0 and p.x <= 92.0 and p.z < -70.0: + cherries.append(node) + for node in _named("Cedar"): + var p := (node as Node3D).global_position + if p.x >= -96.0 and p.x <= 92.0 and p.z <= -78.0 and p.z >= -89.0: + cedars.append(node) + var min_gap := INF + for cherry in cherries: + var cp := (cherry as Node3D).global_position + for cedar in cedars: + var ep := (cedar as Node3D).global_position + min_gap = minf(min_gap, Vector2(cp.x, cp.z).distance_to(Vector2(ep.x, ep.z))) + _ok(not cherries.is_empty() and not cedars.is_empty() and min_gap > 8.0, + "north-park cherry and evergreen planting bands are spatially separated") + + +func _audit_shrine() -> void: + _ok(_named("Komainu").size() >= 4, "outer and inner shrine thresholds have guardian pairs") + _ok(_named("ShrineBanner").size() >= 4, "shrine approach and precinct have banner rhythm") + _ok(_named("OmikujiRack").size() == 1, "precinct has a dedicated fortune-tying rack") + _ok(_named("SacredTree").size() == 1 and _named("Hokora").size() == 1, + "shrine lawn has a sacred tree and auxiliary shrine destination") + + +func _process(_delta: float) -> bool: + _frames += 1 + if _frames < 120: + return false + var nodes := _all_nodes() + _audit_signs(nodes) + _audit_sidewalk_connectors(nodes) + _audit_east_link(nodes) + _audit_park_and_playground() + _audit_shrine() + print("=== SAKURA FOLLOW-UP AUDIT: %s ===" % + ("ALL CLEAR" if _fails == 0 else "%d FAILED" % _fails)) + quit(0 if _fails == 0 else 1) + return true diff --git a/debug/sakura_followup_audit.gd.uid b/debug/sakura_followup_audit.gd.uid new file mode 100644 index 0000000..0e9772c --- /dev/null +++ b/debug/sakura_followup_audit.gd.uid @@ -0,0 +1 @@ +uid://c01r2mnvkv5f7 diff --git a/debug/test_level_builder.gd b/debug/test_level_builder.gd index efb973e..632312e 100644 --- a/debug/test_level_builder.gd +++ b/debug/test_level_builder.gd @@ -54,6 +54,14 @@ func _build_geometry() -> void: _build_speed_corridor() _build_target_dummy() _build_lighting() + _build_combat_area() + + +## The play volume, generous around the 80x80 arena. Subclasses that build a +## bigger map override this — dust2 has its own, and add_to() is a no-op if a +## CombatArea of the same name already exists. +func _build_combat_area() -> void: + CombatArea.add_to(self, Vector3(140, 80, 140), Vector3(0, 30, 0)) # ── Utility ─────────────────────────────────────────────────────────────────── diff --git a/debug/transition_check.gd b/debug/transition_check.gd index 169c6e8..c889717 100644 --- a/debug/transition_check.gd +++ b/debug/transition_check.gd @@ -1,41 +1,36 @@ extends SceneTree -## Dev tool: how abruptly does the model change posture when you start running? +## Regression for the original "slide, then walk, then run" locomotion bug. ## ## godot --headless --path . -s res://debug/transition_check.gd -- [skin_glb] ## -## Accelerates from a standstill the way the movement code does, then stops, and -## logs the clip in play plus the body's forward lean each frame. Reports the -## worst single-frame change in lean and how long the lean took to arrive. -## -## Written for "idle to running snaps the character leaning forward". The lean -## is procedural (ShooterPoseModifier), so it does NOT come from the clip -## crossfade and is not visible in an animation-blend graph — it was driven by a -## normalised input direction that steps 0 -> 1 the instant a key goes down, -## planting a full run posture in ~0.1 s while the Idle->Run crossfade still had -## 0.4 s to run. +## Ground acceleration is the same exponential response as state_ground.gd. +## The test proves that the authored BlendSpace receives that physical speed on +## the same frame, including the first high-acceleration frame and the stop +## frame. It also proves that the removed procedural lean/brake layer stays off. -const ACCEL := 18.0 # m/s^2, roughly the controller's ground acceleration -const TOP := 9.0 # m/s +const RESPONSE := 60.0 +const TOP := 11.0 const DT := 1.0 / 60.0 +const RUN_FRAMES := 24 -var _t := 0.0 var _frames := 0 var _model: SkinnedPlayerModel = null var _speed := 0.0 -var _prev_lean := 0.0 -var _worst_step := 0.0 -var _clip_changes: Array = [] -var _last_clip := "" -var _t_10 := -1.0 -var _t_90 := -1.0 -var _peak := 0.0 +var _first_speed := -1.0 +var _first_stride := -1.0 +var _worst_error := 0.0 +var _stop_stride := -1.0 +var _manual_tick := false +var _foot_bone := -1 +var _foot_rotation_start := Quaternion.IDENTITY +var _foot_rotation_delta := 0.0 func _initialize() -> void: var args := OS.get_cmdline_user_args() var path: String = args[0] if args.size() > 0 \ - else "res://assets/characters/skins/taila.glb" + else "res://assets/characters/skins/miku.glb" var scene := Node3D.new() root.add_child(scene) current_scene = scene @@ -46,36 +41,75 @@ func _initialize() -> void: func _process(_delta: float) -> bool: _frames += 1 - if _frames < 8: - return false - if not _model.loaded: + if _frames < 8 or not _model.loaded: return false + if not _manual_tick: + _model.set_process(false) + _manual_tick = true + _foot_bone = _model._role_bone("foot.L", + ["foot.L", "LeftFoot", "Left ankle"]) - # Accelerate for 1.5 s, then coast at top speed to 2.5 s. - _t += DT - _speed = minf(TOP, _speed + ACCEL * DT) if _t < 2.5 else 0.0 - _model.update_state("ground", _speed, false) + var sample := _frames - 8 + if sample < RUN_FRAMES: + var response := 1.0 - exp(-RESPONSE * DT) + _speed = lerpf(_speed, TOP, response) + else: + _speed = 0.0 + + # Same order as the live controller: direction first, then physical speed. _model.set_locomotion(0.0, 1.0 if _speed > 0.01 else 0.0, 0.0) + _model.update_state("ground", _speed, false) + _model._process(DT) - var lean: float = _model.get_lean_debug() - if _last_clip != _model.current_clip_debug(): - _last_clip = _model.current_clip_debug() - _clip_changes.append("%.2fs %s @ %.1f m/s" % [_t, _last_clip, _speed]) - if _t < 2.5: - _peak = maxf(_peak, lean) - if _t_10 < 0.0 and lean > 0.1: - _t_10 = _t - if _t_90 < 0.0 and lean > 0.9: - _t_90 = _t - _worst_step = maxf(_worst_step, absf(lean - _prev_lean)) - _prev_lean = lean + var stride := _model.locomotion_effective_speed_debug() + _worst_error = maxf(_worst_error, absf(stride - _speed)) + if _foot_bone >= 0: + var foot_rotation := _model.skeleton.get_bone_pose_rotation(_foot_bone) + if sample == 1: + _foot_rotation_start = foot_rotation + elif sample > 1 and sample < RUN_FRAMES: + _foot_rotation_delta = maxf(_foot_rotation_delta, + _foot_rotation_start.angle_to(foot_rotation)) + if sample == 0: + _first_speed = _speed + _first_stride = stride + if sample == RUN_FRAMES: + _stop_stride = stride - if _t > 3.5: - print("\n=== idle -> run transition ===") - print(" clip changes: " + ", ".join(_clip_changes)) - print(" lean reached 10%% at %.2fs, 90%% at %.2fs (peak %.2f)" % [_t_10, _t_90, _peak]) - print(" worst single-frame lean change: %.4f (%.2f per second at 60fps)" - % [_worst_step, _worst_step * 60.0]) - print(" a snap looks like ~0.10s to 90%%; a blended move is ~0.6s or more\n") + if sample > RUN_FRAMES + 2: + print("\n=== velocity-driven locomotion ===") + print(" first frame: body %.3f m/s, authored stride %.3f m/s" \ + % [_first_speed, _first_stride]) + print(" worst stride/speed error: %.5f m/s" % _worst_error) + print(" stop-frame stride: %.5f m/s" % _stop_stride) + print(" animated foot rotation delta: %.5f rad" % _foot_rotation_delta) + print(" procedural lean %.5f, brake %.5f" \ + % [_model.get_lean_debug(), _model.get_brake_debug()]) + + var failures: Array[String] = [] + if _first_speed < 6.0: + failures.append("test did not reproduce high first-frame acceleration") + if absf(_first_stride - _first_speed) > 0.02: + failures.append("legs did not receive first-frame physical speed") + if _worst_error > 0.02: + failures.append("authored stride diverged from physical speed") + if _stop_stride > 0.02: + failures.append("legs did not reach Idle on the stop frame") + if _foot_bone < 0: + failures.append("could not resolve a foot bone") + elif _foot_rotation_delta < 0.03: + failures.append("runtime foot pose stayed static") + if absf(_model.get_lean_debug()) > 0.02: + failures.append("procedural locomotion lean is still active") + if absf(_model.get_brake_debug()) > 0.02: + failures.append("procedural stop pose is still active") + + if failures.is_empty(): + print("TRANSITION_CHECK PASS: same-frame, authored locomotion\n") + quit(0) + else: + for failure in failures: + push_error("TRANSITION_CHECK FAIL: " + failure) + quit(1) return true return false diff --git a/debug/travel_dir_check.gd b/debug/travel_dir_check.gd index 1892573..70a84ec 100644 --- a/debug/travel_dir_check.gd +++ b/debug/travel_dir_check.gd @@ -1,162 +1,102 @@ extends SceneTree -## Dev tool: do the LEGS point where the character is going, and does the CHEST -## stay pointed where the player is aiming? +## Regression for authored directional locomotion. ## -## godot --headless --path . -s res://debug/travel_dir_check.gd -- [skin_glb] -## -## The clip library has one forward locomotion cycle and no strafe or backpedal -## clips, so nothing in the animation itself says which way the character is -## travelling. SkinnedPlayerModel._update_travel says it by yawing the hips onto -## the travel direction and unwinding that yaw up the spine. -## -## The number that matters is LEGS OFF BY: how far the stride points from the -## direction the character is actually travelling. A backpedal is achieved by -## running the cycle BACKWARDS with the hips near square, so it is scored on -## where the feet go, not on where the hips face. -## -## CHEST is reported for information only, and is not a pass/fail. Most of it is -## the clip's own torso twist — a run cycle counter-rotates the chest against the -## hips by tens of degrees twice a stride — and averaging over a stride only -## partly removes it. It is also not what keeps the gun on target: the rifle hold -## places the weapon from the SHOULDER and the aim direction and then solves the -## arms onto it, so the aim survives whatever the torso is doing. Read this -## column as "does the upper body still look square-ish", nothing stronger. +## The old test expected one forward run to be procedurally yawed or reversed. +## The runtime now blends separate forward/back/left/right source cycles. This +## gate verifies that physical direction enters that authored BlendSpace, stride +## speed stays calibrated, and the feet actually move in every cardinal case. -## label, strafe, forward, the direction of travel this should read as (degrees -## about the character's up axis, 0 = straight ahead) +## The real controller ground speed, not a convenient library-preview speed. +## This is the failure mode the test exists to catch: the old lateral cycle was +## technically "matched" at 5 m/s but exploded to 6.1x playback in gameplay. +const SPEED := 11.0 +const SETTLE_FRAMES := 18 +const SAMPLE_FRAMES := 24 const CASES := [ - ["forward", 0.0, 1.0, 0.0], - ["strafe right", 1.0, 0.0, -90.0], - ["strafe left", -1.0, 0.0, 90.0], - ["fwd-right", 0.7, 0.7, -45.0], - ["fwd-left", -0.7, 0.7, 45.0], - ["backpedal", 0.0, -1.0, 180.0], - ["back-right", 0.7, -0.7, -135.0], + ["forward", 0.0, 1.0, "Sprint"], + ["backward", 0.0, -1.0, "RunBackward"], + ["left", -1.0, 0.0, "RunLeft"], + ["right", 1.0, 0.0, "RunRight"], + ["forward-right", 1.0, 1.0, ""], + ["back-left", -1.0, -1.0, ""], ] -const SETTLE := 70 -## Frames averaged at the end of each case — comfortably more than one stride. -const MEASURE_OVER := 40 -var _frames := 0 -var _model: SkinnedPlayerModel = null -var _probe = null -var _case := 0 -var _rows: Array = [] -var _hips_sum := 0.0 -var _chest_sum := 0.0 -var _n := 0.0 +var _failures := 0 func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + await process_frame var args := OS.get_cmdline_user_args() - var path: String = args[0] if args.size() > 0 \ + var path: String = args[0] if not args.is_empty() \ else "res://assets/characters/skins/taila.glb" + var scene := Node3D.new() root.add_child(scene) current_scene = scene - _model = SkinnedPlayerModel.new() - _model.model_path = path - scene.add_child(_model) + var model := SkinnedPlayerModel.new() + model.model_path = path + scene.add_child(model) + + for _frame in 60: + await process_frame + if not model.loaded or model.skeleton == null: + _expect(false, "the authored character loads") + _done() + return + + var foot := model._role_bone("foot.L", ["foot.L", "LeftFoot", "Left ankle"]) + _expect(foot >= 0, "a foot bone resolves") + + for test_case in CASES: + var direction := Vector2(float(test_case[1]), float(test_case[2])).normalized() + for _frame in SETTLE_FRAMES: + model.set_locomotion(direction.x, direction.y, 0.0) + model.update_state("ground", SPEED, false) + await process_frame + + var blend := model.locomotion_blend_debug() + _expect(blend.length() > 0.01 and blend.normalized().dot(direction) > 0.999, + "%s drives the authored BlendSpace in the physical direction" + % String(test_case[0])) + _expect(absf(model.locomotion_effective_speed_debug() - SPEED) < 0.02, + "%s authored stride matches %.1f m/s" + % [String(test_case[0]), SPEED]) + var expected_clip := String(test_case[3]) + if not expected_clip.is_empty(): + _expect(model.current_clip_debug() == expected_clip, + "%s selects %s" % [String(test_case[0]), expected_clip]) + + if foot >= 0: + var start := model.skeleton.get_bone_pose_rotation(foot) + var greatest := 0.0 + for _frame in SAMPLE_FRAMES: + model.set_locomotion(direction.x, direction.y, 0.0) + model.update_state("ground", SPEED, false) + await process_frame + greatest = maxf(greatest, start.angle_to( + model.skeleton.get_bone_pose_rotation(foot))) + _expect(greatest > 0.03, + "%s uses a moving authored foot cycle (%.3f rad)" + % [String(test_case[0]), greatest]) + + model.queue_free() + _done() -## Bone poses read from INSIDE the modifier pass — Godot restores them after it, -## so anything read later is the animation alone with the pose layer missing. -class PoseProbe extends SkeletonModifier3D: - var pose: Array = [] - - func _process_modification() -> void: - var skel := get_skeleton() - if skel == null: - return - pose.resize(skel.get_bone_count()) - for i in skel.get_bone_count(): - pose[i] = skel.get_bone_global_pose(i) - - -func _process(_delta: float) -> bool: - _frames += 1 - if _frames < 8 or not _model.loaded: - return false - var skel: Skeleton3D = _model.skeleton - if skel == null: - return true - if _probe == null: - _probe = PoseProbe.new() - _probe.name = "TravelProbe" - skel.add_child(_probe) - return false - - var c: Array = CASES[_case] - # Run fast enough that the lean, which scales with speed, is fully in. - _model.update_state("ground", 9.0, false) - _model.set_locomotion(c[1], c[2], 0.0) - # AVERAGED over the tail of each case, not sampled at one instant. - # - # A run cycle twists the torso against the hips by tens of degrees twice per - # stride, all of it authored and correct. Reading a single frame therefore - # measures wherever the clip happened to be, and the numbers swing far wider - # than anything the pose layer does — a backpedal read as 50 degrees of chest - # yaw with the hips almost square, which is the clip, not the layer. - var phase: int = (_frames - 9) % SETTLE - if phase >= SETTLE - MEASURE_OVER and _probe.pose.size() == skel.get_bone_count(): - _hips_sum += _yaw_of(skel, "hips") - _chest_sum += _yaw_of(skel, "spine_top") - _n += 1.0 - if phase != SETTLE - 1: - return false - - if _probe.pose.size() == skel.get_bone_count(): - _rows.append([c[0], _hips_sum / _n, _chest_sum / _n, - float(c[3]), _model.stride_reversed_debug()]) - _hips_sum = 0.0 - _chest_sum = 0.0 - _n = 0.0 - _case += 1 - if _case >= CASES.size(): - _report() - return true - return false - - -## Yaw of a bone away from its own rest orientation, in degrees, about the -## character's up axis. Measured against REST rather than against the parent, so -## it reads as "how far round from square" whatever the rig's bone axes are. -func _yaw_of(skel: Skeleton3D, which: String) -> float: - var roles: Dictionary = _model._rig_info.get("roles", {}) - var name := "" - if which == "hips": - name = String(roles.get("hips", "DEF-hips")) +func _expect(ok: bool, description: String) -> void: + if ok: + print(" OK: ", description) else: - var spine: Array = roles.get("spine", []) - # The chest: the last spine bone that is not the neck or the head. - var neck := String(roles.get("neck", "")) - var head := String(roles.get("head", "")) - for n in spine: - if String(n) != neck and String(n) != head: - name = String(n) - var b := skel.find_bone(name) - if b < 0: - return 0.0 - var now: Basis = (_probe.pose[b] as Transform3D).basis - var rest: Basis = skel.get_bone_global_rest(b).basis - var delta := rest.inverse() * now - # The character faces +Z; how far that axis has swung in the ground plane. - var f: Vector3 = rest * (delta * (rest.inverse() * Vector3(0, 0, 1))) - return rad_to_deg(atan2(f.x, f.z)) + printerr(" FAIL: ", description) + _failures += 1 -func _report() -> void: - print("\n=== does the body say which way it is going? ===") - print(" %-13s %9s %9s %9s %s" % [ - "input", "hips", "chest", "want legs", "stride"]) - for r in _rows: - # A reversed stride points the feet the opposite way to the hips. - var legs: float = r[1] + (180.0 if r[4] else 0.0) - var err: float = absf(wrapf(legs - r[3], -180.0, 180.0)) - print(" %-13s %+8.1f° %+8.1f° %+8.1f° %-8s legs off by %5.1f°%s" % [ - r[0], r[1], r[2], r[3], "reversed" if r[4] else "forward", err, - " CHEST OFF AIM" if absf(r[2]) > 25.0 else ""]) - print("\n 'legs off by' is how far the stride points from the travel") - print(" direction. The hips are capped (see MAX_TRAVEL_YAW), so a pure") - print(" sidestep is expected to fall short — a shooter keeps its aim.\n") +func _done() -> void: + print("\n=== AUTHORED DIRECTION SUMMARY ===") + print("Failures: %d" % _failures) + quit(1 if _failures > 0 else 0) diff --git a/debug/traversal_animation_check.gd b/debug/traversal_animation_check.gd new file mode 100644 index 0000000..20b75ca --- /dev/null +++ b/debug/traversal_animation_check.gd @@ -0,0 +1,232 @@ +extends SceneTree + +## Regression gate for authored wall running and physical grapple posing. +## +## Wall running must select side-specific authored performances, never a ground +## run under another name. Grapple must keep its authored airborne base while +## the live arm constraint lands the palm on the cable ray and body alignment +## responds to pendulum energy. + +const SETTLE_FRAMES := 90 +const SAMPLE_FRAMES := 30 +const WALL_RUN_SPEED := 13.0 +const WALL_RUN_REFERENCE_SPEED := 8.25 + +var _failures := 0 + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + await process_frame + var args := OS.get_cmdline_user_args() + var path: String = args[0] if not args.is_empty() \ + else "res://assets/characters/skins/taila.glb" + + var scene := Node3D.new() + root.add_child(scene) + current_scene = scene + var model := SkinnedPlayerModel.new() + model.model_path = path + scene.add_child(model) + + for _frame in 60: + await process_frame + if not model.loaded or model.skeleton == null: + _expect(false, "the authored character loads") + _done() + return + + var foot := model._role_bone("foot.L", ["foot.L", "LeftFoot", "Left ankle"]) + var hips := model._role_bone("hips", ["hips", "Hips", "pelvis"]) + var left_hand := model._role_bone("hand.L", ["hand.L", "LeftHand", "hand_l"]) + var right_hand := model._role_bone("hand.R", ["hand.R", "RightHand", "hand_r"]) + _expect(foot >= 0, "a foot bone resolves") + _expect(hips >= 0 and left_hand >= 0 and right_hand >= 0, + "hips and both hands resolve") + + model.set_wall_side(-1.0) + model.set_wall_run_motion(Vector3(13.0, 0.0, 0.0)) + await _settle(model, "wall_run", WALL_RUN_SPEED) + _expect(model.current_clip_debug() == "WallRunLeft", + "left wall selects the authored WallRunLeft performance") + var wall_scale := float(model._anim_tree.get("parameters/loco_scale/scale")) + _expect(absf(wall_scale - WALL_RUN_SPEED / WALL_RUN_REFERENCE_SPEED) < 0.02, + "wall-run cadence tracks %.1f m/s (%.3fx)" + % [WALL_RUN_SPEED, wall_scale]) + var wall_foot_motion := await _sample_foot_motion( + model, foot, "wall_run", WALL_RUN_SPEED) + _expect(wall_foot_motion > 0.30, + "wall-run has a full wall-stride leg cycle (%.3f rad)" + % wall_foot_motion) + var wall_forward := model.wall_run_forward_debug() + _expect(wall_forward.dot(Vector3.RIGHT) > 0.97, + "wall-run model faces along wall-tangent velocity (dot %.3f)" + % wall_forward.dot(Vector3.RIGHT)) + model.set_wall_side(1.0) + await _settle(model, "wall_run", WALL_RUN_SPEED) + _expect(model.current_clip_debug() == "WallRunRight", + "right wall selects the authored WallRunRight performance") + _expect(not model.clip_names_debug().has("WallRun"), + "the relabelled ground-run WallRun clip is absent") + + # The imported performance must own the unarmed silhouette. A target rig's + # crossed/T-pose rest arms used to survive every clip because only relative + # bone rotations were copied. + model.set_weapon("") + model.set_locomotion(0.0, 0.0, 0.0) + await _settle(model, "idle", 0.0) + var left_shoulder := model._role_bone( + "shoulder.L", ["shoulder.L", "LeftShoulder", "clavicle_l"]) + var right_shoulder := model._role_bone( + "shoulder.R", ["shoulder.R", "RightShoulder", "clavicle_r"]) + var idle_drop := minf( + model.skeleton.get_bone_global_pose(left_shoulder).origin.y + - model.skeleton.get_bone_global_pose(left_hand).origin.y, + model.skeleton.get_bone_global_pose(right_shoulder).origin.y + - model.skeleton.get_bone_global_pose(right_hand).origin.y, + ) + _expect(idle_drop > 0.22, + "unarmed idle hands hang below the shoulders (drop %.3f m)" + % idle_drop) + + # A real backward capture turns/leans the whole body. The old fabricated + # version rotated only hips and legs by 180 degrees, leaving the first + # spine joint facing forward and creating an abdominal corkscrew. + model.set_locomotion(0.0, -1.0, 0.0) + await _settle(model, "ground", 8.25) + var spine_names: Array = model._rig_info.get("roles", {}).get("spine", []) + var first_spine := model.skeleton.find_bone(String(spine_names[0])) \ + if not spine_names.is_empty() else -1 + _expect(first_spine >= 0, "the first spine bone resolves") + if first_spine >= 0: + var hips_pose := model.skeleton.get_bone_global_pose(hips) \ + .basis.get_rotation_quaternion() + var hips_rest := model.skeleton.get_bone_global_rest(hips) \ + .basis.get_rotation_quaternion() + var spine_pose := model.skeleton.get_bone_global_pose(first_spine) \ + .basis.get_rotation_quaternion() + var spine_rest := model.skeleton.get_bone_global_rest(first_spine) \ + .basis.get_rotation_quaternion() + var hips_delta := hips_pose * hips_rest.inverse() + var spine_delta := spine_pose * spine_rest.inverse() + var abdomen_twist := hips_delta.angle_to(spine_delta) + _expect(abdomen_twist < 1.45, + "backward capture keeps hips and abdomen coherent (%.3f rad)" + % abdomen_twist) + + var anchor := model.global_position + Vector3(3.0, 7.0, -6.0) + await _settle_grapple(model, anchor, Vector3(1.0, 0.0, 0.0)) + _expect(model.current_clip_debug() == "Grapple", + "grapple selects the authored Grapple clip") + var grapple_scale := float(model._anim_tree.get("parameters/loco_scale/scale")) + _expect(absf(grapple_scale - 1.0) < 0.001, + "grapple holds its authored pose at 1.0x") + var grapple_foot_motion := await _sample_grapple_foot_motion( + model, foot, anchor, Vector3(1.0, 0.0, 0.0)) + _expect(grapple_foot_motion < 0.06, + "grapple legs stay held instead of swimming (%.3f rad)" + % grapple_foot_motion) + + var low: Dictionary = model.grapple_debug() + print(" grapple slow debug: ", low) + _expect(float(low.get("line_error", 1.0)) < 0.035, + "slow grapple palm lies on the cable ray (%.1f mm error)" + % (float(low.get("line_error", 1.0)) * 1000.0)) + _expect(float(low.get("alignment", 1.0)) < 0.20, + "slow grapple hangs under gravity (alignment %.3f)" + % float(low.get("alignment", 1.0))) + var low_body: Quaternion = low.get( + "body_rotation", Quaternion.IDENTITY) + + await _settle_grapple(model, anchor, Vector3(18.0, 2.0, 4.0)) + var medium: Dictionary = model.grapple_debug() + print(" grapple medium debug: ", medium) + _expect(float(medium.get("alignment", 1.0)) < 0.60, + "medium grapple still reads as hanging (alignment %.3f)" + % float(medium.get("alignment", 1.0))) + + await _settle_grapple(model, anchor, Vector3(32.0, 4.0, 8.0)) + var fast: Dictionary = model.grapple_debug() + print(" grapple high-speed debug: ", fast) + _expect(float(fast.get("line_error", 1.0)) < 0.035, + "high-speed grapple palm stays on the cable ray (%.1f mm error)" + % (float(fast.get("line_error", 1.0)) * 1000.0)) + _expect(float(fast.get("alignment", 0.0)) > 0.85, + "only high-speed grapple aligns the body (alignment %.3f)" + % float(fast.get("alignment", 0.0))) + var fast_body: Quaternion = fast.get( + "body_rotation", Quaternion.IDENTITY) + _expect(low_body.angle_to(fast_body) > 0.20, + "body orientation physically changes with swing energy (%.3f rad)" + % low_body.angle_to(fast_body)) + + model.queue_free() + _done() + + +func _settle(model: SkinnedPlayerModel, state: String, speed: float) -> void: + for _frame in SETTLE_FRAMES: + model.update_state(state, speed, false) + await process_frame + + +func _sample_foot_motion( + model: SkinnedPlayerModel, + foot: int, + state: String, + speed: float, +) -> float: + if foot < 0: + return 0.0 + var start := model.skeleton.get_bone_pose_rotation(foot) + var greatest := 0.0 + for _frame in SAMPLE_FRAMES: + model.update_state(state, speed, false) + await process_frame + greatest = maxf(greatest, start.angle_to( + model.skeleton.get_bone_pose_rotation(foot))) + return greatest + + +func _settle_grapple(model: SkinnedPlayerModel, anchor: Vector3, + velocity: Vector3) -> void: + for _frame in SETTLE_FRAMES: + model.update_state("grapple", velocity.length(), false) + model.set_grapple_target(anchor, velocity) + await process_frame + + +func _sample_grapple_foot_motion( + model: SkinnedPlayerModel, + foot: int, + anchor: Vector3, + velocity: Vector3, +) -> float: + if foot < 0: + return 0.0 + var start := model.skeleton.get_bone_pose_rotation(foot) + var greatest := 0.0 + for _frame in SAMPLE_FRAMES: + model.update_state("grapple", velocity.length(), false) + model.set_grapple_target(anchor, velocity) + await process_frame + greatest = maxf(greatest, start.angle_to( + model.skeleton.get_bone_pose_rotation(foot))) + return greatest + + +func _expect(ok: bool, description: String) -> void: + if ok: + print(" OK: ", description) + else: + printerr(" FAIL: ", description) + _failures += 1 + + +func _done() -> void: + print("\n=== AUTHORED TRAVERSAL SUMMARY ===") + print("Failures: %d" % _failures) + quit(1 if _failures > 0 else 0) diff --git a/debug/traversal_animation_check.gd.uid b/debug/traversal_animation_check.gd.uid new file mode 100644 index 0000000..8fcd1ba --- /dev/null +++ b/debug/traversal_animation_check.gd.uid @@ -0,0 +1 @@ +uid://b43ksbou375or diff --git a/debug/visual_capture.gd b/debug/visual_capture.gd index 567524c..63c1a91 100644 --- a/debug/visual_capture.gd +++ b/debug/visual_capture.gd @@ -17,6 +17,14 @@ func _initialize() -> void: _out_dir = args[0] if args.size() > 1: _scene = args[1] + if not _out_dir.is_absolute_path(): + _out_dir = ProjectSettings.globalize_path(_out_dir) + var mkdir_error := DirAccess.make_dir_recursive_absolute(_out_dir) + if mkdir_error != OK: + printerr("visual_capture: could not create '%s' (error %d)" % [ + _out_dir, mkdir_error]) + quit(mkdir_error) + return change_scene_to_file("res://ui/main_menu/main_menu.tscn") @@ -61,6 +69,11 @@ func _process(_delta: float) -> bool: cam.look_at(Vector3(-8, 4, -18), Vector3.UP) elif _frames == 360: _shot("street") + # Let the loaded scene and RenderingServer resources drain before the + # process exits. Large CSG maps can still have deferred frees queued on + # the exact frame of the last screenshot. + unload_current_scene() + elif _frames == 480: return true return false @@ -68,5 +81,9 @@ func _process(_delta: float) -> bool: func _shot(tag: String) -> void: var img := root.get_viewport().get_texture().get_image() var path := _out_dir + "/shot_" + tag + ".png" - img.save_png(path) - print("visual_capture: saved ", path) + var save_error := img.save_png(path) + if save_error == OK: + print("visual_capture: saved ", path) + else: + printerr("visual_capture: could not save '%s' (error %d)" % [ + path, save_error]) diff --git a/debug/walkability_probe.gd b/debug/walkability_probe.gd new file mode 100644 index 0000000..2a77386 --- /dev/null +++ b/debug/walkability_probe.gd @@ -0,0 +1,385 @@ +extends SceneTree + +## Is every part of a map actually reachable on foot? +## +## godot --headless --path . -s res://debug/walkability_probe.gd \ +## -- res://scenes/maps/sakura_crossing/sakura_crossing.tscn 80 60 +## +## Builds a 2.5-D walkability graph of the level and flood-fills it from the +## spawn points, then reports what it could not reach. +## +## ── Why a probe and not an eyeball ─────────────────────────────────────────── +## +## A stair that climbs away from its landing, a pavement that stops at a wall, a +## roof with no way onto it — these are all invisible from the one camera angle +## you happen to photograph, and all obvious the moment you try to WALK there. +## Screenshots verify how a map looks; only a traversal check verifies that it +## works. Sakura Crossing shipped with three staircases ending in mid-air and +## not one of the captures showed it. +## +## ── How it works ───────────────────────────────────────────────────────────── +## +## For every cell of a horizontal grid the probe rays downward repeatedly, +## collecting EVERY up-facing surface in that column, not just the topmost one. +## That matters here: a single downward ray over the service alley hits the +## footbridge deck six metres above it, and the alley — the thing you actually +## want to check — never appears in the graph at all. +## +## Edges are DIRECTED. You may always fall, but you may only climb `STEP_UP`, +## the same 0.95 m the ground state uses for its step assist. So the forward +## fill answers "can a player get here from spawn", and the reverse fill answers +## "having got here, can they get back" — a surface that fails only the second +## test is a pit, which is its own kind of bug. + +## ── Sample spacing, and why it is this small ───────────────────────────────── +## +## Two separate false failures came out of sampling too coarsely, and both of +## them looked exactly like a broken map: +## +## 2.0 m missed the 1.3 m-wide fire-escape flights altogether — the rays +## landed beside the stairs — so every roof they served was reported +## unreachable. +## 1.0 m found the stairs but could not WALK them. A flight with a 0.34 m +## going climbs about 0.96 m per metre travelled, which is just over +## the 0.95 m step limit, so the probe declared a perfectly good +## staircase impassable at every other sample. +## +## The rule: CELL must be small enough that one cell of travel is at most one +## or two treads of climb. At 0.5 m the steepest flight in the map gains 0.64 m +## per cell, comfortably inside the limit. +const CELL := 0.5 +const STEP_UP := 0.95 # matches max_step_height in state_ground.gd +const WALKABLE_NORMAL := 0.7 # cos of the steepest floor we call ground +const RAY_TOP := 60.0 +const RAY_BOTTOM := -6.0 +const MAX_LEVELS := 8 +const MAX_FALL := 12.0 +## Islands smaller than this are single props — a bollard top, a lantern cap — +## and reporting them is noise rather than signal. +const MIN_ISLAND := 12 + +const DIRS: Array[Vector2i] = [ + Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1), +] + +## ── Named routes ───────────────────────────────────────────────────────────── +## +## The reachability percentage is a summary, and a summary can hide the one +## thing you actually care about: 96% reachable is a pass mark that would still +## be reported if the entire railway were sealed off. These are the places a +## player must be able to stand and walk between, named, so a failure says +## "the alley is cut off" instead of "4% unreachable". +const ROUTES := { + "sakura_crossing": [ + ["railway west end", Vector3(-108, 0.4, 0)], + ["railway at the crossing", Vector3(0, 0.4, 0)], + ["railway before the curve", Vector3(58, 0.4, 0)], + ["railway through the curve", Vector3(95, 0.4, -12)], + ["main street, north end", Vector3(0, 0.1, -84)], + ["main street, south end", Vector3(0, 0.1, 84)], + ["shopping street, west", Vector3(-108, 0.1, -30)], + ["shopping street, east", Vector3(46, 0.1, -30)], + ["south street, west", Vector3(-108, 0.1, 20)], + ["south street, east", Vector3(108, 0.1, 20)], + ["service alley, west", Vector3(-108, 0.2, -11)], + ["service alley, east", Vector3(46, 0.2, -11)], + ["north fields lane, west", Vector3(-90, 0.1, -69)], + ["north fields lane, east", Vector3(90, 0.1, -69)], + ["footbridge deck", Vector3(-40, 6.2, 0)], + ["footbridge north stair foot", Vector3(-47, 0.2, -10)], + ["footbridge south stair foot", Vector3(-33, 0.2, 10)], + ["footbridge approach street", Vector3(-40, 0.1, -18)], + ["station platform", Vector3(42, 1.1, 9)], + ["station canopy roof", Vector3(40, 4.6, 9)], + ["shrine approach, at the street", Vector3(-22, 0.1, 29)], + ["shrine steps", Vector3(-22, 0.1, 48)], + ["east district, north-south street", Vector3(64, 0.1, 40)], + ["east district, east-west street", Vector3(30, 0.1, 54)], + ["east district, far corner", Vector3(100, 0.1, 74)], + # A two-storey shophouse roof on the railway row. The exact unit matters: + # the row is laid out from a seeded RNG, so a waypoint has to sit on a + # roof that is actually there rather than in the joint between two. + ["a shop roof", Vector3(27, 6.8, -19)], + ], +} + +var _scene := "res://scenes/maps/sakura_crossing/sakura_crossing.tscn" +var _half_x := 80.0 +var _half_z := 60.0 + +var _nx := 0 +var _nz := 0 +# Flat arrays keyed by cell*MAX_LEVELS + level. String-keyed dictionaries are +# unusable at this resolution — 77 000 columns is a quarter of a million +# lookups per fill, and GDScript hashes every one of those strings. +var _height := PackedFloat32Array() +var _count := PackedByteArray() + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if args.size() > 0: + _scene = args[0] + if args.size() > 2: + _half_x = float(args[1]) + _half_z = float(args[2]) + _run() + + +func _run() -> void: + await process_frame + var packed: PackedScene = load(_scene) + if packed == null: + printerr("walkability: cannot load ", _scene) + quit(1) + return + var level: Node = packed.instantiate() + root.add_child(level) + for _i in 90: + await process_frame + for _i in 8: + await physics_frame + + var space: PhysicsDirectSpaceState3D = \ + level.get_viewport().find_world_3d().direct_space_state + _nx = int(_half_x * 2.0 / CELL) + _nz = int(_half_z * 2.0 / CELL) + var cells := _nx * _nz + _height.resize(cells * MAX_LEVELS) + _count.resize(cells) + + var total := 0 + for ix in range(_nx): + for iz in range(_nz): + var x := -_half_x + (float(ix) + 0.5) * CELL + var z := -_half_z + (float(iz) + 0.5) * CELL + total += _column(space, x, z, (ix * _nz + iz)) + + var seeds := PackedInt32Array() + for s in level.find_children("*", "Marker3D", true, false): + if s.is_in_group("spawn_points"): + var n := _nearest(s.global_position) + if n >= 0: + seeds.append(n) + if seeds.is_empty(): + var n := _nearest(Vector3(0, 1.0, 0)) + if n >= 0: + seeds.append(n) + + var reached := _fill(seeds, true) + var can_return := _fill(seeds, false) + + var reach_n := 0 + for v in reached: + if v: + reach_n += 1 + + print("WALK: %s" % _scene) + print("WALK: %d columns, %d walkable surfaces, %d spawn seeds" + % [cells, total, seeds.size()]) + print("WALK: reachable from spawn: %d / %d (%.1f%%)" + % [reach_n, total, 100.0 * float(reach_n) / maxf(1.0, float(total))]) + + var stranded := _islands(reached) + var pits := 0 + for c in range(cells): + for li in range(_count[c]): + var id := c * MAX_LEVELS + li + if reached[id] and not can_return[id]: + pits += 1 + + # ── The named routes ───────────────────────────────────────────────────── + var route_fails := 0 + var key := _scene.get_file().get_basename() + if ROUTES.has(key): + print("WALK: named routes —") + for entry in ROUTES[key]: + var label: String = entry[0] + var want: Vector3 = entry[1] + var id := _nearest_loose(want) + if id < 0: + print("WALK: MISSING %s — no walkable ground near (%.0f, %.1f, %.0f)" + % [label, want.x, want.y, want.z]) + route_fails += 1 + elif not reached[id]: + print("WALK: CUT OFF %s (found ground at y=%.2f)" + % [label, _height[id]]) + route_fails += 1 + elif not can_return[id]: + print("WALK: ONE-WAY %s — reachable but cannot get back" + % label) + route_fails += 1 + else: + print("WALK: ok %s" % label) + if route_fails == 0: + print("WALK: all %d named routes connect" % ROUTES[key].size()) + + if stranded.is_empty(): + print("WALK: PASS — every walkable surface is reachable from a spawn") + else: + print("WALK: %d unreachable island(s) of %d+ surfaces:" + % [stranded.size(), MIN_ISLAND]) + for isl in stranded: + print("WALK: %5d x[%.0f..%.0f] z[%.0f..%.0f] y[%.2f..%.2f]" + % [isl.count, isl.bb_min.x, isl.bb_max.x, isl.bb_min.z, + isl.bb_max.z, isl.bb_min.y, isl.bb_max.y]) + if pits > 0: + print("WALK: %d surface(s) reachable but not escapable (one-way pits)" % pits) + + level.queue_free() + for _i in 4: + await process_frame + quit(0 if route_fails == 0 else 2) + + +## Every up-facing surface in one column, top to bottom. +func _column(space: PhysicsDirectSpaceState3D, x: float, z: float, cell: int) -> int: + var n := 0 + var y := RAY_TOP + var ex: Array[RID] = [] + for _i in MAX_LEVELS * 2: + if n >= MAX_LEVELS: + break + var q := PhysicsRayQueryParameters3D.create( + Vector3(x, y, z), Vector3(x, RAY_BOTTOM, z), 1) + q.exclude = ex + var hit: Dictionary = space.intersect_ray(q) + if hit.is_empty(): + break + var pos: Vector3 = hit["position"] + var nrm: Vector3 = hit["normal"] + if nrm.y >= WALKABLE_NORMAL: + _height[cell * MAX_LEVELS + n] = pos.y + n += 1 + ex.append(hit["rid"]) + y = pos.y - 0.001 + if y <= RAY_BOTTOM: + break + _count[cell] = n + return n + + +func _nearest(pos: Vector3) -> int: + var ix := int((pos.x + _half_x) / CELL) + var iz := int((pos.z + _half_z) / CELL) + if ix < 0 or iz < 0 or ix >= _nx or iz >= _nz: + return -1 + var cell := ix * _nz + iz + var best := -1 + var best_d := 3.0 + for li in range(_count[cell]): + var d: float = absf(_height[cell * MAX_LEVELS + li] - pos.y) + if d < best_d: + best_d = d + best = cell * MAX_LEVELS + li + return best + + +## As `_nearest`, but tolerant on height and willing to search the neighbouring +## cells. Route waypoints are written by hand from the builder's constants, so +## their Y is approximate and their X/Z can land on a kerb or a paint stripe. +func _nearest_loose(pos: Vector3) -> int: + var best := -1 + var best_d := 3.0 + for ox in range(-2, 3): + for oz in range(-2, 3): + var ix := int((pos.x + _half_x) / CELL) + ox + var iz := int((pos.z + _half_z) / CELL) + oz + if ix < 0 or iz < 0 or ix >= _nx or iz >= _nz: + continue + var cell := ix * _nz + iz + for li in range(_count[cell]): + var d: float = absf(_height[cell * MAX_LEVELS + li] - pos.y) + if d < best_d: + best_d = d + best = cell * MAX_LEVELS + li + return best + + +## Directed flood fill. `forward` climbs at most STEP_UP and falls freely; +## reversed, it finds what can get BACK to the seeds. +func _fill(seeds: PackedInt32Array, forward: bool) -> Array: + var seen := [] + seen.resize(_nx * _nz * MAX_LEVELS) + seen.fill(false) + var queue := PackedInt32Array() + for s in seeds: + if not seen[s]: + seen[s] = true + queue.append(s) + var head := 0 + while head < queue.size(): + var id: int = queue[head] + head += 1 + var cell := id / MAX_LEVELS + var y := _height[id] + var ix := cell / _nz + var iz := cell % _nz + for d in DIRS: + var jx := ix + d.x + var jz := iz + d.y + if jx < 0 or jz < 0 or jx >= _nx or jz >= _nz: + continue + var ncell := jx * _nz + jz + for lj in range(_count[ncell]): + var nid := ncell * MAX_LEVELS + lj + if seen[nid]: + continue + var ny := _height[nid] + # Climbing is capped; falling is not. A fall of more than a + # storey is still a legal move, just not a reversible one. + var rise: float = (ny - y) if forward else (y - ny) + if rise > STEP_UP or rise < -MAX_FALL: + continue + seen[nid] = true + queue.append(nid) + return seen + + +## Group everything the fill missed into contiguous islands. +func _islands(reached: Array) -> Array: + var seen := [] + seen.resize(_nx * _nz * MAX_LEVELS) + seen.fill(false) + var out: Array = [] + for cell in range(_nx * _nz): + for li in range(_count[cell]): + var start := cell * MAX_LEVELS + li + if reached[start] or seen[start]: + continue + seen[start] = true + var queue := PackedInt32Array([start]) + var head := 0 + var n := 0 + var lo := Vector3.INF + var hi := -Vector3.INF + while head < queue.size(): + var id: int = queue[head] + head += 1 + n += 1 + var c := id / MAX_LEVELS + var ix := c / _nz + var iz := c % _nz + var y := _height[id] + var p := Vector3(-_half_x + (float(ix) + 0.5) * CELL, y, + -_half_z + (float(iz) + 0.5) * CELL) + lo = lo.min(p) + hi = hi.max(p) + for d in DIRS: + var jx := ix + d.x + var jz := iz + d.y + if jx < 0 or jz < 0 or jx >= _nx or jz >= _nz: + continue + var ncell := jx * _nz + jz + for lj in range(_count[ncell]): + var nid := ncell * MAX_LEVELS + lj + if seen[nid] or reached[nid]: + continue + if absf(_height[nid] - y) > STEP_UP: + continue + seen[nid] = true + queue.append(nid) + if n >= MIN_ISLAND: + out.append({ "count": n, "bb_min": lo, "bb_max": hi }) + out.sort_custom(func(a, b): return a.count > b.count) + return out diff --git a/debug/walkability_probe.gd.uid b/debug/walkability_probe.gd.uid new file mode 100644 index 0000000..c056324 --- /dev/null +++ b/debug/walkability_probe.gd.uid @@ -0,0 +1 @@ +uid://00s457y80gpu diff --git a/debug/weapon_hold_check.gd b/debug/weapon_hold_check.gd index 4d12877..0dbef8e 100644 --- a/debug/weapon_hold_check.gd +++ b/debug/weapon_hold_check.gd @@ -27,12 +27,10 @@ extends SceneTree ## ## ── Measured in the SHOULDER's frame ──────────────────────────────────────── ## -## Not in world space, and not even in skeleton space. The hold breathes — a -## `sin(_time * 2.2) * 0.012` on the muzzle pitch — and the idle clip moves the -## whole torso, so no hand is ever at the same place twice and two samples of the -## SAME weapon would differ by more than two different weapons do. Taking each -## hand relative to the right shoulder joint, in the chest's own basis, cancels -## both, because they move the shoulder and the hand together. +## Not in world space, and not even in skeleton space. The authored idle clip +## moves the whole torso, so a world-space sample would mix body motion into the +## hold. Taking each hand relative to the right shoulder joint in the chest's +## own basis cancels that motion. const LAB := "res://debug/rig_lab.tscn" diff --git a/docs/CHARACTER_ART_DIRECTION.md b/docs/CHARACTER_ART_DIRECTION.md new file mode 100644 index 0000000..9693ee0 --- /dev/null +++ b/docs/CHARACTER_ART_DIRECTION.md @@ -0,0 +1,85 @@ +# Character Art Direction + +## North star + +Papaya Shooter is **urban anime first, expressive utility tech second**. +Grace Howard from *Zenless Zone Zero* is a directional reference for the +relationship between character and machinery: confident, readable streetwear +and personality remain dominant while a few believable tools explain +extraordinary movement. It is not a costume, silhouette, or prop set to copy. + +The working ratio is: + +```text +90% character / urban anime +10% functional mechanics / technology +``` + +The ratio is perceptual rather than a polygon count. At combat distance, the +player should identify face, hair, body language, clothing silhouette, team +colour and weapon before noticing a mechanism. Technology earns attention only +when its related action begins. + +## The 90%: character and city + +- Strong hair, face, garment and pose silhouettes carry recognition. +- Clothing uses broad graphic panels, asymmetry, layered streetwear and one + clear accent rhythm instead of dense sci-fi segmentation. +- Materials stay readable: cloth looks soft, rubberized trim looks matte, and + skin/hair keep their anime value grouping. +- Decals, seams and accessories describe taste, job and attitude before lore. +- Animation sells confidence and intent. The body leads; hardware follows. + +Avoid full exosuit coverage, exposed machinery on every limb, uniform emissive +strips, or greebles spread evenly across the body. Those invert the ratio and +turn a character into a vehicle. + +## The 10%: hero tools + +Each character may express the same movement verbs through a different form +language, but the function and timing remain legible. + +### Jump jets + +Jump jets support bunny-hopping, air correction and quick direction changes. +They may live at heels, calves, a belt, coat hardware or a compact backpack. +The visual event is a short deployment, a graphic thrust wedge, and a fast +retraction. The audio is mostly air/thrust with one soft actuator detail. + +### Grappling hook + +The grapple needs three readable beats: launch, latch, and loaded reel. Its +housing may be wrist-mounted, weapon-mounted, hip-carried or deployed by a +small companion device. Cable direction and tension matter more than housing +detail. The mechanism stays quiet enough that the body swing remains the hero. + +### Thrust dash + +Dashing is a decisive impulse, not sustained flight. Thrusters may appear at +the waist, shoulder blades, boots or weapon rig. Use a clean ignition core, +directional air wedge and brief afterimage. Do not add a rattling engine loop; +the device should be visually dormant again by the recovery pose. + +## Shared visual and audio law + +- **Idle:** technology is folded, dark and quiet. +- **Anticipation:** one seam opens, nozzle pivots, or latch arms. +- **Action:** a single bright shape and clean thrust/mechanical cue explains + the verb. +- **Recovery:** the tool retracts quickly and character animation takes focus. +- **Sound ratio:** roughly 90% motion, thrust and body; 10% relay, latch or + servo. No constant rattle, scrape wall, or narrow high-frequency chirp. +- **VFX ratio:** one authored wedge/ring/line is preferable to many particles. + +This law lets characters share technology without looking mass-produced. +Function, gameplay colour and timing stay consistent; placement, casing, +fasteners, deployment motion and wear pattern belong to the individual. + +## Review questions + +1. Does the silhouette still read as a person before it reads as equipment? +2. Can the movement tool be understood during its active animation? +3. Does the hardware disappear from attention when inactive? +4. Is every mechanical sound attached to a visible or gameplay-relevant cause? +5. Would removing half the greebles improve the character? If yes, remove them. + diff --git a/docs/GRAPHICAL_FIDELITY.md b/docs/GRAPHICAL_FIDELITY.md new file mode 100644 index 0000000..8b17b2c --- /dev/null +++ b/docs/GRAPHICAL_FIDELITY.md @@ -0,0 +1,254 @@ +# Graphical Fidelity Direction + +**Primary reference: [Sakura Crossing](https://github.com/Kenton-GMI/sakura-crossing)** +(MIT) — a procedural Japanese suburb in three.js, rendered to look like a +hand-painted 2D anime background. This is the target image for the project, with +one deliberate departure: **the line work is slightly harder and thicker.** + +Secondary reference: [The Long Silence](https://github.com/achimala/TheLongSilence). +The transferable lesson there is not its realistic-space rendering. It is that +one authored visual law is reused everywhere, expensive detail is paid for only +where it can be seen, and every art decision is checked in repeatable captures. + +Character-specific decisions are governed by +[CHARACTER_ART_DIRECTION.md](CHARACTER_ART_DIRECTION.md): 90% urban anime +character read, 10% selectively deployed utility technology. + +## The cel law + +Four layered techniques, ported from the reference. `scenes/maps/sakura_crossing` +is the map built to demonstrate all four; `scenes/maps/sakura_palette.gd` is the +palette port. + +**1. Quantised toon lighting with a hue-shifted shadow** — +`assets/shaders/sakura_cel.gdshader`. Direct light is snapped to one of 2–5 flat +values from a hand-authored ramp, sampled at `dot(N,L) * 0.5 + 0.5`. Then: + +``` +irradiance = band * mix(shadow_tint, vec3(1.0), band) +``` + +The band drives its own tint, so full light is untinted and the darkest band is +almost pure cool violet. **That hue shift in shadow is most of what separates +"anime cel" from "low-poly 3D"** — a wall whose shade is merely 40% of its lit +colour reads as an unlit polygon. High-key ramps (`RAMP_SOFT`, `RAMP_SOFT3`) +exist for pale masses like blossom that must stay light on the shadow side. + +Faceted shading is on by default and is not a shortcut: a quantised ramp over +smooth normals puts a *curved* band boundary across a curved surface, which +reads as an airbrushed gradient no matter how few steps it has. + +**2. Screen-space ink from the SECOND difference of linear depth** — +`assets/shaders/ink_edge.gdshader`. This replaced a conventional first-difference +edge detect, and the distinction is the single most important one in the file. A +first difference measures depth *slope*, which is large wherever a surface is +oblique to the camera — so the road, every roof and every long wall carried ink, +and the threshold had to be raised until it smeared. A second difference measures +*curvature*, which is exactly zero across any planar surface however oblique. It +therefore fires only on real silhouettes and real creases, and the threshold can +be low enough to catch a distant railing. + +Convex curvature inks strongly, concave faintly — the outside line of a shape is +heavy, the contact line inside it is light, which is what an animator draws. + +Two things to know when tuning it: `ink_hardness` also sets the **noise floor**, +because a grazing ground plane quantises in the depth buffer into a staircase and +a staircase has a large second difference at every step. Too soft and the road +comes out finely hatched. And anything smaller than the line width — petals, +distant wires — will be inked *solid*, so sub-pixel scatter belongs in the +transparent pass where the ink cannot see it. + +**3. Inverted-hull outlines** on hero props only — `toon_outline.gdshader`, +applied via `LevelMaterials.outline()`. + +**4. A four-light anime rig** — `LevelEnvironment._build_light_rig`. A warm +quantised key (the only shadow caster), a **strong** cool bounce from the +opposite quarter at roughly a third of the key, a weak violet up-light for +undersides, and a violet-ground hemisphere carried by ambient. The bounce is the +important one: it puts a distinct, cool, non-black value on every shadow plane, +so a surface turning away from the sun turns *into another colour* rather than +into darkness. + +The colour grade is a **split-tone** — cool violet into the darks, warm paper +white into the lights — and it lives at the end of the ink pass rather than in +`Environment.adjustment`, so it can be a split rather than a single saturation +number. Environment's adjustment stage is therefore disabled; enabling both +grades the frame twice. + +## Lettering and trim + +The reference is emphatic that *"every sign, fascia, lantern and price strip is +drawn at runtime — there is not one image asset in `src/`."* That is not a +technical footnote. A Japanese street is **made of lettering**, and the first +build of Sakura Crossing had correct forms, correct colour and no type on it +anywhere; it read as an architectural massing study. + +`scenes/maps/sakura_signage.gd` is the answer. Godot's equivalent of the +reference's Canvas2D is **`TextMesh`** — real geometry from a font — which suits +this renderer better than a drawn texture would, because the whole map is +untextured flat colour and, more importantly, **geometry gets inked**. Every +glyph picks up the screen-space outline exactly as the buildings do, which is +what seats the lettering *in* the picture rather than on top of it. + +Settings that matter, both measured: + +- **`depth = 0`.** Extruded text is 4 500 verts for four kanji; flat is 2 100. + The extrusion's side walls are most of the mesh and are never seen on a sign + fixed to a wall. Flat glyphs sit a centimetre proud of their board instead. +- **`curve_step = 8`.** The 0.5 default is 43 000 verts for three signs; 8 is + 13 700 and looks identical at any distance a sign is read from. + +The font is a **system stack** — the same one the reference uses (`Yu Gothic`, +`Meiryo`, `Hiragino Kaku Gothic ProN`, `MS Gothic`). Ships no font file; if none +resolve, signs go blank and nothing crashes. + +**Ink or no ink is an art decision, per object.** Raised lettering on a sign +board wants the outline — it is an object standing off a wall. Paint on the +ground does not: road markings were inked at first and came out *hollow*, +because a stroke of 止まれ at a grazing angle is a few pixels wide, the ink fired +on both edges and the two outlines met in the middle and ate the letter. Pass +`inked: false` and the glyphs go to the transparent pass where the ink cannot +see them. Same mechanism as the petals — **anything thinner than the line width +gets inked solid.** + +Trim is the other half. A cel image is judged on its *third* tier of structure, +and the first build had almost none: no sills, no downpipes, no meters, no +manholes, no tactile dots. All of it is render-only and most of it is a box — +none of it needs to be clever, it needs to be **there**. + +One layout lesson worth keeping: unit width is the character of a street. Eight +shop units across this map made each one 19.5 m wide, which is a department +store, not a shop — one awning and one noren stretched across sixty feet of +frontage. Six-to-ten-metre units give twenty awning colours, twenty names and +twenty doorways per side, and that rhythm is most of what a 商店街 *is*. + +Cost: ~5 300 mesh instances, 6.85 ms/frame at 1080p in the heaviest view. + +## Image contract + +Every gameplay frame should have: + +- A readable dark, mid and light band with less than 2% practical clipping. +- One dominant warm/cool lighting relationship, not white ambient everywhere. +- Dark contact at feet, props and building bases. +- Large, medium and small visual structure, in that order. +- Ink weight that follows importance: character silhouette, rigid accessory, + architecture crease, then tertiary texture. +- Quiet traversal space around loud signs, characters and combat VFX. +- Surface detail that disappears cleanly before it becomes sub-pixel noise. + +The current automatic guardrail is: + +```powershell +python tools/levels.py --inset 8 --gate +``` + +`debug/fidelity_probe.gd` is the deterministic look-development scene. +`debug/visual_capture.gd` remains the real-game regression capture. + +## Lighting and post + +- Keep the sun as the form key and ambient as a low-energy colour bed. +- Use tight SSAO only for contact. Broad grey AO fights flat cel bands. +- Keep cast shadow and form shadow separate; cast shadows may be deeper and + harder than a character or prop's terminator. +- Let only authored emissives cross the glow threshold. White paint must never + bloom. +- Preserve a filmic shoulder and measure clipping instead of judging it from a + single monitor. +- The first-person weapon uses a camera-relative warm key and cool fill. Its + graphic read must not change when the player turns in world space. + +## Texturing and materials + +`LevelMaterials.SURFACE_LAW` is the shared construction language: + +- `wall`: storey-scale seams plus a restrained base grade. +- `ground`: larger paving divisions with no vertical grade. +- `panel`: tight machined divisions for metal and kiosks. +- `trim`: grounding grade without a grid. + +These are semantic materials, not colour presets. Do not apply one noise field +to every asset. Foliage, vehicles and character silhouettes stay quiet unless +their own authored texture calls for detail. + +The next texture pass should add a small reusable decal sheet for: + +- one or two edge chips; +- drain and service markings; +- poster tears and tape corners; +- sparse hazard chevrons; +- directional pavement paint. + +Place decals by composition and gameplay route. Random coverage is not detail; +it is visual noise. + +## Modeling + +Build and review assets in three reads: + +1. Silhouette at combat distance. +2. Primary planes and construction breaks at traversal distance. +3. Fasteners, bevels and decals only at inspection distance. + +For the city kit: + +- Give rooflines and street corners deliberate silhouette interruptions. +- Prefer modeled awnings, frames, vents and cable bundles over high-frequency + material noise. +- Reuse a small vocabulary of construction dimensions so adjacent modules feel + manufactured together. +- Merge static pieces by material only after their panel coordinates are stable; + seams should continue across parts meant to read as one assembly. +- Add LOD or visibility ranges before adding tertiary geometry across all 100 + city blocks. + +## Character and weapon animation + +Anime fidelity comes from pose design and timing more than interpolation count: + +- Let the body lead every movement verb. Jump jets, grapple hardware and dash + thrusters deploy only long enough to explain the motion, then return visual + focus to the character. +- Gameplay anticipation must be short but unmistakable. +- Key combat silhouettes need a readable held pose before recovery. +- Accelerate through impact, hold the strongest pose, then use a clean recovery + arc instead of uniformly smooth motion. +- Keep weapon, support hand and head intent aligned; the existing hold-profile + checks should remain mandatory. +- Judge locomotion from the gameplay camera, not only in an animation viewer. +- Use `debug/anim_capture.gd`, `debug/fp_weapon_capture.gd` and the hold/pose + checks whenever animation assets change. + +Secondary cloth and hair should lag the torso and settle quickly enough not to +obscure aim or team recognition. Their job is to sell acceleration, not to run +an independent simulation showcase. + +## VFX + +- Reserve the brightest values for hits, muzzle flashes and abilities. +- Shape effects as graphic wedges, rings and speed lines before adding soft + particles. +- Use colour to communicate gameplay class consistently. +- Fade detail by projected size. A distant effect should become one clean spark, + not many sub-pixel particles. +- Keep screen-space distortion local and brief; the HUD and target silhouette + must remain readable. + +## Performance and review + +Fidelity features should degrade by resolution or distance before being +disabled outright. Panel seams and sky clouds are analytic and band-limited; +future decals and tertiary geometry should use visibility ranges. + +Review sets should include: + +- deterministic fidelity probe, key and grazing angles; +- Neon Alley street, spawn and aerial views; +- one bright and one dark character skin; +- each first-person weapon family; +- run, slide, dash, reload and one ability impact; +- a low render-scale capture to expose shimmer and sub-pixel detail. + +Compare the set as a group. A change is not an improvement if one hero frame is +better while combat readability or another map regresses. diff --git a/docs/SOUND_DESIGN.md b/docs/SOUND_DESIGN.md index da9f04b..7769f1e 100644 --- a/docs/SOUND_DESIGN.md +++ b/docs/SOUND_DESIGN.md @@ -1,67 +1,104 @@ -# Sound Design Plan +# Sound Design -The current sounds are procedurally generated sine/noise blends -(`generate_sounds.py`) — functional, but robotic. This doc is the path to -real sound design with zero budget. +Papaya Shooter's mix is **stylized impact, comfortable repetition**: graphic +and energetic enough to match the cel-shaded action, but deliberately free of +the clipped white-noise and narrow resonant tones that make a fast shooter +fatiguing. -## What's in place now +Character movement follows the same 90/10 rule as +`docs/CHARACTER_ART_DIRECTION.md`: about 90% clean motion/thrust/body and 10% +actuator, latch or servo. Mechanics explain a verb; they do not become a +constant texture over the character. -`AudioManager` (autoload, `globals/audio_manager.gd`) provides the plumbing -good sound design needs: +## Runtime mix -- **Bus layout**: `Master ← SFX ← {Weapons, Footsteps, UI}`, `Master ← Music` - — created automatically at boot, so volume sliders and effects (EQ, - compression, reverb zones) can target categories. -- **Variation playback**: drop `footstep_01.wav`, `footstep_02.wav`, - `footstep_03.wav` into `assets/sounds/` and `AudioManager.play_3d("footstep", pos)` - picks one at random. -- **Pitch randomization**: every playback is pitched ±6% by default — the - single cheapest trick to stop repeated sounds sounding fake. -- **Pooled 3D players**: `play_3d()` is safe to call every frame; no node - churn, no cut-off management. +`AudioManager` owns registration, aliases, variation, routing and pooled +playback. The hierarchy is: -```gdscript -# anywhere in gameplay code: -AudioManager.play_3d("ak47_fire", muzzle.global_position) -AudioManager.play_ui("hit_confirm") +```text +Master (hard limiter) +├── SFX +│ ├── Weapons (light parallel compression) +│ ├── Explosions (slower body compression) +│ ├── Impacts +│ ├── Footsteps +│ ├── Movement +│ ├── Ambience +│ ├── UI +│ └── Wind +└── Music ``` -## Replacing the procedural sounds (priority order) +The limiter is only a safety net for coincident shots/explosions. Assets are +mastered below full scale, and category gain establishes the hierarchy before +the limiter is reached. -Sound is ~50% of "game feel" in an FPS. Replace in this order: +Legacy event ids such as `m4_fire`, `explosion`, `wind` and `ui_hover` resolve +to premium families in `globals/audio_manager.gd`. This preserves gameplay API +compatibility while keeping source filenames out of weapon and UI code. -1. **Weapon fire** (heard constantly): layered = body (low thump) + crack - (mid transient) + tail (room reflection). Sonniss GDC packs have complete - layered gunshots. One file per weapon is fine to start; add `_01.._03` - variations later. -2. **Hit confirm + kill confirm**: short, bright, satisfying (this is the - "touchdown" pillar in the design doc). -3. **Footsteps**: 4+ variations minimum, quieter than you think, on the - `Footsteps` bus so they can be ducked while firing. -4. **Movement verbs**: slide (cloth+concrete scrape), dash (air whoosh), - wall-run loop, jump/land (land intensity scaled by fall speed). -5. **Ambience**: a quiet room tone per map kills the "dead air" feeling. -6. **UI**: menu hover/click/equip from a Kenney UI pack. +## Asset construction -Where to get files: see [ASSET_SOURCES.md](ASSET_SOURCES.md#sounds) — -Sonniss GDC bundles first, Kenney CC0 packs second, Freesound (CC0 filter) -for gaps. +Run: -## Conventions +```powershell +python tools/build_audio_assets.py +python tools/audio_audit.py +``` -- Files: `assets/sounds/[_NN].wav` — 44.1 kHz, 16-bit, mono for 3D - positional sounds, stereo only for UI/music. -- Ids are auto-registered from filenames at boot (`ak47_fire.wav` → - `"ak47_fire"`). Names containing `fire`/`reload` route to the Weapons bus, - `footstep` to Footsteps, everything else to SFX - (see `BUS_HINTS` in audio_manager.gd). -- Keep source loudness consistent: normalize weapon shots to about -6 dBFS - peak, footsteps/foley around -18 dBFS. +The builder is deterministic except for downloading the pinned CC0 menu track. +It: -## Later polish (cheap wins, in order) +- retains the transient of the CC0 firearm recordings; +- layers a low-passed body and a short, dark reflection tail; +- reduces the abrasive 3–5 kHz region and controls peaks; +- creates four concrete, metal, wood and glass footstep variations; +- constructs a 48.5-second speed-wind master plus softened scrape, wall-run and + grapple loops from recordings; +- constructs mono mechanical projectile loops for correct 3D localization; +- layers low relay/clunk UI hover/click/confirm/error/equip feedback; +- masters the CC0 menu loop to a conservative music level. -1. Distance low-pass on the Weapons bus (far gunfire sounds muffled). -2. A short duck (sidechain) on Footsteps/ambience when local weapon fires. -3. Reverb `AudioEffectReverb` per-map on SFX for indoor spaces. -4. Surface-dependent footsteps: raycast down, map material → sound id - (`footstep_metal`, `footstep_concrete`). +`tools/audio_audit.py` gates family coverage, 48 kHz format, mono/stereo intent, +clipping, DC offset, headroom, loop duration, seam continuity, spectral +flatness, high-frequency centroid and the explosion/firearm loudness hierarchy. + +## Gameplay behavior + +- Footsteps raycast the actual floor collider and use its + `acoustic_material`; brick and untagged structure map to concrete. +- Local and remote footsteps use the same surface families. Remote steps remain + positional and pass through material-aware occlusion. +- Slides switch recorded friction gestures if the floor material changes and + scale restrained pitch/gain with speed. +- Wall-run friction scales with speed and gets a restrained wood/metal + resonance shift. +- Speed wind is a narrowed, filtered field recording and fades in above 12 m/s + with a stronger velocity-pitch relationship. Its 48.5-second cycle prevents + a short gust cadence from becoming audible. There is no global noise bed. +- Jump and double-jump are compact jump-jet impulses. Dash is an equally brief + thrust-jet cue; clean air and low engine body dominate both. +- Grapple launch/latch/reel use distinct air, padded latch and restrained motor + cues. The reel stays below the character movement and never becomes a + continuous metallic foreground. +- Automatic weapon players keep eight overlapping voices; a new shot no longer + cuts the previous shot's body and tail. +- Explosion pitch and gain scale with blast radius, while reflections and + occlusion locate the source in the environment. Masters carry a measured + body advantage over firearms and a dark industrial debris layer. +- UI hover has a short cooldown so crossing dense menus does not become a buzz. + All UI feedback is built from softened relay/clunk gestures with 8 ms + transient ramps and a two-pole 2.4 kHz ceiling. +- Menu music fades in and is owned by the menu scene, so it stops cleanly when + a match begins. + +## Source and naming conventions + +Licenses and download pages are recorded in `assets/sounds/SOURCES.md`. + +- Positional one-shots and projectile loops: 48 kHz, 16-bit mono WAV. +- Player-local movement beds: 48 kHz, 16-bit stereo WAV. +- Music: 48 kHz stereo Ogg Vorbis. +- Variations: `_00.wav`, `_01.wav`, etc. +- Runtime event families, bus gains and pitch ranges live in + `AudioManager.EVENT_TUNING`. diff --git a/entities/combat_area.gd b/entities/combat_area.gd index 874333e..01cbdd3 100644 --- a/entities/combat_area.gd +++ b/entities/combat_area.gd @@ -1,6 +1,37 @@ extends Area3D class_name CombatArea +## The play volume. Leaving it starts a five second countdown with an on-screen +## warning and a reddening overlay; reaching zero kills the player. +## +## ── Every map needs one ────────────────────────────────────────────────────── +## +## This existed for a long while and only dust2 installed it, which meant that +## on every other map a player who got over a boundary wall — and this game has +## a grapple and a dash — simply fell out of the world and kept falling, with no +## warning, no death and no way back. `add_to()` exists so wiring it up is one +## line at the bottom of a level builder rather than fifteen, because the reason +## it was missing everywhere was that it was fiddly to add. +## +## Size it GENEROUSLY: the volume is the point at which a player is told they +## have left, not the edge of the level geometry. A box tight to the boundary +## walls fires the warning while someone is still standing on a legitimate +## rooftop. +static func add_to(level: Node, size: Vector3, centre: Vector3 = Vector3.ZERO, + node_name: String = "CombatArea") -> CombatArea: + if level.has_node(NodePath(node_name)): + return level.get_node(NodePath(node_name)) as CombatArea + var area := CombatArea.new() + area.name = node_name + var shape := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = size + shape.shape = box + area.add_child(shape) + area.position = centre + level.add_child(area) + return area + var out_of_bounds_players: Dictionary = {} # body -> { time_left: 5.0 } var recovery_players: Dictionary = {} # body -> { time_safe: 0.0, time_left: float } @@ -15,6 +46,12 @@ func _ready() -> void: collision_mask = 4294967295 # All layers body_exited.connect(_on_body_exited) body_entered.connect(_on_body_entered) + # Own the warning UI for the same lifetime as the combat volume. Creating it + # from body_exited races level teardown: removing the player from physics can + # emit that signal while the scene is already being freed, leaving the + # deferred overlay orphaned. + _create_ui() + _ui_layer.visible = false set_process(false) func _process(delta: float) -> void: @@ -72,11 +109,17 @@ func _on_body_exited(body: Node3D) -> void: out_of_bounds_players[body] = { "time_left": start_time } - var is_local = false + # Singleplayer goes through OfflineMultiplayerPeer, so it takes the + # branch below and works. This default covers the case with NO peer at + # all — a level opened directly, without NetworkManager having started a + # match, which is how the debug and capture tools run it. There is one + # player then and they are the local one; without this they would get + # the silent countdown and no warning. + var is_local := true if multiplayer.has_multiplayer_peer(): - if body.get_multiplayer_authority() == multiplayer.get_unique_id(): - is_local = true - + is_local = body.get_multiplayer_authority() == multiplayer.get_unique_id() + + if is_local: _is_local_player_out = true _local_player_body = body @@ -99,7 +142,9 @@ func _on_body_entered(body: Node3D) -> void: _local_player_body = null func _create_ui() -> void: - if _ui_layer: return + if _ui_layer: + _ui_layer.visible = true + return _ui_layer = CanvasLayer.new() _ui_layer.layer = 100 # Put it on top of everything @@ -126,21 +171,22 @@ func _create_ui() -> void: _ui_layer.add_child(_ui_label) - if get_tree().current_scene: - get_tree().current_scene.call_deferred("add_child", _ui_layer) + add_child(_ui_layer) func _remove_ui() -> void: if _ui_layer: - _ui_layer.queue_free() - _ui_layer = null - _ui_label = null - _ui_overlay = null + _ui_layer.visible = false func _kill_player(body: Node3D) -> void: if body: - if multiplayer.is_server(): - if body.has_method("take_damage"): - body.take_damage(9999, Vector3.ZERO, null, Vector3.ZERO) + # Damage is the server's call. With no peer there is no server to defer + # to and `is_server()` answers false, so without this a level opened + # outside a match would count down to zero and then quietly drop the + # player from its own list without ever killing them. + var authoritative := not multiplayer.has_multiplayer_peer() \ + or multiplayer.is_server() + if authoritative and body.has_method("take_damage"): + body.take_damage(9999, Vector3.ZERO, null, Vector3.ZERO) out_of_bounds_players.erase(body) if body == _local_player_body and _is_local_player_out: diff --git a/globals/acoustics.gd b/globals/acoustics.gd index 07520c5..04ebf87 100644 --- a/globals/acoustics.gd +++ b/globals/acoustics.gd @@ -35,6 +35,14 @@ const MATERIALS := { "concrete": { "trans_db": -18.0, "trans_cutoff": 450.0, "refl_db": -7.0, "refl_cutoff": 3600.0 }, "metal": { "trans_db": -11.0, "trans_cutoff": 1000.0, "refl_db": -4.0, "refl_cutoff": 6000.0 }, "glass": { "trans_db": -5.0, "trans_cutoff": 2600.0, "refl_db": -9.0, "refl_cutoff": 4200.0 }, + # Loose aggregate — track ballast, a gravel path. Transmission is close to + # concrete's, because what a ray actually passes through is the bed under + # it, but REFLECTION is the opposite: a surface of loose stone scatters + # rather than reflects, so it returns little and returns it dull. Standing + # on the ballast should sound noticeably deader than standing on the road. + "gravel": { "trans_db": -17.0, "trans_cutoff": 500.0, "refl_db": -15.0, "refl_cutoff": 1800.0 }, + # Turf and bare earth: the strongest absorbers in the set. + "grass": { "trans_db": -19.0, "trans_cutoff": 400.0, "refl_db": -18.0, "refl_cutoff": 1200.0 }, } const OPEN_CUTOFF := 20500.0 @@ -55,7 +63,19 @@ static func classify(collider: Object) -> String: return "concrete" var node := collider as Node if node.has_meta("acoustic_material"): - return node.get_meta("acoustic_material") + # Validated, not trusted. A level builder can set this meta to anything, + # and an unknown value used to reach `MATERIALS[mat]` directly and take + # the whole audio path down with it — a map tagged a slab "gravel" + # before gravel existed here, and every hitscan impact on it threw from + # inside occlusion(). A surface with an unrecognised material should + # sound like concrete, not stop the game making sound. + var tagged: String = str(node.get_meta("acoustic_material")) + if MATERIALS.has(tagged): + return tagged + push_warning("Acoustics: unknown acoustic_material '%s' on '%s'; using concrete." + % [tagged, node.name]) + node.set_meta("acoustic_material", "concrete") # cache, warn once + return "concrete" var mat := "concrete" var n := node.name.to_lower() diff --git a/globals/audio_manager.gd b/globals/audio_manager.gd index d7509c3..bbac9e5 100644 --- a/globals/audio_manager.gd +++ b/globals/audio_manager.gd @@ -19,8 +19,9 @@ extends Node ## suffixes and they are picked up as one id with random selection: ## footstep.wav, footstep_01.wav, footstep_02.wav -> id "footstep" (3 vars) ## -## Buses: Master <- SFX <- {Weapons, Footsteps, UI}, Master <- Music. -## See docs/SOUND_DESIGN.md for where to get good source audio. +## Buses: Master <- SFX <- {Weapons, Explosions, Footsteps, Movement, UI, +## Wind}, Master <- Music. The master hard limiter is a safety net, not a +## loudness tool: checked-in assets are already mastered with headroom. const SOUNDS_DIR := "res://assets/sounds" const POOL_SIZE_3D := 32 @@ -32,19 +33,115 @@ var _pool_3d: Array[AudioStreamPlayer3D] = [] var _pool_2d: Array[AudioStreamPlayer] = [] var _next_3d: int = 0 var _next_2d: int = 0 +var _last_ui_play_ms: Dictionary = {} +var _last_variant: Dictionary = {} # Default routing/tuning per id prefix (applied during auto-registration). const BUS_HINTS := { "footstep": "Footsteps", + "explosion": "Explosions", + "movement": "Movement", + "projectile": "Movement", + "ui_": "UI", + "weapon": "Weapons", "fire": "Weapons", "reload": "Weapons", } +## Old gameplay ids remain API-compatible while resolving to the remastered +## event families. Keeping aliases here avoids sound-path knowledge spreading +## back into every weapon and HUD script. +const PREMIUM_ALIASES := { + "ak47_fire": "weapon_ak47", + "m4_fire": "weapon_m4", + "mp7_fire": "weapon_mp7", + "dmr_fire": "weapon_dmr", + "awp_fire": "weapon_awp", + "shotgun_fire": "weapon_shotgun", + "nailgun_fire": "weapon_nailgun", + "plasma_fire": "weapon_plasma", + "rocket_fire": "weapon_rocket", + "swarm_fire": "weapon_swarm", + "mortar_fire": "weapon_mortar", + "knife_swing": "weapon_knife", + "shotgun_reload": "weapon_reload", + "explosion": "explosion_deep", + "bullet_impact": "impact_bullet", + "footstep": "footstep_concrete", + "wind": "movement_wind", + "slide": "movement_slide_concrete", + "wallrun": "movement_wallrun", + "dash": "movement_dash", + "jump": "movement_jump", + "double_jump": "movement_double_jump", + "vault": "movement_vault", + "land": "movement_land", + "grapple_launch": "movement_grapple_launch", + "grapple_latch": "movement_grapple_latch", + "grapple_reel": "movement_grapple_reel", + "rocket_flight": "projectile_rocket_loop", + "swarm_flight": "projectile_swarm_loop", + "hit_confirm": "ui_confirm_premium", + "kill_confirm": "ui_kill_premium", + "ui_hover": "ui_hover_premium", + "ui_click": "ui_click_premium", + "ui_confirm": "ui_confirm_premium", + "ui_error": "ui_error_premium", + "ui_equip": "ui_equip_premium", +} + +## Mix intent belongs beside event registration. Source files have controlled +## peaks; these values establish hierarchy and variation without per-call magic +## numbers. Loops opt into sample-accurate looping when loaded. +const EVENT_TUNING := { + "weapon_ak47": {"bus": "Weapons", "pitch": 0.025, "db": -3.0}, + "weapon_m4": {"bus": "Weapons", "pitch": 0.022, "db": -3.5}, + "weapon_mp7": {"bus": "Weapons", "pitch": 0.028, "db": -5.0}, + "weapon_dmr": {"bus": "Weapons", "pitch": 0.018, "db": -2.5}, + "weapon_awp": {"bus": "Weapons", "pitch": 0.015, "db": -1.5}, + "weapon_shotgun": {"bus": "Weapons", "pitch": 0.020, "db": -2.0}, + "weapon_nailgun": {"bus": "Weapons", "pitch": 0.025, "db": -4.0}, + "weapon_plasma": {"bus": "Weapons", "pitch": 0.025, "db": -4.0}, + "weapon_rocket": {"bus": "Weapons", "pitch": 0.018, "db": -3.0}, + "weapon_swarm": {"bus": "Weapons", "pitch": 0.025, "db": -4.0}, + "weapon_mortar": {"bus": "Weapons", "pitch": 0.015, "db": -2.0}, + "weapon_knife": {"bus": "Weapons", "pitch": 0.035, "db": -5.0}, + "weapon_reload": {"bus": "Weapons", "pitch": 0.018, "db": -7.0}, + "explosion_deep": {"bus": "Explosions", "pitch": 0.018, "db": 0.5}, + "impact_bullet": {"bus": "Impacts", "pitch": 0.045, "db": -5.0}, + "footstep_concrete": {"bus": "Footsteps", "pitch": 0.035, "db": -4.0}, + "footstep_metal": {"bus": "Footsteps", "pitch": 0.030, "db": -5.0}, + "footstep_wood": {"bus": "Footsteps", "pitch": 0.040, "db": -4.0}, + "footstep_glass": {"bus": "Footsteps", "pitch": 0.025, "db": -6.0}, + "movement_wind": {"bus": "Wind", "pitch": 0.0, "db": 0.0, "loop": true}, + "movement_slide_concrete": {"bus": "Movement", "pitch": 0.0, "db": 0.0, "loop": true}, + "movement_slide_metal": {"bus": "Movement", "pitch": 0.0, "db": -1.0, "loop": true}, + "movement_slide_wood": {"bus": "Movement", "pitch": 0.0, "db": 0.0, "loop": true}, + "movement_slide_glass": {"bus": "Movement", "pitch": 0.0, "db": -2.0, "loop": true}, + "movement_wallrun": {"bus": "Movement", "pitch": 0.0, "db": -1.0, "loop": true}, + "movement_grapple_reel": {"bus": "Movement", "pitch": 0.0, "db": -4.0, "loop": true}, + "movement_grapple_launch": {"bus": "Movement", "pitch": 0.010, "db": -3.0}, + "movement_grapple_latch": {"bus": "Movement", "pitch": 0.008, "db": -3.0}, + "movement_dash": {"bus": "Movement", "pitch": 0.015, "db": -2.5}, + "movement_jump": {"bus": "Movement", "pitch": 0.010, "db": -3.5}, + "movement_double_jump": {"bus": "Movement", "pitch": 0.010, "db": -3.2}, + "movement_vault": {"bus": "Movement", "pitch": 0.025, "db": -6.0}, + "movement_land": {"bus": "Movement", "pitch": 0.025, "db": -4.0}, + "projectile_rocket_loop": {"bus": "Movement", "pitch": 0.015, "db": -4.0, "loop": true}, + "projectile_swarm_loop": {"bus": "Movement", "pitch": 0.020, "db": -6.0, "loop": true}, + "ui_hover_premium": {"bus": "UI", "pitch": 0.008, "db": -5.0, "cooldown": 55}, + "ui_click_premium": {"bus": "UI", "pitch": 0.006, "db": -3.5, "cooldown": 30}, + "ui_confirm_premium": {"bus": "UI", "pitch": 0.005, "db": -3.0}, + "ui_kill_premium": {"bus": "UI", "pitch": 0.005, "db": -1.5}, + "ui_error_premium": {"bus": "UI", "pitch": 0.005, "db": -3.0}, + "ui_equip_premium": {"bus": "UI", "pitch": 0.006, "db": -3.0}, +} + ## weapon_name (as broadcast in fire RPCs) -> fire sound id. const WEAPON_SOUND_IDS := { "AK-47": "ak47_fire", "M4": "m4_fire", "MP7": "mp7_fire", "DMR": "dmr_fire", "AWP": "awp_fire", - "Double Barrel Shotgun": "shotgun_fire", "Nail Gun": "nailgun_fire", + "Double Barrel": "shotgun_fire", "Nail Gun": "nailgun_fire", "Plasma Gun": "plasma_fire", "Rocket Launcher": "rocket_fire", "Rocket Swarm": "swarm_fire", "Mortar": "mortar_fire", "Knife": "knife_swing", @@ -94,18 +191,65 @@ func weapon_sound_id(weapon_name: String) -> String: func _setup_buses() -> void: _ensure_bus("SFX", "Master") _ensure_bus("Weapons", "SFX") + _ensure_bus("Explosions", "SFX") + _ensure_bus("Impacts", "SFX") _ensure_bus("Footsteps", "SFX") + _ensure_bus("Movement", "SFX") + _ensure_bus("Ambience", "SFX") _ensure_bus("UI", "SFX") _ensure_bus("Music", "Master") + _ensure_bus("Wind", "SFX") + _install_mix_effects() func _ensure_bus(bus_name: String, send_to: String) -> void: - if AudioServer.get_bus_index(bus_name) != -1: - return - var idx := AudioServer.bus_count - AudioServer.add_bus(idx) - AudioServer.set_bus_name(idx, bus_name) - AudioServer.set_bus_send(idx, send_to) + var idx := AudioServer.get_bus_index(bus_name) + if idx == -1: + idx = AudioServer.bus_count + AudioServer.add_bus(idx) + AudioServer.set_bus_name(idx, bus_name) + if bus_name != "Master": + AudioServer.set_bus_send(idx, send_to) + + +func _install_mix_effects() -> void: + var master := AudioServer.get_bus_index("Master") + if master >= 0 and not _bus_has_effect(master, "AudioEffectHardLimiter"): + var limiter := AudioEffectHardLimiter.new() + limiter.ceiling_db = -1.0 + limiter.pre_gain_db = 0.0 + limiter.release = 0.10 + AudioServer.add_bus_effect(master, limiter) + + var weapons := AudioServer.get_bus_index("Weapons") + if weapons >= 0 and not _bus_has_effect(weapons, "AudioEffectCompressor"): + var punch := AudioEffectCompressor.new() + punch.threshold = -9.0 + punch.ratio = 2.0 + punch.attack_us = 1800.0 + punch.release_ms = 95.0 + punch.gain = 0.5 + punch.mix = 0.42 + AudioServer.add_bus_effect(weapons, punch) + + var explosions := AudioServer.get_bus_index("Explosions") + if explosions >= 0 and not _bus_has_effect(explosions, "AudioEffectCompressor"): + var depth := AudioEffectCompressor.new() + depth.threshold = -10.0 + depth.ratio = 1.8 + depth.attack_us = 2000.0 + depth.release_ms = 240.0 + depth.gain = 0.5 + depth.mix = 0.35 + AudioServer.add_bus_effect(explosions, depth) + + +func _bus_has_effect(bus: int, effect_class: String) -> bool: + for i in AudioServer.get_bus_effect_count(bus): + var effect := AudioServer.get_bus_effect(bus, i) + if effect and effect.is_class(effect_class): + return true + return false func set_bus_volume_linear(bus_name: String, linear: float) -> void: @@ -158,11 +302,20 @@ func _auto_register_sounds() -> void: if id.findn(hint) != -1: bus = BUS_HINTS[hint] break - register_sound(id, groups[id], bus) + var tuning: Dictionary = EVENT_TUNING.get(id, {}) + register_sound(id, groups[id], + str(tuning.get("bus", bus)), + float(tuning.get("pitch", 0.045)), + float(tuning.get("db", 0.0))) func has_sound(id: String) -> bool: - return _library.has(id) + return _library.has(_resolve_id(id)) + + +func _resolve_id(id: String) -> String: + var premium: String = str(PREMIUM_ALIASES.get(id, id)) + return premium if _library.has(premium) else id ## Build an AudioStreamRandomizer for a registered id: every play() picks a @@ -170,16 +323,67 @@ func has_sound(id: String) -> bool: ## (weapons) instead of going through the pools. Falls back to a plain load ## of fallback_path if the id isn't registered. func stream_for(id: String, fallback_path: String = "") -> AudioStream: - var entry = _library.get(id) + var resolved := _resolve_id(id) + var entry = _library.get(resolved) if entry == null: return load(fallback_path) if fallback_path != "" and ResourceLoader.exists(fallback_path) else null var r := AudioStreamRandomizer.new() r.random_pitch = 1.0 + entry["pitch_var"] for s in entry["streams"]: + if bool(EVENT_TUNING.get(resolved, {}).get("loop", false)): + _enable_loop(s) r.add_stream(-1, s) return r +## Configure an owned AudioStreamPlayer/AudioStreamPlayer3D from the same event +## metadata used by pooled playback. This erases the old per-weapon gain/bus +## lottery while letting weapons keep their existing player nodes. +func configure_player(player: Node, id: String, fallback_path: String = "", + volume_db_offset: float = 0.0) -> void: + if player == null: + return + var resolved := _resolve_id(id) + var entry = _library.get(resolved) + player.set("stream", stream_for(id, fallback_path)) + if entry: + player.set("bus", entry["bus"]) + player.set("volume_db", float(entry["volume_db"]) + volume_db_offset) + if player is AudioStreamPlayer3D: + var spatial := player as AudioStreamPlayer3D + spatial.unit_size = 7.0 + spatial.max_distance = 85.0 + # Automatic fire must not amputate the body/tail of the previous shot. + # Godot defaults to one voice per player, which was making every rifle + # turn into a dry click train under sustained fire. + spatial.max_polyphony = 8 if resolved.begins_with("weapon_") \ + and resolved != "weapon_reload" else 1 + elif player is AudioStreamPlayer: + (player as AudioStreamPlayer).max_polyphony = 4 + + +func _enable_loop(stream: AudioStream) -> void: + if stream is AudioStreamWAV: + var wav := stream as AudioStreamWAV + wav.loop_mode = AudioStreamWAV.LOOP_FORWARD + wav.loop_begin = 0 + wav.loop_end = maxi(1, roundi(wav.get_length() * wav.mix_rate)) + elif stream is AudioStreamOggVorbis: + (stream as AudioStreamOggVorbis).loop = true + + +func _pick_stream(id: String, entry: Dictionary) -> AudioStream: + var streams: Array = entry["streams"] + if streams.size() <= 1: + return streams[0] + var previous := int(_last_variant.get(id, -1)) + var pick := randi_range(0, streams.size() - 2) + if pick >= previous: + pick += 1 + _last_variant[id] = pick + return streams[pick] + + # ── Playback ────────────────────────────────────────────────────────────────── func _build_pools() -> void: @@ -202,12 +406,13 @@ func _build_pools() -> void: ## Safe to call every frame — players are pooled. func play_3d(id: String, global_pos: Vector3, volume_db_offset: float = 0.0, pitch_scale: float = 1.0) -> AudioStreamPlayer3D: - var entry = _library.get(id) + var resolved := _resolve_id(id) + var entry = _library.get(resolved) if entry == null: return null var player := _pool_3d[_next_3d] _next_3d = (_next_3d + 1) % POOL_SIZE_3D - player.stream = entry["streams"].pick_random() + player.stream = _pick_stream(resolved, entry) player.bus = entry["bus"] player.volume_db = entry["volume_db"] + volume_db_offset player.pitch_scale = pitch_scale * randf_range(1.0 - entry["pitch_var"], 1.0 + entry["pitch_var"]) @@ -217,7 +422,7 @@ func play_3d(id: String, global_pos: Vector3, volume_db_offset: float = 0.0, if cam: var occ := Acoustics.occlusion(cam.get_world_3d(), cam.global_position, global_pos) Acoustics.apply_occlusion(player, occ) - if _is_loud(id): + if _is_loud(resolved): _spawn_reflections(player.stream, global_pos, cam, entry, volume_db_offset) else: Acoustics.apply_occlusion(player, { "db": 0.0, "cutoff": Acoustics.OPEN_CUTOFF, "surfaces": 0 }) @@ -226,7 +431,7 @@ func play_3d(id: String, global_pos: Vector3, volume_db_offset: float = 0.0, func _is_loud(id: String) -> bool: - return id.findn("fire") != -1 or id.findn("explosion") != -1 + return id.begins_with("weapon_") or id.begins_with("explosion") ## Delayed, filtered copies of a loud sound from nearby wall mirror points. @@ -249,16 +454,32 @@ func _spawn_reflections(stream: AudioStream, source: Vector3, cam: Camera3D, ) -## Play a non-positional sound (UI clicks, local confirms). -func play_ui(id: String, volume_db_offset: float = 0.0) -> AudioStreamPlayer: - var entry = _library.get(id) +## Play a non-positional event on its authored bus. Local movement feedback +## uses this too; play_ui adds hover/click rate limiting on top. +func play_local(id: String, volume_db_offset: float = 0.0, + pitch_scale: float = 1.0) -> AudioStreamPlayer: + var resolved := _resolve_id(id) + var entry = _library.get(resolved) if entry == null: return null var player := _pool_2d[_next_2d] _next_2d = (_next_2d + 1) % POOL_SIZE_2D - player.stream = entry["streams"].pick_random() - player.bus = "UI" + player.stream = _pick_stream(resolved, entry) + player.bus = entry["bus"] player.volume_db = entry["volume_db"] + volume_db_offset - player.pitch_scale = randf_range(1.0 - entry["pitch_var"], 1.0 + entry["pitch_var"]) + player.pitch_scale = pitch_scale * randf_range( + 1.0 - entry["pitch_var"], 1.0 + entry["pitch_var"]) player.play() return player + + +## Play a non-positional UI sound. A small per-event cooldown stops a pointer +## sweeping across a dense menu from turning gentle hovers into a grating buzz. +func play_ui(id: String, volume_db_offset: float = 0.0) -> AudioStreamPlayer: + var resolved := _resolve_id(id) + var cooldown := int(EVENT_TUNING.get(resolved, {}).get("cooldown", 0)) + var now := Time.get_ticks_msec() + if cooldown > 0 and now - int(_last_ui_play_ms.get(resolved, -cooldown)) < cooldown: + return null + _last_ui_play_ms[resolved] = now + return play_local(id, volume_db_offset) diff --git a/movement/movement_params.gd b/movement/movement_params.gd index 5a96942..e4bd008 100644 --- a/movement/movement_params.gd +++ b/movement/movement_params.gd @@ -12,7 +12,10 @@ class_name MovementParams @export var jump_velocity: float = 8.5 @export var coyote_time: float = 0.15 @export var jump_buffer: float = 0.12 -@export var air_control: float = 0.35 +# Retuned to half its old 0.35 (2026-08-01). Air control is how much of the +# player's input authority survives leaving the ground; at 0.35 a jump could be +# steered almost like a run, which flattened the cost of committing to a leap. +@export var air_control: float = 0.175 @export var air_acceleration: float = 30.0 @export var max_air_speed: float = 18.0 @export var gravity: float = 22.0 @@ -41,7 +44,11 @@ class_name MovementParams @export var slide_min_speed: float = 4.0 @export var slide_cooldown: float = 0.35 # prevents rapid slide re-entry @export var slide_steer_rate: float = 2.5 # how fast slide direction bends toward input -@export var slide_jump_speed_boost: float = 1.5 # extra m/s added when jumping out of slide +# Retuned to a tenth of its old 1.5 (2026-08-01). Jumping out of a slide still +# PRESERVES the slide's speed — that is `out_speed` in state_slide, and it is +# untouched — this is only the bonus stacked on top of it, which at 1.5 m/s a +# chain could compound into the bunny-hop cap almost for free. +@export var slide_jump_speed_boost: float = 0.15 # extra m/s added when jumping out of slide # ── Wall Run ────────────────────────────────────────────────────────────────── @export var wall_run_speed: float = 13.0 @@ -83,7 +90,10 @@ class_name MovementParams @export var grapple_shoot_speed: float = 70.0 # hook travel speed (m/s) # ── Dash ────────────────────────────────────────────────────────────────────── -@export var dash_speed: float = 13.0 +# Retuned to three quarters of its old 13.0 (2026-08-01). This is the speed a +# dash ADDS: state_dash projects the current velocity onto the dash direction +# and adds this on top, so a forward dash at full run used to gain a flat 13 m/s. +@export var dash_speed: float = 9.75 @export var dash_duration: float = 0.18 @export var dash_charges: int = 2 # dashes available before recharging @export var dash_cooldown: float = 2.0 # seconds to regain ONE charge diff --git a/movement/player_movement_controller.gd b/movement/player_movement_controller.gd index 34d7215..c65ffdd 100644 --- a/movement/player_movement_controller.gd +++ b/movement/player_movement_controller.gd @@ -39,6 +39,7 @@ var footstep_player: AudioStreamPlayer var footstep_streams: Array = [] var land_player: AudioStreamPlayer var _remote_footstep_timer: float = 0.0 +var _slide_surface_event: String = "" # Networked one-shot animation actions (reload/throw): the owner bumps the # sequence; remote peers play the named action when it changes. @@ -50,6 +51,7 @@ var _was_reloading: bool = false ## own third-person model (remote shooters go through server_play_fire_effects). var _last_local_ammo: int = -1 var dash_player: AudioStreamPlayer +var _jet_vfx: PlayerJetVFX = null var recent_attackers: Dictionary = {} # attacker_id: timestamp var hit_player: AudioStreamPlayer var slide_player: AudioStreamPlayer @@ -146,6 +148,7 @@ func _ready() -> void: camera.add_child(flashlight) _setup_audio() + _setup_jet_vfx() _setup_grapple() _setup_grenade_trajectory() @@ -215,7 +218,13 @@ func set_third_person(on: bool) -> void: return third_person = on if is_instance_valid(_tp_camera): - _tp_camera.fov = SettingsManager.world_fov # stay in sync with settings + # Tool-script regression runners compile this controller before autoload + # singletons register, so a direct `SettingsManager` identifier makes the + # whole movement state machine fail to compile. Resolve the same singleton + # at runtime; level scenes and `-s` tests now share one safe path. + var settings = get_tree().root.get_node_or_null("SettingsManager") + if settings: + _tp_camera.fov = settings.world_fov _tp_camera.current = on if is_instance_valid(camera): camera.current = not on @@ -223,6 +232,8 @@ func set_third_person(on: bool) -> void: var visual = get_visual_model() if visual and visual.has_method("set_owner_visible"): visual.set_owner_visible(on) + if is_instance_valid(_jet_vfx): + _jet_vfx.set_render_enabled(on) # Hide the first-person weapon viewmodel in third person (it would float in # the middle of the screen); the third-person weapon on the model shows. if is_instance_valid(camera): @@ -260,6 +271,21 @@ func get_visual_model() -> Node3D: return skinned_model return get_node_or_null("HumanoidModel") + +func _setup_jet_vfx() -> void: + _jet_vfx = PlayerJetVFX.new() + _jet_vfx.name = "JetVFX" + add_child(_jet_vfx) + _jet_vfx.set_render_enabled(not is_multiplayer_authority() or third_person) + + +func _update_jet_vfx(state: String, move_dir: Vector2) -> void: + if not is_instance_valid(_jet_vfx): + return + _jet_vfx.set_visual_model(get_visual_model()) + _jet_vfx.set_render_enabled(not is_multiplayer_authority() or third_person) + _jet_vfx.set_dash(state == "dash", move_dir) + ## Swap the visual model to match a skin id. GLB skins replace the procedural ## model entirely; color skins tint the procedural model. func _apply_skin_model(skin_id: String) -> void: @@ -306,88 +332,150 @@ func _hide_remote_weapons() -> void: func _setup_audio() -> void: footstep_player = AudioStreamPlayer.new() - footstep_player.bus = "SFX" - for path in ["res://assets/sounds/footstep.wav", "res://assets/sounds/footstep_01.wav", - "res://assets/sounds/footstep_02.wav", "res://assets/sounds/footstep_03.wav", - "res://assets/sounds/footstep_04.wav"]: - if ResourceLoader.exists(path): - footstep_streams.append(load(path)) - footstep_player.stream = footstep_streams[0] if not footstep_streams.is_empty() else null + _configure_audio_player(footstep_player, "footstep_concrete") add_child(footstep_player) land_player = AudioStreamPlayer.new() - land_player.bus = "SFX" - land_player.stream = load("res://assets/sounds/land.wav") + _configure_audio_player(land_player, "land") add_child(land_player) jump_player = AudioStreamPlayer.new() - jump_player.bus = "SFX" - jump_player.stream = load("res://assets/sounds/jump.wav") + _configure_audio_player(jump_player, "jump") add_child(jump_player) double_jump_player = AudioStreamPlayer.new() - double_jump_player.bus = "SFX" - jump_player.bus = "SFX" - double_jump_player.stream = load("res://assets/sounds/double_jump.wav") + _configure_audio_player(double_jump_player, "double_jump") add_child(double_jump_player) vault_player = AudioStreamPlayer.new() - vault_player.bus = "SFX" - vault_player.stream = load("res://assets/sounds/vault.wav") + _configure_audio_player(vault_player, "vault") add_child(vault_player) dash_player = AudioStreamPlayer.new() - dash_player.bus = "SFX" - dash_player.stream = load("res://assets/sounds/dash.wav") - dash_player.volume_db = -5.0 + _configure_audio_player(dash_player, "dash") add_child(dash_player) hit_player = AudioStreamPlayer.new() - hit_player.bus = "SFX" - hit_player.stream = load("res://assets/sounds/hit_confirm.wav") + _configure_audio_player(hit_player, "hit_confirm") hit_player.max_polyphony = 8 add_child(hit_player) slide_player = AudioStreamPlayer.new() - slide_player.bus = "SFX" - slide_player.stream = load("res://assets/sounds/slide.wav") - slide_player.volume_db = -10.0 + _configure_audio_player(slide_player, "movement_slide_concrete") add_child(slide_player) wallrun_player = AudioStreamPlayer.new() - wallrun_player.bus = "SFX" - wallrun_player.stream = load("res://assets/sounds/wallrun.wav") - wallrun_player.volume_db = -10.0 + _configure_audio_player(wallrun_player, "wallrun") add_child(wallrun_player) wind_player = AudioStreamPlayer.new() - wind_player.bus = "Wind" - wind_player.stream = load("res://assets/sounds/wind.wav") + _configure_audio_player(wind_player, "wind") wind_player.volume_db = -80.0 # Start silent add_child(wind_player) wind_player.play() grapple_shoot_player = AudioStreamPlayer.new() - grapple_shoot_player.bus = "SFX" - grapple_shoot_player.stream = load("res://assets/sounds/dash.wav") - grapple_shoot_player.volume_db = -5.0 - grapple_shoot_player.pitch_scale = 1.5 + _configure_audio_player(grapple_shoot_player, "grapple_launch") add_child(grapple_shoot_player) grapple_latch_player = AudioStreamPlayer.new() - grapple_latch_player.bus = "SFX" - grapple_latch_player.stream = load("res://assets/sounds/hit_confirm.wav") - grapple_latch_player.pitch_scale = 0.8 + _configure_audio_player(grapple_latch_player, "grapple_latch") add_child(grapple_latch_player) grapple_swing_player = AudioStreamPlayer.new() - grapple_swing_player.bus = "SFX" - grapple_swing_player.stream = load("res://assets/sounds/wind.wav") - grapple_swing_player.pitch_scale = 1.5 + _configure_audio_player(grapple_swing_player, "grapple_reel") grapple_swing_player.volume_db = -80.0 add_child(grapple_swing_player) grapple_swing_player.play() + +func _configure_audio_player(player: Node, event: String, + fallback_path: String = "", volume_db_offset: float = 0.0) -> void: + # See set_third_person(): direct autoload identifiers are unavailable while + # `-s` regression scripts compile this controller's dependency graph. + var audio_manager = get_tree().root.get_node_or_null("AudioManager") + if audio_manager: + audio_manager.configure_player( + player, event, fallback_path, volume_db_offset) + + +## Material under/alongside the controller, using the same collider metadata as +## gunshot occlusion. Builders already identify wood/metal/glass/brick, so +## movement audio stays correct without map-specific node lists. +func acoustic_surface(direction: Vector3 = Vector3.DOWN, + distance: float = 1.8) -> String: + if not is_inside_tree() or direction.length_squared() < 0.001: + return "concrete" + var start := global_position + Vector3.UP * 0.12 + var query := PhysicsRayQueryParameters3D.create( + start, start + direction.normalized() * distance, 1) + query.exclude = [get_rid()] + var hit := get_world_3d().direct_space_state.intersect_ray(query) + if hit.is_empty(): + return "concrete" + return Acoustics.classify(hit.collider) + + +func _surface_family(material: String) -> String: + match material: + "metal": + return "metal" + "wood": + return "wood" + "glass": + return "glass" + _: + # Brick, stone, sand and untagged structural surfaces all use the + # concrete family; each still varies sample and pitch. + return "concrete" + + +func surface_event(prefix: String, direction: Vector3 = Vector3.DOWN, + distance: float = 1.8) -> String: + return "%s_%s" % [prefix, _surface_family( + acoustic_surface(direction, distance))] + + +func play_footstep(speed: float) -> void: + if footstep_player == null: + return + var event := surface_event("footstep") + var speed_mix := clampf((speed - 1.0) / 10.0, 0.0, 1.0) + _configure_audio_player(footstep_player, event, "", + lerpf(-3.0, 0.5, speed_mix)) + footstep_player.pitch_scale = lerpf(0.96, 1.045, speed_mix) \ + * randf_range(0.985, 1.015) + footstep_player.play() + + +func update_slide_audio(speed: float) -> void: + if slide_player == null: + return + var event := surface_event("movement_slide") + if event != _slide_surface_event: + var was_playing := slide_player.playing + _configure_audio_player(slide_player, event) + _slide_surface_event = event + if was_playing: + slide_player.play() + var intensity := clampf((speed - 4.0) / 16.0, 0.0, 1.0) + slide_player.volume_db = lerpf(-24.0, -13.0, intensity) + slide_player.pitch_scale = lerpf(0.94, 1.02, intensity) + + +func update_wallrun_audio(speed: float, wall_normal: Vector3) -> void: + if wallrun_player == null: + return + var intensity := clampf((speed - 4.0) / 16.0, 0.0, 1.0) + # The same warm friction bed works on every wall; material changes its + # resonance subtly without ever turning metal walls into a piercing whine. + var material := acoustic_surface(-wall_normal, 1.3) + var material_pitch := 1.035 if material == "metal" else ( + 0.94 if material == "wood" else 1.0) + wallrun_player.volume_db = lerpf(-24.0, -13.0, intensity) + wallrun_player.pitch_scale = lerpf(0.95, 1.02, intensity) * material_pitch + + func _setup_speedlines() -> void: _speedlines = ColorRect.new() _speedlines.name = "SpeedLines" @@ -646,7 +734,8 @@ func server_play_explosion(pos: Vector3, radius: float) -> void: func rpc_play_explosion(pos: Vector3, radius: float) -> void: if is_multiplayer_authority(): return - ExplosionVFX.spawn(get_tree(), pos, radius) + var pitch := clampf(1.06 - (radius - 2.5) * 0.035, 0.76, 1.12) + ExplosionVFX.spawn(get_tree(), pos, radius, pitch) @rpc("any_peer", "call_local", "reliable") func server_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_name: String, impulse: Vector3) -> void: @@ -900,8 +989,12 @@ func _physics_process(_delta: float) -> void: machine.input_grapple_just_pressed = input_grapple_just var speed = velocity.length() - var wind_factor = clampf((speed - 10.0) / 25.0, 0.0, 1.0) - wind_player.volume_db = lerpf(-40.0, -15.0, wind_factor) + var wind_factor = clampf((speed - 12.0) / 24.0, 0.0, 1.0) + var wind_target := -70.0 if wind_factor <= 0.001 \ + else lerpf(-42.0, -15.0, pow(wind_factor, 0.82)) + wind_player.volume_db = lerpf(wind_player.volume_db, wind_target, + 1.0 - exp(-4.5 * _delta)) + wind_player.pitch_scale = lerpf(1.0, 1.24, wind_factor) if not wind_player.playing and wind_factor > 0.0: wind_player.play() @@ -927,7 +1020,13 @@ func _physics_process(_delta: float) -> void: # Update grapple rope visuals (runs on all clients for smooth interpolation) if (synced_movement_state == "grapple" or synced_is_grapple_shooting) and camera: grapple_rope.visible = true - var start_pos = camera.global_position + camera.global_transform.basis * Vector3(0.3, -0.3, -0.15) + var start_pos = camera.global_position \ + + camera.global_transform.basis * Vector3(0.3, -0.3, -0.15) + var grapple_visual := get_visual_model() + if synced_movement_state == "grapple" and grapple_visual \ + and grapple_visual.has_method( + "get_grapple_hand_world_position"): + start_pos = grapple_visual.get_grapple_hand_world_position() var end_pos = synced_grapple_point var dist = start_pos.distance_to(end_pos) if dist > 0.01: @@ -943,9 +1042,13 @@ func _physics_process(_delta: float) -> void: grapple_rope.visible = false if synced_movement_state == "grapple" and velocity.length() > 5.0: - grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -15.0, _delta * 10.0) + var grapple_factor := clampf((velocity.length() - 5.0) / 18.0, 0.0, 1.0) + grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, + lerpf(-28.0, -15.0, grapple_factor), 1.0 - exp(-8.0 * _delta)) + grapple_swing_player.pitch_scale = lerpf(0.92, 1.08, grapple_factor) else: - grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -80.0, _delta * 15.0) + grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, + -80.0, 1.0 - exp(-12.0 * _delta)) # Local player's aim-down-sights state (drives the model's weapon raise). synced_is_ads = _read_ads() @@ -954,16 +1057,21 @@ func _physics_process(_delta: float) -> void: var visual = get_visual_model() if visual: var h_speed = Vector2(velocity.x, velocity.z).length() - visual.update_state(sm.current_state, h_speed, sm.input_crouch) if visual.has_method("set_locomotion"): var d := _local_move_dir() visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0) + # Direction must be present before update_state drives the BlendSpace; + # otherwise the first movement frame still samples last frame's Idle. if visual.has_method("set_wall_side"): visual.set_wall_side(sm.wall_side) + if visual.has_method("set_wall_run_motion"): + visual.set_wall_run_motion(velocity) + visual.update_state(sm.current_state, h_speed, sm.input_crouch) if visual.has_method("set_dancing"): visual.set_dancing(synced_is_dancing, synced_dance_index) if visual.has_method("set_grapple_target") and sm.current_state == "grapple": - visual.set_grapple_target(synced_grapple_point) + visual.set_grapple_target(synced_grapple_point, velocity) + _update_jet_vfx(sm.current_state, _local_move_dir()) # Publish state for remote peers synced_movement_state = sm.current_state @@ -1005,6 +1113,10 @@ func _on_movement_event(ev: String, data: Dictionary) -> void: grapple_latch_player.play() elif ev == "dash": _speedline_burst = 1.0 + elif ev == "double_jump": + _trigger_action("double_jump") + if is_instance_valid(_jet_vfx): + _jet_vfx.burst_double_jump() elif ev == "land": if land_player: var heavy: bool = data.get("heavy", false) @@ -1039,16 +1151,24 @@ func _process(delta: float) -> void: var visual = get_visual_model() if visual: - visual.update_state(synced_movement_state, synced_movement_speed, synced_is_crouching) if visual.has_method("set_locomotion"): var d := _local_move_dir() visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0) if visual.has_method("set_wall_side"): visual.set_wall_side(synced_wall_side) + if visual.has_method("set_wall_run_motion"): + visual.set_wall_run_motion(synced_velocity) + visual.update_state( + synced_movement_state, + synced_movement_speed, + synced_is_crouching, + ) + _update_jet_vfx(synced_movement_state, _local_move_dir()) if visual.has_method("set_dancing"): visual.set_dancing(synced_is_dancing, synced_dance_index) if visual.has_method("set_grapple_target") and synced_movement_state == "grapple": - visual.set_grapple_target(synced_grapple_point) + visual.set_grapple_target( + synced_grapple_point, synced_velocity) # Upper body follows the owner's synced camera pitch if visual.has_method("set_aim_pitch") and head_pivot: visual.set_aim_pitch(head_pivot.rotation.x) @@ -1056,6 +1176,8 @@ func _process(delta: float) -> void: if visual.has_method("play_action") and synced_action_seq != _last_action_seq: _last_action_seq = synced_action_seq visual.play_action(synced_action) + if synced_action == "double_jump" and is_instance_valid(_jet_vfx): + _jet_vfx.burst_double_jump() # Check for weapon changes if synced_weapon_path != "" and synced_weapon_path != visual.get_meta("current_weapon_path", ""): visual.set_weapon(synced_weapon_path) @@ -1068,8 +1190,11 @@ func _process(delta: float) -> void: _remote_footstep_timer -= delta if _remote_footstep_timer <= 0.0: var am = get_tree().root.get_node_or_null("AudioManager") - if am and am.has_sound("footstep"): - am.play_3d("footstep", global_position + Vector3(0, -0.8, 0), -8.0) + var event := surface_event("footstep") + if am and am.has_sound(event): + am.play_3d(event, global_position + Vector3(0, -0.8, 0), + -4.0, lerpf(0.96, 1.04, + clampf(synced_movement_speed / 14.0, 0.0, 1.0))) _remote_footstep_timer = maxf(0.2, 3.0 / synced_movement_speed) else: _remote_footstep_timer = 0.0 diff --git a/movement/states/state_air.gd b/movement/states/state_air.gd index 233547e..6127959 100644 --- a/movement/states/state_air.gd +++ b/movement/states/state_air.gd @@ -104,6 +104,7 @@ func update(delta: float) -> void: machine.jump_cooldown_timer = params.jump_cooldown machine.current_jump_count += 1 machine.register_chain_mechanic("double_jump") + machine.movement_event.emit("double_jump", {}) if player.double_jump_player: player.double_jump_player.play() return diff --git a/movement/states/state_ground.gd b/movement/states/state_ground.gd index 2b1055b..3d28426 100644 --- a/movement/states/state_ground.gd +++ b/movement/states/state_ground.gd @@ -81,13 +81,8 @@ func update(delta: float) -> void: if current_hspeed > 1.0 and player.is_on_floor(): _footstep_timer -= delta if _footstep_timer <= 0.0: - if player.footstep_player: - # Random sample + slight pitch variation so steps don't machine-gun. - if "footstep_streams" in player and not player.footstep_streams.is_empty(): - player.footstep_player.stream = player.footstep_streams.pick_random() - player.footstep_player.pitch_scale = randf_range(0.92, 1.08) - player.footstep_player.volume_db = -6.0 - player.footstep_player.play() + if player.has_method("play_footstep"): + player.play_footstep(current_hspeed) _footstep_timer = max(0.2, 3.0 / current_hspeed) else: _footstep_timer = 0.0 diff --git a/movement/states/state_slide.gd b/movement/states/state_slide.gd index b392232..ce4002b 100644 --- a/movement/states/state_slide.gd +++ b/movement/states/state_slide.gd @@ -23,6 +23,8 @@ func enter(_data: Dictionary = {}) -> void: # Slide in the direction of current velocity (momentum-based) var hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z) + if machine.player.has_method("update_slide_audio"): + machine.player.update_slide_audio(hvel.length()) if hvel.length_squared() > 0.01: _slide_direction = hvel.normalized() else: @@ -57,6 +59,8 @@ func update(delta: float) -> void: var vel: Vector3 = player.velocity var hvel := Vector3(vel.x, 0.0, vel.z) var hspeed := hvel.length() + if player.has_method("update_slide_audio"): + player.update_slide_audio(hspeed) # ── Slope physics: gravity projected along the floor plane ──────────── var on_slope := false diff --git a/movement/states/state_wall_run.gd b/movement/states/state_wall_run.gd index 2eafbfb..1ad3fac 100644 --- a/movement/states/state_wall_run.gd +++ b/movement/states/state_wall_run.gd @@ -27,6 +27,8 @@ func enter(_data: Dictionary = {}) -> void: machine.player.wallrun_player.play() var hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z) + if machine.player.has_method("update_wallrun_audio"): + machine.player.update_wallrun_audio(hvel.length(), machine.wall_normal) _current_tangent = machine.wall_normal.cross(Vector3.UP).normalized() if hvel.dot(_current_tangent) < 0.0: _current_tangent = -_current_tangent @@ -71,6 +73,8 @@ func update(delta: float) -> void: var hvel := Vector3(vel.x, 0.0, vel.z) var along := hvel.dot(wall_tangent) + if player.has_method("update_wallrun_audio"): + player.update_wallrun_audio(hvel.length(), machine.wall_normal) var target_speed: float = maxf(machine.get_effective_speed(params.wall_run_speed), along) along = move_toward(along, target_speed, params.wall_run_accel * delta) # Redirect all horizontal velocity along the wall (kills the into-wall part) diff --git a/movement/tests/test_fsm_runner.gd b/movement/tests/test_fsm_runner.gd index c3adc37..37ed294 100644 --- a/movement/tests/test_fsm_runner.gd +++ b/movement/tests/test_fsm_runner.gd @@ -33,6 +33,7 @@ func run_all() -> void: var tests := [ "test_states_register_and_initialize", "test_do_jump_sets_velocity_and_counters", + "test_double_jump_emits_jet_event", "test_wall_run_preserves_fast_entry_speed", "test_wall_run_entry_keeps_some_upward_momentum", "test_dash_cooldown_is_per_machine", @@ -127,6 +128,20 @@ func test_do_jump_sets_velocity_and_counters() -> void: _expect(not sm.on_ground, "no longer grounded") +func test_double_jump_emits_jet_event() -> void: + var events := [] + sm.movement_event.connect(func(ev, data): events.append([ev, data])) + sm.switch_to("air") + sm.current_jump_count = 1 + sm.jump_cooldown_timer = 0.0 + sm.input_jump_just_pressed = true + sm.states["air"].update(0.016) + _expect(fake_player.velocity.y == params.double_jump_velocity, + "double jump sets its own vertical impulse") + _expect(events.any(func(item): return item[0] == "double_jump"), + "double jump emits the replicated jet event") + + func test_wall_run_preserves_fast_entry_speed() -> void: # Enter a wall run at 20 m/s along the wall; speed must not drop to base. sm.wall_normal = Vector3.RIGHT diff --git a/scenes/maps/cloud_sun_sync.gd b/scenes/maps/cloud_sun_sync.gd new file mode 100644 index 0000000..64c9fe8 --- /dev/null +++ b/scenes/maps/cloud_sun_sync.gd @@ -0,0 +1,43 @@ +extends Node +class_name CloudSunSync + +## Keeps the fullscreen sky composite tied to the real DirectionalLight3D. +## +## The Environment sky shader receives LIGHT0_DIRECTION automatically, but the +## ink pass has to repaint background pixels after the screen copy. This bridge +## gives that second shader the same moving direction and color every frame. + +var _sun: DirectionalLight3D +var _ink_material: ShaderMaterial +var _palette_sun_color := Color.WHITE + + +func configure(sun: DirectionalLight3D, ink_material: ShaderMaterial) -> void: + _sun = sun + _ink_material = ink_material + var authored = ink_material.get_shader_parameter("sky_sun_color") + if authored is Color: + _palette_sun_color = authored + _sync() + + +func _process(_delta: float) -> void: + _sync() + + +func _sync() -> void: + if not is_instance_valid(_sun) or not is_instance_valid(_ink_material): + set_process(false) + return + + # DirectionalLight3D shines down its local -Z axis, so +Z points back + # toward the apparent sun—the direction a sky pixel uses for illumination. + var toward_sun := _sun.global_transform.basis.z.normalized() + _ink_material.set_shader_parameter("sky_sun_direction", toward_sun) + + # Preserve the authored day/sunset palette while allowing a dynamically + # recolored sun to tint the cloud's lit face. + var live_color := _palette_sun_color.lerp(_sun.light_color, 0.35) + var energy_scale: float = clampf(_sun.light_energy / 1.4, 0.75, 1.25) + _ink_material.set_shader_parameter( + "sky_sun_color", live_color * energy_scale) diff --git a/scenes/maps/cloud_sun_sync.gd.uid b/scenes/maps/cloud_sun_sync.gd.uid new file mode 100644 index 0000000..15b762f --- /dev/null +++ b/scenes/maps/cloud_sun_sync.gd.uid @@ -0,0 +1 @@ +uid://51bevlnoh6tt diff --git a/scenes/maps/level_environment.gd b/scenes/maps/level_environment.gd index c050e64..0d50638 100644 --- a/scenes/maps/level_environment.gd +++ b/scenes/maps/level_environment.gd @@ -1,6 +1,8 @@ extends Object class_name LevelEnvironment +const CLOUD_SUN_SYNC_SCRIPT := preload("res://scenes/maps/cloud_sun_sync.gd") + ## Shared stylized environment for every level: saturated anime-sky gradient, ## bloom for emissives (tracers, plasma), and a color grade that pushes the ## cel-shaded look (high saturation, slight contrast lift). Builders call @@ -15,82 +17,205 @@ static func add_to(level: Node, sky_variant: String = "day") -> WorldEnvironment env.environment = make_environment(sky_variant) level.add_child(env) - if not level.has_node("Sun"): - var sun := DirectionalLight3D.new() - sun.name = "Sun" - sun.rotation_degrees = Vector3(-52, 38, 0) - sun.light_color = Color(1.0, 0.97, 0.88) - sun.light_energy = 1.4 - sun.shadow_enabled = true - sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS - sun.directional_shadow_max_distance = 120.0 - level.add_child(sun) - - var fill := DirectionalLight3D.new() - fill.name = "FillLight" - fill.rotation_degrees = Vector3(-30, -142, 0) - fill.light_color = Color(0.6, 0.7, 1.0) - fill.light_energy = 0.25 - fill.shadow_enabled = false - level.add_child(fill) + var sun := level.get_node_or_null("Sun") as DirectionalLight3D + if sun == null: + sun = _build_light_rig(level, sky_variant) # Screen-space ink edge pass: drawn outlines on every model for whatever # camera renders (fullscreen POSITION-override quad, never culled). + var ink_material: ShaderMaterial if not level.has_node("InkEdgePost"): var ink := MeshInstance3D.new() ink.name = "InkEdgePost" var quad := QuadMesh.new() quad.size = Vector2(2, 2) - var ink_mat := ShaderMaterial.new() - ink_mat.shader = load("res://assets/shaders/ink_edge.gdshader") - quad.material = ink_mat + ink_material = ShaderMaterial.new() + ink_material.shader = load("res://assets/shaders/ink_edge.gdshader") + _apply_sky_palette(ink_material, sky_variant, true) + quad.material = ink_material ink.mesh = quad ink.extra_cull_margin = 16384.0 level.add_child(ink) + else: + var ink := level.get_node("InkEdgePost") as MeshInstance3D + if ink and ink.mesh: + ink_material = ink.mesh.material as ShaderMaterial - # Quiet ambient bed so the arena never sits in dead air. - if not level.has_node("AmbientBed") and ResourceLoader.exists("res://assets/sounds/wind.wav"): - var amb := AudioStreamPlayer.new() - amb.name = "AmbientBed" - amb.stream = load("res://assets/sounds/wind.wav") - if amb.stream is AudioStreamWAV: - amb.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD - amb.stream.loop_end = amb.stream.data.size() / 2 - amb.volume_db = -30.0 - amb.pitch_scale = 0.6 - amb.bus = "SFX" - amb.autoplay = true - level.add_child(amb) + if sun and ink_material and not level.has_node("CloudSunSync"): + var sync := CLOUD_SUN_SYNC_SCRIPT.new() + sync.name = "CloudSunSync" + level.add_child(sync) + sync.configure(sun, ink_material) + + # Deliberately no global broadband ambience. Constant air/noise masks the + # physical cues that matter in a competitive shooter and was perceived as + # television static. Authored map emitters may add localized machinery; + # traversal wind remains silent until the player is genuinely moving fast. return env +## The anime light rig. +## +## Not one key and a token fill. The reference names four lights and every one +## of them is doing a specific job that the cel ramp cannot do without it: +## +## key Warm, and the only one casting shadows. It is what the ramp +## quantises, so it alone decides where every band edge in the frame +## falls. +## bounce A STRONG cool light from the opposite quarter — roughly a third of +## the key, not the tenth a PBR fill would be. This is the single most +## important light in an anime rig: it is what puts a distinct, cool, +## non-black value on every shadow plane, so a surface turning away +## from the sun turns INTO another colour rather than into darkness. +## up A weak violet up-light standing in for ground bounce, which keeps +## undersides — eaves, canopies, the underside of a train — from +## collapsing into one flat mass with the shadow planes. +## ground The hemisphere's ground colour, carried here by ambient (see +## make_environment): violet rather than grey, for the same reason the +## cel ramp's shadow tint is. +## +## The energies look high because the cel shaders divide by PI in light(), the +## same as any Lambert term: a key at 2.6 lands its top band at 2.6/PI ≈ 0.83 of +## the surface albedo, which is what "fully lit" should mean. +static func _build_light_rig(level: Node, sky_variant: String) -> DirectionalLight3D: + var warm := Color(1.0, 0.945, 0.847) # PAL.sun 0xfff1d8 + var cool := Color(0.663, 0.741, 0.961) # PAL.fill 0xa9bdf5 + var key_energy := 2.6 + var bounce_energy := 0.95 + var key_angle := Vector3(-48, 34, 0) + if sky_variant == "sunset": + warm = Color(1.0, 0.78, 0.58) + cool = Color(0.52, 0.56, 0.92) + key_energy = 2.2 + bounce_energy = 1.1 + key_angle = Vector3(-16, 28, 0) + + var sun := DirectionalLight3D.new() + sun.name = "Sun" + sun.rotation_degrees = key_angle + sun.light_color = warm + sun.light_energy = key_energy + sun.shadow_enabled = true + sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS + sun.directional_shadow_max_distance = 160.0 + level.add_child(sun) + + # The opposite quarter: the key's yaw turned 180 degrees, and a shallower + # pitch, so the bounce rakes across the shadow planes instead of lighting + # them like a second sun. + var bounce := DirectionalLight3D.new() + bounce.name = "FillLight" + bounce.rotation_degrees = Vector3(-24, key_angle.y + 180.0, 0) + bounce.light_color = cool + bounce.light_energy = bounce_energy + bounce.shadow_enabled = false + level.add_child(bounce) + + var up := DirectionalLight3D.new() + up.name = "UpLight" + up.rotation_degrees = Vector3(74, key_angle.y - 60.0, 0) + up.light_color = Color(0.714, 0.651, 0.776) # PAL.hemiGround 0xb6a6c6 + up.light_energy = 0.30 + up.shadow_enabled = false + level.add_child(up) + + return sun + + +## Cel sky with rounded, flat-toned anime clouds. +## +## This replaces ProceduralSkyMaterial, whose smooth two-colour ramp was the +## largest single flat area in every frame — tools/levels.py measured a quarter +## to a third of each shot sitting at one shade, and almost all of it was sky. +## Clouds give the top half of the picture something to be. They use a layered +## 2D field and painted directional tones rather than volumetric ray marching, +## keeping the richer silhouette practical for a competitive shooter. +static func make_sky(sky_variant: String = "day") -> Sky: + var sky := Sky.new() + var mat := ShaderMaterial.new() + mat.shader = load("res://assets/shaders/anime_sky.gdshader") + _apply_sky_palette(mat, sky_variant) + sky.sky_material = mat + # The sky animates, so its radiance would otherwise be re-rendered in full + # every frame for the maps that take ambient from it. INCREMENTAL spreads + # that across frames, which is invisible at a drift this slow. + sky.process_mode = Sky.PROCESS_MODE_INCREMENTAL + return sky + + +## Keep the Environment sky (ambient/reflections) and the ink pass's final +## background composite on one authored palette. +static func _apply_sky_palette( + mat: ShaderMaterial, sky_variant: String, ink_pass: bool = false) -> void: + var prefix := "sky_" if ink_pass else "" + match sky_variant: + "sakura": + # A hand-painted anime background sky, which means PALE. The bold + # saturated blue the other variants use is a video-game sky; a cel + # background painter keeps the top of the picture light and low in + # contrast so the ink line work and the blossom read against it. The + # horizon carries the reference's pink haze (PAL.skyHaze), which is + # what makes the air itself look like it has petals in it. + mat.set_shader_parameter(prefix + "top_color", Color(0.561, 0.741, 0.918)) + mat.set_shader_parameter(prefix + "horizon_color", Color(0.949, 0.910, 0.933)) + mat.set_shader_parameter(prefix + "ground_color", Color(0.714, 0.651, 0.776)) + mat.set_shader_parameter(prefix + "cloud_color", Color(0.992, 0.980, 0.973)) + mat.set_shader_parameter(prefix + "cloud_shadow", Color(0.902, 0.902, 0.949)) + mat.set_shader_parameter(prefix + "cloud_cover", 0.520) + mat.set_shader_parameter(prefix + "cloud_scale", 2.2) + # Harder cloud edges than the other skies. A cel cloud is a shape + # with a drawn outline, not a soft mass. + mat.set_shader_parameter(prefix + "cloud_softness", 0.014) + mat.set_shader_parameter(prefix + "horizon_falloff", 0.62) + if ink_pass: + mat.set_shader_parameter("sky_sun_color", Color(1.0, 0.945, 0.847)) + mat.set_shader_parameter("sky_sun_halo", 0.25) + mat.set_shader_parameter("sky_sun_direction", Vector3(0.46, 0.52, -0.72)) + "sunset": + mat.set_shader_parameter(prefix + "top_color", Color(0.25, 0.20, 0.50)) + mat.set_shader_parameter(prefix + "horizon_color", Color(0.95, 0.60, 0.45)) + mat.set_shader_parameter(prefix + "ground_color", Color(0.18, 0.11, 0.25)) + # Sunset clouds are lit from underneath and from the side, so their + # lit face takes the sun's colour and their shadow goes violet + # rather than grey. + mat.set_shader_parameter(prefix + "cloud_color", Color(1.0, 0.80, 0.62)) + mat.set_shader_parameter(prefix + "cloud_shadow", Color(0.52, 0.38, 0.55)) + mat.set_shader_parameter(prefix + "cloud_cover", 0.515) + mat.set_shader_parameter(prefix + "cloud_scale", 2.8) + mat.set_shader_parameter(prefix + "cloud_softness", 0.020) + mat.set_shader_parameter(prefix + "sun_halo", 0.55) + if ink_pass: + mat.set_shader_parameter("sky_sun_color", Color(1.0, 0.62, 0.42)) + mat.set_shader_parameter("sky_sun_direction", Vector3(0.55, 0.20, -0.81)) + _: + # Bold anime day sky: deep saturated blue up top, bright cyan horizon. + mat.set_shader_parameter(prefix + "top_color", Color(0.18, 0.40, 0.85)) + mat.set_shader_parameter(prefix + "horizon_color", Color(0.72, 0.88, 0.98)) + mat.set_shader_parameter(prefix + "ground_color", Color(0.22, 0.20, 0.28)) + mat.set_shader_parameter(prefix + "cloud_color", Color(1.0, 0.99, 0.97)) + mat.set_shader_parameter(prefix + "cloud_shadow", Color(0.72, 0.75, 0.88)) + mat.set_shader_parameter(prefix + "cloud_cover", 0.500) + mat.set_shader_parameter(prefix + "cloud_scale", 2.5) + mat.set_shader_parameter(prefix + "cloud_softness", 0.020) + + static func make_environment(sky_variant: String = "day") -> Environment: var environment := Environment.new() environment.background_mode = Environment.BG_SKY - var sky := Sky.new() - var sky_mat := ProceduralSkyMaterial.new() - match sky_variant: - "sunset": - sky_mat.sky_top_color = Color(0.25, 0.2, 0.5) - sky_mat.sky_horizon_color = Color(0.95, 0.6, 0.45) - sky_mat.ground_bottom_color = Color(0.18, 0.12, 0.2) - sky_mat.ground_horizon_color = Color(0.85, 0.55, 0.45) - sky_mat.sun_curve = 0.12 - _: - # Bold anime day sky: deep saturated blue up top, bright cyan horizon. - sky_mat.sky_top_color = Color(0.18, 0.4, 0.85) - sky_mat.sky_horizon_color = Color(0.72, 0.88, 0.98) - sky_mat.ground_bottom_color = Color(0.22, 0.2, 0.24) - sky_mat.ground_horizon_color = Color(0.62, 0.7, 0.75) - sky_mat.sun_curve = 0.1 - sky_mat.sun_angle_max = 20.0 - sky.sky_material = sky_mat - environment.sky = sky + environment.sky = make_sky(sky_variant) # Flat-ish ambient keeps toon shadow bands readable (sun does the shaping). environment.ambient_light_source = Environment.AMBIENT_SOURCE_SKY environment.ambient_light_energy = 0.55 + if sky_variant == "sakura": + # The rig's bounce and up-light already put a cool value on every shadow + # plane, and the cel ramp's darkest stop is 0.36 rather than zero. Left + # at 0.55 on top of that, ambient becomes a flood: the bands converge, + # the violet shadow tint washes out, and the picture goes milky. This is + # the hemisphere's SKY half only — its ground half is the UpLight. + environment.ambient_light_energy = 0.22 + # Filmic with a high white point: near-linear through the cel bands (they # stay crisp) but a soft shoulder above 1.0. LINEAR clipped lit white # surfaces and then bloomed the clip — every facade glowed like porcelain. @@ -104,16 +229,68 @@ static func make_environment(sky_variant: String = "day") -> Environment: environment.glow_bloom = 0.05 environment.glow_hdr_threshold = 1.45 - # Cel color grade: a LIGHT saturation lift only. Anything stronger shifts - # hues — authored ginger hair grades into fire-truck red, pastels go neon — - # and the whole game reads "filtered". Let the textures' own colors carry - # the vibrancy. - environment.adjustment_enabled = true - environment.adjustment_saturation = 1.08 - environment.adjustment_contrast = 1.03 + # The colour grade moved into the ink pass, where it can be a SPLIT-tone + # (cool violet into the darks, warm paper white into the lights) instead of + # a single saturation number. Environment's adjustment stage runs after that + # pass, so leaving it at 1.08 would grade the frame twice and re-introduce + # exactly the hue shifting the note below warns about. + environment.adjustment_enabled = false + environment.adjustment_saturation = 1.0 + environment.adjustment_contrast = 1.0 # A touch of depth haze for scale; far enough to not gray the arena. environment.fog_enabled = true environment.fog_light_color = Color(0.65, 0.75, 0.9) environment.fog_density = 0.0012 + + if sky_variant == "sakura": + # Haze, not fog. The reference's far hills are pale and low-contrast + # because everything more than a hundred metres out is painted as + # atmosphere; the density is chosen so a two-hundred-metre view still + # reads as air rather than as weather. + environment.fog_light_color = Color(0.902, 0.925, 0.969) # PAL.fog + environment.fog_density = 0.0022 + + _add_grounding(environment) return environment + + +## Contact darkening. This is what was missing more than anything else. +## +## Ambient here is a flood — a single energy applied to every surface no matter +## what is standing over it — so a lamp post met the pavement with no darkening +## at all, and so did every building, every crate and every character's feet. +## `tools/levels.py` puts a number on it: the Neon Alley aerial measured a 1st +## percentile of 119, meaning the DARKEST few pixels in the whole frame were +## still mid-grey. Nothing was dark, so nothing looked like it was resting on +## anything. +## +## Lowering the old 1.35 ambient flood restores form, but it cannot solve +## contact on its own. The rest of the fix is to take ambient away only where +## geometry is in the way, which is exactly what SSAO does. +## +## The tuning is what keeps it anime rather than "PBR game with dirt in the +## corners": +## +## radius 0.7 Contact, not global. A big radius produces the soft grey +## haze under everything that reads as smog; a tight one draws +## a dark seam right where two surfaces meet, which is what a +## cel artist would ink in by hand. +## power 3.0 Sharpens the falloff so the occlusion is nearly gone a +## half-metre out. Combined with the small radius this gives a +## defined contact line rather than a gradient. +## sharpness .98 Keeps the AO from bleeding across depth discontinuities. +## Blurry AO over a hard-edged cel look is instantly wrong. +## light_affect Only 0.08. Direct sunlight must keep landing in clean flat +## bands — that IS the style. AO belongs in the ambient term, +## where it darkens what the sun never reached anyway. +static func _add_grounding(environment: Environment) -> void: + environment.ssao_enabled = true + environment.ssao_radius = 0.7 + environment.ssao_intensity = 3.2 + environment.ssao_power = 3.0 + environment.ssao_detail = 0.6 + environment.ssao_horizon = 0.1 + environment.ssao_sharpness = 0.98 + environment.ssao_light_affect = 0.08 + environment.ssao_ao_channel_affect = 0.0 diff --git a/scenes/maps/level_materials.gd b/scenes/maps/level_materials.gd index 3f0dc13..2450df7 100644 --- a/scenes/maps/level_materials.gd +++ b/scenes/maps/level_materials.gd @@ -53,11 +53,56 @@ static func tinted(tint: Color, dark: bool = false) -> Material: static var _flat_cache: Dictionary = {} + +## Surface laws: what KIND of thing this is, rather than what colour it is. +## +## A flat cel colour is the style, but a forty-metre wall holding exactly one +## value is not stylised, it is unfinished — the probe measured 0.28 mean +## adjacent-pixel difference, meaning the only structure in the frame was the +## silhouettes. These give a surface its own quiet detail underneath the cel +## bands, all of it procedural and world-space, so nothing needs UVs, textures, +## or an artist. +## +## The laws are deliberately few. Every one of them has to be a thing a cel +## painter would actually draw: +## +## wall Panel seams at storey-ish spacing plus a grade that darkens the +## bottom few metres. The grade is the important half: it is +## painted-in ambient occlusion, it grounds a building at any +## distance, and unlike SSAO it does not vanish when the camera +## pulls back. +## ground Wider seams and no grade — a floor has no "bottom" to darken, +## and paving slabs are bigger than wall panels. +## panel Tight seams for machined props: shutters, containers, kiosks. +## trim Grade only. For kerbs, plinths and bases, where a seam grid +## would fight the shape but contact darkening still helps. +## "" Plain flat colour. Props, vehicles, foliage and anything whose +## silhouette is doing the work — a seam grid crawling over a tree +## or a car reads as dirt, not as construction. +const SURFACE_LAW := { + "wall": {"seam_scale": 2.6, "seam_strength": 0.20, "seam_width": 0.010, + "grade_height": 7.0, "grade_strength": 0.20}, + "ground": {"seam_scale": 4.0, "seam_strength": 0.13, "seam_width": 0.006}, + "panel": {"seam_scale": 0.9, "seam_strength": 0.24, "seam_width": 0.020}, + "trim": {"grade_height": 1.6, "grade_strength": 0.24}, +} + + +static func _apply_law(mat: ShaderMaterial, law: String) -> void: + if law == "" or not SURFACE_LAW.has(law): + return + for param in SURFACE_LAW[law]: + mat.set_shader_parameter(param, SURFACE_LAW[law][param]) + + ## Flat cel color: toon banding with NO grid texture — the full stylized ## look for dressed maps (vs. tinted()'s greybox grid for blockouts). ## Rim/specular stay off (they blob on large level surfaces). -static func flat(tint: Color) -> Material: - var key := tint.to_html() +## +## `law` names a SURFACE_LAW: what the surface is made of, not what colour it +## is. Defaults to plain flat colour, so every existing call site is unchanged. +static func flat(tint: Color, law: String = "") -> Material: + var key := "%s|%s" % [tint.to_html(), law] if _flat_cache.has(key): return _flat_cache[key] var mat := ShaderMaterial.new() @@ -67,6 +112,7 @@ static func flat(tint: Color) -> Material: mat.set_shader_parameter("albedo_color", tint) mat.set_shader_parameter("rim_strength", 0.0) mat.set_shader_parameter("specular_strength", 0.0) + _apply_law(mat, law) _flat_cache[key] = mat return mat @@ -252,10 +298,114 @@ static func apply_character_look(root: Node, surfaces: SkinSurfaces = null) -> v toon.set_shader_parameter("shadow_color", CHARACTER_SHADOW) # Eye irises and highlights are flat art on a curved ball; an ink # line around them reads as a second pupil. + # The dummy headless renderer has no material backend for an + # outline pass and emits teardown errors while following next_pass + # RIDs. CI needs the animation/scene graph, not pixels. toon.next_pass = null if detail == "eyes" \ + or DisplayServer.get_name() == "headless" \ else outline(look["outline"]) +## First-person weapons live in their own viewport and are read at arm's +## length, so they need a different emphasis from distant world props: a broad +## cool rim for silhouette separation and a small stepped metal glint that +## reveals receivers, magazines and sights without turning the gun glossy. +## This is intentionally viewmodel-only; characters stay matte. +const VIEWMODEL_LOOK := { + "dark_metal": { + "rim": 0.12, "rim_width": 0.26, "band": 0.035, "mid": 0.62, + "shadow": Color(0.12, 0.15, 0.25), "cast": 0.34, + "specular": 0.14, "shininess": 104.0, "floor": 0.0, + "ambient": 0.0, "contrast": 1.12, "saturation": 1.02, "gain": 0.96, + "painted_light": 1.15, "vm_shadow": 0.18, "vm_mid": 0.58, + "crease": 0.34, "crease_threshold": 0.09, + }, + "light_metal": { + "rim": 0.11, "rim_width": 0.28, "band": 0.045, "mid": 0.70, + "shadow": Color(0.30, 0.36, 0.52), "cast": 0.42, + "specular": 0.10, "shininess": 112.0, "floor": 0.0, + "ambient": 0.0, "contrast": 0.96, "saturation": 0.96, "gain": 0.52, + "painted_light": 0.35, "vm_shadow": 0.16, "vm_mid": 0.50, + "crease": 0.28, "crease_threshold": 0.10, + }, + "painted": { + "rim": 0.12, "rim_width": 0.30, "band": 0.050, "mid": 0.72, + "shadow": Color(0.25, 0.30, 0.46), "cast": 0.44, + "specular": 0.035, "shininess": 88.0, "floor": 0.0, + "ambient": 0.0, "contrast": 0.90, "saturation": 1.30, "gain": 0.90, + "painted_light": 0.88, "vm_shadow": 0.14, "vm_mid": 0.48, + "crease": 0.24, "crease_threshold": 0.11, + }, + "polymer": { + "rim": 0.09, "rim_width": 0.26, "band": 0.045, "mid": 0.64, + "shadow": Color(0.16, 0.19, 0.29), "cast": 0.38, + "specular": 0.015, "shininess": 72.0, "floor": 0.0, + "ambient": 0.0, "contrast": 0.82, "saturation": 1.02, "gain": 0.88, + "painted_light": 0.88, "vm_shadow": 0.11, "vm_mid": 0.44, + "crease": 0.30, "crease_threshold": 0.10, + }, +} + + +static func _viewmodel_profile(mat: ShaderMaterial) -> String: + var value = mat.get_shader_parameter("albedo_color") + var color := value as Color if value is Color else Color.WHITE + var luma := color.get_luminance() + var chroma := maxf(color.r, maxf(color.g, color.b)) \ + - minf(color.r, minf(color.g, color.b)) + var textured := bool(mat.get_shader_parameter("has_texture")) + if textured or chroma > 0.045: + return "painted" + if luma < 0.34: + return "dark_metal" + if luma > 0.72: + return "light_metal" + return "polymer" + + +static func apply_viewmodel_look(root: Node) -> void: + for mi in root.find_children("*", "MeshInstance3D", true, false): + if not mi.mesh: + continue + for s in range(mi.mesh.get_surface_count()): + var toon := mi.get_surface_override_material(s) as ShaderMaterial + if toon == null: + continue + var look: Dictionary = VIEWMODEL_LOOK[_viewmodel_profile(toon)] + toon.set_shader_parameter("rim_strength", look["rim"]) + toon.set_shader_parameter("rim_width", look["rim_width"]) + toon.set_shader_parameter("band_softness", look["band"]) + toon.set_shader_parameter("mid_tone", look["mid"]) + toon.set_shader_parameter("shadow_color", look["shadow"]) + toon.set_shader_parameter("cast_shadow_depth", look["cast"]) + toon.set_shader_parameter("specular_strength", look["specular"]) + toon.set_shader_parameter("specular_shininess", look["shininess"]) + toon.set_shader_parameter("albedo_floor", look["floor"]) + toon.set_shader_parameter("ambient_fill", look["ambient"]) + toon.set_shader_parameter("albedo_contrast", look["contrast"]) + toon.set_shader_parameter("albedo_saturation", look["saturation"]) + toon.set_shader_parameter("albedo_gain", look["gain"]) + toon.set_shader_parameter( + "viewmodel_light_strength", look["painted_light"]) + toon.set_shader_parameter( + "viewmodel_shadow_tone", look["vm_shadow"]) + toon.set_shader_parameter( + "viewmodel_mid_tone", look["vm_mid"]) + toon.set_shader_parameter( + "viewmodel_crease_strength", look["crease"]) + toon.set_shader_parameter( + "viewmodel_crease_threshold", look["crease_threshold"]) + # Set these explicitly rather than relying on shader defaults. + # Runtime-created ShaderMaterials can predate a hot-reloaded shader + # default in the editor cache, which left the fill at black. + toon.set_shader_parameter( + "viewmodel_key_direction", Vector3(-0.42, 0.58, 0.70)) + toon.set_shader_parameter( + "viewmodel_key_color", Color(0.98, 0.88, 0.76)) + toon.set_shader_parameter( + "viewmodel_fill_color", Color(0.62, 0.68, 0.88)) + + ## Swap every mesh surface under `node` to toon shading and add an ## inverted-hull outline overlay. Safe on skinned meshes (material_overlay ## re-renders the same deformed mesh). @@ -265,14 +415,99 @@ static func apply_toon_recursive(node: Node, outline_width: float = 0.005) -> vo var surface_count: int = mi.mesh.get_surface_count() if mi.mesh else 0 for s in range(surface_count): var src := mi.get_active_material(s) + # Some kit surfaces intentionally omit a material. Leaving those + # null is visually equivalent to Godot's white default, but the + # headless renderer cannot query instance shader parameters from a + # null material during scene teardown. Give them an explicit toon + # default so runtime map sweeps remain error-free. + if src == null: + var fallback := StandardMaterial3D.new() + fallback.albedo_color = Color(0.78, 0.78, 0.82) + src = fallback if src and not (src is ShaderMaterial): mi.set_surface_override_material(s, toonify(src)) - if outline_width > 0.0: + var toon := mi.get_surface_override_material(s) as ShaderMaterial + if toon: + var arrays: Array = mi.mesh.surface_get_arrays(s) + var has_vertex_colors: bool = arrays.size() > Mesh.ARRAY_COLOR \ + and arrays[Mesh.ARRAY_COLOR] is PackedColorArray \ + and not (arrays[Mesh.ARRAY_COLOR] as PackedColorArray).is_empty() + toon.set_shader_parameter("use_vertex_color", has_vertex_colors) + # Avoid constructing render-only overlay chains on the headless dummy + # backend. The real game renderer still receives the complete ink pass. + if outline_width > 0.0 and DisplayServer.get_name() != "headless": mi.material_overlay = outline(outline_width) for child in node.get_children(): apply_toon_recursive(child, outline_width) +## ── The sakura cel look ────────────────────────────────────────────────────── +## +## `flat()` above is the older path: toon.gdshader's three-tone break, calibrated +## against imported character textures that already carry painted shading. These +## build on sakura_cel.gdshader instead — a quantised N-band ramp with a +## hue-shifted shadow — which is what flat-coloured world geometry wants. +const CEL_SHADER := "res://assets/shaders/sakura_cel.gdshader" + +## Ramp ids, matching `ramp_id` in sakura_cel.gdshader. +## +## RAMP_3 the default, and what most of a scene should be +## RAMP_2 two hard tones, for small props whose silhouette does the work +## RAMP_4/5 more steps, for large curved masses that would otherwise show +## their band edges as hard bars across a wall +## RAMP_SOFT* high key: the darkest band is 0.71 rather than 0.36, for pale +## masses (blossom, cloud, plaster) that must stay light even on +## the shadow side +enum { + RAMP_2 = 0, + RAMP_3 = 1, + RAMP_4 = 2, + RAMP_5 = 3, + RAMP_SOFT = 4, + RAMP_SOFT3 = 5, +} + +static var _cel_cache: Dictionary = {} +static var _unlit_cache: Dictionary = {} + + +## A cel material. `law` names a SURFACE_LAW as `flat()` does; `faceted` maps to +## the shader's flat_shading and should stay on for anything built out of boxes +## and cylinders, which here is everything. +static func cel(color: Color, ramp: int = RAMP_3, tint: Color = SakuraPalette.TINT, + law: String = "", faceted: bool = true) -> ShaderMaterial: + var key := "%s|%d|%s|%s|%s" % [color.to_html(), ramp, tint.to_html(), law, faceted] + if _cel_cache.has(key): + return _cel_cache[key] + var mat := ShaderMaterial.new() + mat.shader = load(CEL_SHADER) + mat.set_shader_parameter("albedo_color", color) + mat.set_shader_parameter("has_texture", false) + mat.set_shader_parameter("ramp_id", ramp) + mat.set_shader_parameter("shadow_tint", tint) + mat.set_shader_parameter("flat_shading", faceted) + _apply_law(mat, law) + _cel_cache[key] = mat + return mat + + +## Unlit flat colour — the reference's `flat()`. For sky panels, distant +## silhouettes, sign faces, glass, lit windows and the ink details a cel painter +## would draw rather than light. Anything that takes light instead of being a +## drawing should use cel(). +static func unlit(color: Color, double_sided: bool = false) -> StandardMaterial3D: + var key := "%s|%s" % [color.to_html(), double_sided] + if _unlit_cache.has(key): + return _unlit_cache[key] + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.albedo_color = color + if double_sided: + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + _unlit_cache[key] = mat + return mat + + static func outline(width: float = 0.005) -> ShaderMaterial: var key := "%.4f" % width if _outline_cache.has(key): diff --git a/scenes/maps/level_runtime.gd b/scenes/maps/level_runtime.gd index b5f8ab7..acb5c45 100644 --- a/scenes/maps/level_runtime.gd +++ b/scenes/maps/level_runtime.gd @@ -4,6 +4,45 @@ class_name LevelRuntime var _player: CharacterBody3D +## The play volume for an AUTHORED map. +## +## The code-built levels each pass their own extents, because they know them as +## constants. A hand-placed .tscn does not, so this measures the level instead — +## merge every reasonably-sized visual's world AABB, then pad it generously. +## +## Without one, a map on this runtime had no out-of-bounds handling at all: a +## player who left the geometry simply fell for ever, with no warning and no +## death. That was the state of fps_blockout. +func _build_combat_area() -> void: + var bounds := AABB() + var any := false + for child in find_children("*", "VisualInstance3D", true, false): + var vi := child as VisualInstance3D + var box := vi.get_aabb() + # Skip the sky, a directional light's own AABB, and anything else + # absurd — one of them would swallow the level and put the boundary + # somewhere useless. + if box.size.length() > 5000.0 or box.size.length() < 0.001: + continue + box = vi.global_transform * box + if not any: + bounds = box + any = true + else: + bounds = bounds.merge(box) + if not any: + bounds = AABB(Vector3(-50, -10, -50), Vector3(100, 60, 100)) + + # Padding, not a tight fit: the volume marks where a player is TOLD they + # have left, and a boundary hugging the geometry fires while someone is + # still standing on a legitimate ledge. Deep below, because falling out of + # the world is the case this exists to catch. + var pad := Vector3(20, 0, 20) + var size := bounds.size + pad * 2.0 + Vector3(0, 60, 0) + var centre := bounds.get_center() + Vector3(0, 20.0, 0) + CombatArea.add_to(self, size, centre) + + func _ready() -> void: # Hide mouse Input.mouse_mode = Input.MOUSE_MODE_CAPTURED @@ -13,6 +52,8 @@ func _ready() -> void: if not has_node("WorldEnvironment"): LevelEnvironment.add_to(self) + _build_combat_area() + # Multiplayer Spawning var spawner = MultiplayerSpawner.new() diff --git a/scenes/maps/neon_alley/neon_alley_builder.gd b/scenes/maps/neon_alley/neon_alley_builder.gd index 66218e2..462521c 100644 --- a/scenes/maps/neon_alley/neon_alley_builder.gd +++ b/scenes/maps/neon_alley/neon_alley_builder.gd @@ -96,26 +96,106 @@ func _kit_scene(key: String) -> PackedScene: return _kit_cache[key] +## Combined rendered bounds expressed in an ancestor's local space. Imported +## kit dimensions are useful for layout, but collision must follow the actual +## imported geometry rather than a rounded catalog number. +func _visual_bounds_in(node: Node, relative_to: Node3D) -> AABB: + var result := AABB() + var has_bounds := false + var to_local := relative_to.global_transform.affine_inverse() + for child in node.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := child as MeshInstance3D + if mesh_instance.mesh == null: + continue + var transformed := _transformed_aabb( + mesh_instance.get_aabb(), to_local * mesh_instance.global_transform) + if has_bounds: + result = result.merge(transformed) + else: + result = transformed + has_bounds = true + return result if has_bounds else AABB() + + +func _transformed_aabb(bounds: AABB, xform: Transform3D) -> AABB: + var result := AABB(xform * bounds.position, Vector3.ZERO) + for x in 2: + for y in 2: + for z in 2: + var corner := bounds.position + Vector3( + bounds.size.x * float(x), + bounds.size.y * float(y), + bounds.size.z * float(z)) + result = result.expand(xform * corner) + return result + + +func _fit_box_collider(body: StaticBody3D, visual: Node, + horizontal_inset: float = 0.04) -> CollisionShape3D: + var bounds := _visual_bounds_in(visual, body) + var shape := CollisionShape3D.new() + shape.name = "CollisionShape3D" + var box := BoxShape3D.new() + box.size = Vector3( + maxf(0.05, bounds.size.x - horizontal_inset), + maxf(0.05, bounds.size.y - 0.02), + maxf(0.05, bounds.size.z - horizontal_inset)) + shape.shape = box + shape.position = bounds.get_center() + body.add_child(shape) + body.set_meta("mesh_fitted_collision", true) + return shape + + +## Exact collision for imported art. One enclosing AABB is not mesh fitting: +## a window sill or roof overhang moves its face away from the playable wall +## and creates an invisible wallrun/bullet plane. Static concave shapes +## preserve every recess, opening and overhang in the rendered mesh. +func _fit_mesh_colliders(body: StaticBody3D, visual: Node) -> int: + var count := 0 + var to_body := body.global_transform.affine_inverse() + for child in visual.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := child as MeshInstance3D + if mesh_instance.mesh == null: + continue + var triangle_shape := mesh_instance.mesh.create_trimesh_shape() + if triangle_shape == null: + continue + var collision := CollisionShape3D.new() + collision.name = "ExactMeshCollision_%03d" % count + collision.shape = triangle_shape + body.add_child(collision) + collision.transform = to_body * mesh_instance.global_transform + collision.set_meta("source_mesh_path", str(mesh_instance.get_path())) + count += 1 + if count == 0: + _fit_box_collider(body, visual, 0.06) + body.set_meta("exact_collision_fallback", true) + else: + body.set_meta("mesh_exact_collision", true) + body.set_meta("exact_collision_count", count) + return count + + # ── Hero assets (original kitbash, tools/build_hero_assets.py) ────────────── -## Hero piece with a box collider. Front faces -Z before yaw (glTF Y-up). -func _hero(key: String, pos: Vector3, yaw_deg: float, col_size: Vector3, - col_center_y: float, acoustic: String = "concrete") -> void: +## Hero piece with exact imported-mesh collision. Front faces -Z before yaw. +func _hero(key: String, pos: Vector3, yaw_deg: float, _col_size: Vector3, + _col_center_y: float, acoustic: String = "concrete") -> void: var body := StaticBody3D.new() body.name = "Hero_" + key + "_%d" % _rng.randi() body.set_meta("acoustic_material", acoustic) add_child(body) body.global_position = pos body.rotation_degrees.y = yaw_deg - var shape := CollisionShape3D.new() - var bs := BoxShape3D.new() - bs.size = col_size - shape.shape = bs - shape.position = Vector3(0, col_center_y, 0) - body.add_child(shape) var inst: Node3D = _kit_scene_at("res://assets/props/hero/" + key + ".glb").instantiate() body.add_child(inst) LevelMaterials.apply_toon_recursive(inst, 0.005) + # Authored origins differ between hero pieces. Seat the rendered bottom on + # the requested support plane before deriving collision from that geometry. + var bounds := _visual_bounds_in(inst, body) + inst.position.y -= bounds.position.y + _fit_mesh_colliders(body, inst) ## Small visual-only kit prop (parasols, awnings): no collider needed. @@ -125,16 +205,19 @@ func _kit_prop(key: String, pos: Vector3, yaw_deg: float, scale_f: float) -> voi add_child(inst) inst.global_position = pos inst.rotation_degrees.y = yaw_deg + inst.set_meta("source_asset_path", KIT_DIR + key + ".glb") LevelMaterials.apply_toon_recursive(inst, 0.007) + var bounds := _visual_bounds_in(inst, self) + inst.global_position.y += pos.y - bounds.position.y + inst.set_meta("ground_fitted", true) -## Place a kit model with a box collider matching its scaled bounds. +## Place a kit model with exact per-mesh collision. ## front_pos: centre of the facade at ground level. yaw_deg: 0 faces +Z. func _kit_building(key: String, front_pos: Vector3, yaw_deg: float, node_name: String, scale_f: float = KIT_SCALE) -> float: var dims: Vector3 = KIT_BUILDINGS.get(key, KIT_SKYSCRAPERS.get(key, Vector3.ONE)) var w := dims.x * scale_f - var h := dims.y * scale_f var d := dims.z * scale_f var fwd := Vector3(sin(deg_to_rad(yaw_deg)), 0, cos(deg_to_rad(yaw_deg))) var center := front_pos - fwd * d * 0.5 @@ -145,19 +228,13 @@ func _kit_building(key: String, front_pos: Vector3, yaw_deg: float, node_name: S add_child(body) body.global_position = center body.rotation_degrees.y = yaw_deg - var shape := CollisionShape3D.new() - var bs := BoxShape3D.new() - bs.size = Vector3(w, h, d) - shape.shape = bs - shape.position = Vector3(0, h * 0.5, 0) - body.add_child(shape) - var inst: Node3D = _kit_scene(key).instantiate() inst.scale = Vector3.ONE * scale_f body.add_child(inst) LevelMaterials.apply_toon_recursive(inst, 0.0) # Subtle per-building tint so repeated kit models read as different shops _tint_recursive(inst, Color(1, 1, 1).lerp(Color.from_hsv(_rng.randf(), 0.3, 1.0), 0.16)) + _fit_mesh_colliders(body, inst) return w @@ -246,14 +323,36 @@ func _kit_fill_side(front_x: float, dir: int, z0: float, z1: float, seed_id: Str func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String = "", acoustic: String = "") -> StaticBody3D: var body := super._box_static(pos, size, color, node_name, acoustic) + var law := _surface_law(size, node_name, acoustic) for mi in body.find_children("*", "MeshInstance3D", false, false): - mi.mesh.surface_set_material(0, LevelMaterials.flat(color)) + mi.mesh.surface_set_material(0, LevelMaterials.flat(color, law)) if maxf(size.x, maxf(size.y, size.z)) < 10.0: var w := clampf(maxf(size.x, maxf(size.y, size.z)) * 0.003, 0.007, 0.018) mi.material_overlay = LevelMaterials.outline(w) return body +## Choose a construction law from what the object is, not from its colour. +## This is the map-side half of LevelMaterials.SURFACE_LAW: it finally applies +## the shared surfacing kit to the actual city instead of only to the fidelity +## probe. Thin horizontal slabs read as paving, structural masonry gets the +## storey-scale wall grid, and machined metal gets tighter panels. +func _surface_law(size: Vector3, node_name: String, acoustic: String) -> String: + var footprint := minf(absf(size.x), absf(size.z)) + if node_name == "Ground" or (absf(size.y) <= 0.5 and footprint >= 2.0): + return "ground" + match acoustic: + "concrete", "brick": + if absf(size.y) >= 3.0 and maxf(absf(size.x), absf(size.z)) >= 4.0: + return "wall" + return "trim" + "metal": + return "panel" + "wood": + return "trim" + return "" + + # ── Ad graphics (generated library: tools/generate_ads.py) ────────────────── var _ad_mats: Dictionary = {} @@ -324,14 +423,15 @@ func _build_environment() -> void: # Under the filmic shoulder the ambient's lavender tint actually shows # (LINEAR used to clip it to white) — keep it lower so the sun shapes # surfaces and lit faces stay bright. - env.environment.ambient_light_energy = 1.35 + env.environment.ambient_light_energy = 0.92 env.environment.fog_density = 0.0007 # light city haze for depth over 600m + env.environment.fog_light_color = Color(0.58, 0.43, 0.66) # Ambient occlusion grounds the modeled facade detail (ZZZ-style depth) env.environment.ssao_enabled = true env.environment.ssao_intensity = 2.0 var sun := get_node_or_null("Sun") if sun: - sun.light_energy = 2.5 + sun.light_energy = 1.85 sun.rotation_degrees = Vector3(-38, 55, 0) sun.directional_shadow_max_distance = 220.0 @@ -339,6 +439,12 @@ func _build_environment() -> void: func _build_geometry() -> void: _build_environment() _build_lighting() + # The play volume: just outside the perimeter wall, and tall enough to clear + # the skyscrapers. Without one a player who grappled over that wall fell out + # of the world with no warning and no death. + CombatArea.add_to(self, + Vector3(CITY_HALF * 2.0 + 26.0, 220.0, CITY_HALF * 2.0 + 26.0), + Vector3(0, 96.0, 0)) # Ground: one asphalt slab for the whole district _box_static(Vector3(0, -0.5, 0), Vector3(CITY_HALF * 2.0 + 16.0, 1.0, CITY_HALF * 2.0 + 16.0), ASPHALT, "Ground") # Perimeter wall @@ -465,12 +571,28 @@ func _standard_block(c: Vector3, bx: int, bz: int) -> void: # Detailed kit buildings fill both street-facing edges _kit_fill_side(c.x - half, 1, c.z - half, c.z + half, "%d_%dW" % [bx, bz]) _kit_fill_side(c.x + half, -1, c.z - half, c.z + half, "%d_%dE" % [bx, bz]) - # Fire escapes into the mid-block gap on some blocks + # A supported mid-block fire-escape scaffold on some blocks. The old + # version was two unrelated slabs suspended in open air. if _rng.randi() % 2 == 0: + var escape_h := FLOOR_H * 2.0 - 0.4 + for ex in [-1.1, 1.1]: + for ez in [2.9, 5.1]: + _box_static(c + Vector3(ex, escape_h * 0.5, ez), + Vector3(0.12, escape_h, 0.12), METAL.darkened(0.18), + "EscPost_%d_%d_%d_%d" % [bx, bz, int(ex), int(ez)], "metal") for f in range(1, 3): var y := float(f) * FLOOR_H - 0.4 - _box_static(c + Vector3(-2.0 + float(f) * 1.3, y, 4.0), Vector3(2.2, 0.16, 2.2), METAL, - "Esc_%d_%d_%d" % [bx, bz, f], "metal") + _box_static(c + Vector3(0, y, 4.0), Vector3(2.6, 0.16, 2.4), + METAL, "EscLanding_%d_%d_%d" % [bx, bz, f], "metal") + var stair_angle := rad_to_deg(atan2(FLOOR_H, 4.0)) + _ramp_static(c + Vector3(0, FLOOR_H * 0.45, 4.0), + Vector3(1.2, 0.16, sqrt(FLOOR_H * FLOOR_H + 16.0)), + Vector3(stair_angle, 0, 0), METAL.darkened(0.08), + "EscStairGround_%d_%d" % [bx, bz], "metal") + _ramp_static(c + Vector3(0, FLOOR_H + 1.6, 4.0), + Vector3(1.2, 0.16, sqrt(FLOOR_H * FLOOR_H + 16.0)), + Vector3(-stair_angle, 0, 0), METAL.darkened(0.08), + "EscStair_%d_%d" % [bx, bz], "metal") # Sidewalk life: phone booth on some corners, bike rows by the fronts if _rng.randi() % 4 == 0: _hero("phonebooth", c + Vector3(-half - 0.6, 0.1, -half - 0.6), _rng.randf_range(0.0, 360.0), @@ -506,16 +628,11 @@ func _tower_block(c: Vector3, bx: int, bz: int) -> void: add_child(body) body.global_position = c + Vector3(0, FLOOR_H, 0) body.rotation_degrees.y = yaw - var shape := CollisionShape3D.new() - var bs := BoxShape3D.new() - bs.size = Vector3(dims.x * s, dims.y * s, dims.z * s) - shape.shape = bs - shape.position = Vector3(0, dims.y * s * 0.5, 0) - body.add_child(shape) var inst: Node3D = _kit_scene(key).instantiate() inst.scale = Vector3.ONE * s body.add_child(inst) LevelMaterials.apply_toon_recursive(inst, 0.0) + _fit_mesh_colliders(body, inst) # Rooftop crown billboard both directions (skyline wayfinding) var tw := dims.x * s _box_static(c + Vector3(0, h + 2.4, 0), Vector3(0.6, 4.0, tw - 4.0), METAL, n + "_Crown", "metal") @@ -542,7 +659,9 @@ func _shrine_block(c: Vector3, bx: int, bz: int) -> void: Vector3(BLOCK - 6.0 if horiz else 0.6, 1.4, 0.6 if horiz else BLOCK - 6.0), Color(0.55, 0.55, 0.52), n + "_Wall%d" % i, "brick") # Kitbashed torii (curved kasagi) at the south entry; pillar colliders - var tz := c.z + (BLOCK - 6.0) * 0.5 + # Local south-edge offset. This used to include c.z and was then added to c + # a second time, launching shrine gates/fences far outside their blocks. + var tz := (BLOCK - 6.0) * 0.5 _hero("torii", c + Vector3(0, 0.1, tz), 0.0, Vector3(0.1, 0.1, 0.1), 0.05, "wood") _box_static(c + Vector3(-4.0, 3.5, tz), Vector3(0.9, 7.0, 0.9), VERMILION, n + "_PillarW", "wood").visible = false _box_static(c + Vector3(4.0, 3.5, tz), Vector3(0.9, 7.0, 0.9), VERMILION, n + "_PillarE", "wood").visible = false @@ -610,7 +729,7 @@ func _plaza_block(c: Vector3, bx: int, bz: int) -> void: var a := TAU * float(i) / 4.0 + PI / 4.0 var p := c + Vector3(cos(a), 0, sin(a)) * (BLOCK * 0.32) _box_static(Vector3(p.x, 0.55, p.z), Vector3(4.0, 1.1, 4.0), Color(0.5, 0.48, 0.52), "%s_Pl%d" % [n, i], "brick") - _kit_prop_path("res://assets/props/nature/tree_oak.glb", Vector3(p.x, 1.0, p.z), float(i) * 90.0, 4.5) + _kit_prop_path("res://assets/props/nature/tree_oak.glb", Vector3(p.x, 0.16, p.z), float(i) * 90.0, 4.5) # Kitbashed clock tower on the south-west plaza cell if bx == 4 and bz == 5: _hero("clock", c + Vector3(0, 0.1, 0), 0.0, Vector3(3.2, 10.5, 3.2), 5.25, "concrete") @@ -699,20 +818,10 @@ func _build_rail_line() -> void: fb.set_meta("acoustic_material", "metal") add_child(fb) fb.global_position = Vector3(0, 0, fb_z) # span runs along x, across the avenue - var fb_shape := CollisionShape3D.new() - var fb_box := BoxShape3D.new() - fb_box.size = Vector3(18.0, 0.4, 3.0) - fb_shape.shape = fb_box - fb_shape.position = Vector3(0, 5.5, 0) - fb.add_child(fb_shape) var fb_inst: Node3D = _kit_scene_at("res://assets/props/hero/footbridge.glb").instantiate() fb.add_child(fb_inst) LevelMaterials.apply_toon_recursive(fb_inst, 0.005) - # Walkable stair ramps at both ends (over the stair visuals) - for e in [-1.0, 1.0]: - var ang := rad_to_deg(atan2(5.5, 6.4)) - _ramp_static(Vector3(e * (9.0 + 3.2), 2.65, fb_z), Vector3(2.8, 0.3, 8.6), - Vector3(0, 0, -ang * e), PAVING, "FootbridgeStair_%d" % int(e), "metal").visible = false + _fit_mesh_colliders(fb, fb_inst) # Slide ramps to the deck at the two main-avenue crossings for sx in [-PITCH * 0.5 - 4.0, PITCH * 0.5 + 4.0]: @@ -746,15 +855,140 @@ func _road_piece(key: String, pos: Vector3, yaw_deg: float, sc: Vector3) -> void var _path_cache: Dictionary = {} +var _power_cable_index := 0 +var _power_curve_index := 0 +var _utility_terminal_index := 0 func _kit_scene_at(path: String) -> PackedScene: if not _path_cache.has(path): _path_cache[path] = load(path) return _path_cache[path] +func _cable_segment(from: Vector3, to: Vector3, curve_id: int, + segment_index: int, segment_count: int) -> void: + var direction := to - from + var cable := MeshInstance3D.new() + cable.name = "PowerCable_%d" % _power_cable_index + _power_cable_index += 1 + var mesh := BoxMesh.new() + mesh.size = Vector3(0.055, 0.055, direction.length() + 0.02) + mesh.material = LevelMaterials.flat(INK_COLOR) + cable.mesh = mesh + add_child(cable) + cable.look_at_from_position((from + to) * 0.5, to, Vector3.UP) + cable.set_meta("cable_from", from) + cable.set_meta("cable_to", to) + cable.set_meta("curve_id", curve_id) + cable.set_meta("segment_index", segment_index) + cable.set_meta("segment_count", segment_count) + + +func _utility_terminal(pos: Vector3, terminal_kind: String) -> void: + var marker := Node3D.new() + marker.name = "UtilityTerminal_%04d" % _utility_terminal_index + _utility_terminal_index += 1 + marker.position = pos + marker.set_meta("utility_terminal", true) + marker.set_meta("terminal_kind", terminal_kind) + add_child(marker) + + +func _cable_curve(from: Vector3, to: Vector3, sag: float, + segments: int = 8) -> void: + var curve_id := _power_curve_index + _power_curve_index += 1 + for segment in segments: + var t0 := float(segment) / float(segments) + var t1 := float(segment + 1) / float(segments) + var p0 := from.lerp(to, t0) - Vector3.UP * sin(t0 * PI) * sag + var p1 := from.lerp(to, t1) - Vector3.UP * sin(t1 * PI) * sag + _cable_segment(p0, p1, curve_id, segment, segments) + + +func _utility_pole(pos: Vector3, height: float, pole_id: String) -> void: + var pole := _box_static( + pos + Vector3(0, height * 0.5, 0), + Vector3(0.24, height, 0.24), METAL.darkened(0.35), + "UtilityPole_" + pole_id, "wood") + for mesh in pole.find_children("*", "MeshInstance3D", false, false): + mesh.material_override = LevelMaterials.flat(METAL.darkened(0.35)) + _deco_box(pos + Vector3(0, height, 0), + Vector3(1.35, 0.12, 0.16), METAL.darkened(0.2)) + for lane in 3: + var lane_x := -0.42 + float(lane) * 0.42 + var terminal := pos + Vector3(lane_x, height + 0.12, 0) + _deco_box(terminal, + Vector3(0.10, 0.16, 0.10), Color(0.32, 0.28, 0.34)) + _utility_terminal(terminal, "pole") + # A transformer and grounded service cabinet make these read as utility + # poles rather than unexplained posts holding decorative strings. + _deco_box(pos + Vector3(0, height - 1.25, 0), + Vector3(0.56, 0.82, 0.42), METAL.darkened(0.08)) + _box_static(pos + Vector3(0.38, 0.55, 0), Vector3(0.62, 1.1, 0.48), + METAL.darkened(0.16), "UtilityCabinet_" + pole_id, "metal") + + +func _storefront_service_drop(pole_pos: Vector3, height: float, + front_x: float, z: float, side: float, _service_id: String) -> void: + # Terminate on a facade service mast one metre outside the decorative + # sign/sill layer, then bracket that mast back to the wall. Driving the wire + # directly into the facade let it pass through three protruding sign columns. + var wall_x := front_x - side * 1.05 + var service_height := 7.25 + var pole_terminal := pole_pos + Vector3(side * 0.42, height + 0.12, 0) + var wall_terminal := Vector3(wall_x, service_height, z) + _utility_terminal(wall_terminal, "building_service") + _cable_curve(pole_terminal, wall_terminal, 0.16, 5) + # Junction box and conduit sit on the facade plane and carry the line down + # to street level: a visually meaningful endpoint. + _deco_box(wall_terminal, Vector3(0.16, 0.40, 0.46), + METAL.darkened(0.12)) + _deco_box(Vector3(wall_x, service_height * 0.5, z), + Vector3(0.08, service_height, 0.10), METAL.darkened(0.25)) + _deco_box(Vector3((wall_x + front_x) * 0.5, service_height, z), + Vector3(absf(front_x - wall_x), 0.08, 0.10), METAL.darkened(0.2)) + + +func _build_utility_network() -> void: + var lines := _street_lines() + var pole_zs: Array[float] = [] + for row in GRID: + pole_zs.append(_cell_center(0, row).z) + for line_index in range(0, lines.size(), 2): + var street_x: float = lines[line_index] + if absf(street_x) < 1.0: + continue + var side := 1.0 if line_index % 4 == 0 else -1.0 + # Stay between the road edge and the furthest storefront sign/canopy. + # The previous 1.15 m offset grazed sign columns that project 0.8 m + # from the facade; 0.55 m is the clear sidewalk utility corridor. + var pole_x := street_x + side * (ROAD_W * 0.5 + 0.55) + var height := 11.35 + for row in GRID: + var pole_pos := Vector3(pole_x, 0, pole_zs[row]) + _utility_pole(pole_pos, height, "%d_%d" % [line_index, row]) + if row < GRID - 1: + var next_pos := Vector3(pole_x, 0, pole_zs[row + 1]) + for lane in 3: + var lane_x := -0.42 + float(lane) * 0.42 + _cable_curve( + pole_pos + Vector3(lane_x, height + 0.12, 0), + next_pos + Vector3(lane_x, height + 0.12, 0), + 0.58, 9) + var adjacent_bx := line_index + 1 if side > 0.0 else line_index + if adjacent_bx < 0 or adjacent_bx >= GRID: + continue + var block_type := LAYOUT[row][adjacent_bx] + if block_type != "S" and block_type != "M": + continue + var front_x := street_x + side * (STREET * 0.5) + _storefront_service_drop( + pole_pos, height, front_x, pole_zs[row], side, + "%d_%d" % [line_index, row]) + + func _build_streets() -> void: var lines := _street_lines() - var seg := PITCH - ROAD_W # straight run between intersections var y := 0.02 # Intersections with line markings for x in lines: @@ -776,24 +1010,7 @@ func _build_streets() -> void: continue _road_piece("road-straight", Vector3((x0 + x1) * 0.5, y, z), 90.0, Vector3(ROAD_W, 2.0, x1 - x0)) - # Overhead cable bundles across east-west streets (urban-Japan clutter) - for z in lines: - var x := -CITY_HALF + 24.0 - while x < CITY_HALF - 24.0: - if absf(x) > 14.0: # keep the central avenue sky clear - var h := 9.5 + fmod(absf(x), 3.0) * 0.4 - for s3 in 3: - var t0 := float(s3) / 3.0 - var t1 := float(s3 + 1) / 3.0 - var sag0 := sin(t0 * PI) * 0.7 - var sag1 := sin(t1 * PI) * 0.7 - var za := z - STREET * 0.5 + STREET * t0 - var zb := z - STREET * 0.5 + STREET * t1 - var mid := Vector3(x, h - (sag0 + sag1) * 0.5, (za + zb) * 0.5) - _deco_box(mid, Vector3(0.07, 0.07, zb - za + 0.1), INK_COLOR) - x += 48.0 - else: - x += 48.0 + _build_utility_network() # ── Main avenues: kit streetlights ─────────────────────────────────────────── @@ -801,16 +1018,21 @@ func _build_streets() -> void: func _build_avenues() -> void: var lines := _street_lines() var lamp_scale := 10.0 + # Keep pole bases a full character radius behind the 12 m road mesh. The + # old +0.4 m curb offset let the imported curved base lean into traffic. + var sidewalk_pole_offset := ROAD_W * 0.5 + 1.45 for z in lines: for side: float in [-1.0, 1.0]: # Double-arm lights along the central N-S avenue at every block line - var x := side * (ROAD_W * 0.5 + 0.4) - _box_static(Vector3(x, 3.4, z), Vector3(0.26, 6.8, 0.26), METAL, "LampP_%d_%d" % [int(z), int(side)], "metal") + var x := side * sidewalk_pole_offset + var lamp_z := z + side * sidewalk_pole_offset + _box_static(Vector3(x, 3.4, lamp_z), Vector3(0.26, 6.8, 0.26), METAL, "LampP_%d_%d" % [int(z), int(side)], "metal") _kit_prop_path("res://assets/props/roads/light-curved.glb", - Vector3(x, 0, z), 90.0 if side > 0 else -90.0, lamp_scale) + Vector3(x, 0, lamp_z), 90.0 if side > 0 else -90.0, lamp_scale) # Traffic signal on the opposite diagonal corner of each crossing _kit_prop_path("res://assets/props/roads/light-square-cross.glb", - Vector3(x, 0, z - side * (ROAD_W * 0.5 + 0.8)), 90.0 if side > 0 else -90.0, lamp_scale * 0.8) + Vector3(x, 0, z - side * sidewalk_pole_offset), + 90.0 if side > 0 else -90.0, lamp_scale * 0.8) # Bus stop shelters on alternating blocks along the avenue if int(roundf(z / PITCH)) % 2 == 0: _hero("busstop", Vector3(ROAD_W * 0.5 + 2.0, 0.02, z + 18.0), -90.0, @@ -822,9 +1044,10 @@ func _build_avenues() -> void: continue # Single lights down the central E-W avenue for side: float in [-1.0, 1.0]: - var z2 := side * (ROAD_W * 0.5 + 0.4) + var z2 := side * sidewalk_pole_offset _kit_prop_path("res://assets/props/roads/light-curved.glb", - Vector3(x, 0, z2), 0.0 if side > 0 else 180.0, lamp_scale) + Vector3(x + side * sidewalk_pole_offset, 0, z2), + 0.0 if side > 0 else 180.0, lamp_scale) # ── Parked cars (Kenney Car Kit) ───────────────────────────────────────────── @@ -843,16 +1066,9 @@ func _parked_car(pos: Vector3, yaw_deg: float) -> void: add_child(body) body.global_position = pos body.rotation_degrees.y = yaw_deg + _rng.randf_range(-3.0, 3.0) - var shape := CollisionShape3D.new() - var bs := BoxShape3D.new() - bs.size = Vector3(2.8, 2.6, 5.0) - shape.shape = bs - shape.position = Vector3(0, 1.3, 0) - body.add_child(shape) var inst: Node3D = _kit_scene_at("res://assets/props/cars/" + key + ".glb").instantiate() inst.scale = Vector3.ONE * CAR_SCALE - inst.position.y = 0.55 # wheel contact at ground after scaling body.add_child(inst) var wheel_scene := _kit_scene_at("res://assets/props/cars/wheel-default.glb") for n in inst.find_children("wheel*", "Node3D", true, false): @@ -860,8 +1076,16 @@ func _parked_car(pos: Vector3, yaw_deg: float) -> void: var wheel: Node3D = wheel_scene.instantiate() n.add_child(wheel) LevelMaterials.apply_toon_recursive(inst, 0.0) - # Darken under the map lighting (kit albedo clips) + per-car hue - _tint_recursive(inst, Color(0.62, 0.62, 0.66) * Color(1, 1, 1).lerp(Color.from_hsv(_rng.randf(), 0.6, 1.0), 0.35)) + # Kenney cars carry their paint/window/wheel palette as vertex colors. + # Toon conversion now preserves it; keep the seeded tint subtle enough that + # those authored regions remain legible. + _tint_recursive(inst, Color(0.82, 0.82, 0.86) \ + * Color(1, 1, 1).lerp(Color.from_hsv(_rng.randf(), 0.55, 1.0), 0.22)) + # Imported origins vary by car. Seat every variant by its actual lowest + # rendered point, then derive collision from the grounded visual. + var bounds := _visual_bounds_in(inst, body) + inst.position.y -= bounds.position.y + _fit_mesh_colliders(body, inst) ## Seeded parking lanes hugging the curbs of the N-S streets. @@ -886,8 +1110,17 @@ func _kit_prop_path(path: String, pos: Vector3, yaw_deg: float, scale_f: float) add_child(inst) inst.global_position = pos inst.rotation_degrees.y = yaw_deg + inst.set_meta("source_asset_path", path) # Nature-kit foliage uses vertex-colored materials that the toon swap # mangles (cyan trees) — keep original materials; the faceted low-poly # look already reads cel under this lighting. if not path.contains("/nature/"): LevelMaterials.apply_toon_recursive(inst, 0.0) + # `pos.y` is always the intended support plane (ground, pedestal, or facade + # attachment). Imported origins vary, so align the actual rendered bottom + # to that plane for every kit prop. The old lantern/awning exceptions were + # the source of many inexplicable floating pieces. + var bounds := _visual_bounds_in(inst, self) + inst.global_position.y += pos.y - bounds.position.y + inst.set_meta("support_fitted", true) + inst.set_meta("support_plane_y", pos.y) diff --git a/scenes/maps/procedural_arena/procedural_arena_runtime.gd b/scenes/maps/procedural_arena/procedural_arena_runtime.gd index 337e92f..d9fb216 100644 --- a/scenes/maps/procedural_arena/procedural_arena_runtime.gd +++ b/scenes/maps/procedural_arena/procedural_arena_runtime.gd @@ -18,7 +18,12 @@ func _ready() -> void: LevelEnvironment.add_to(self) LevelMaterials.apply_toon_recursive(self, 0.0) - + # The play volume. The arena is 3 x 5 sectors of 24 m, so 72 x 120; this is + # generous around that and deep enough below it that falling off an edge + # reads as out of bounds rather than as an endless drop. + CombatArea.add_to(self, Vector3(120, 80, 170), Vector3(0, 28, 0)) + + # Multiplayer Spawning var spawner = MultiplayerSpawner.new() spawner.name = "PlayerSpawner" diff --git a/scenes/maps/sakura_crossing/map_meta.cfg b/scenes/maps/sakura_crossing/map_meta.cfg new file mode 100644 index 0000000..bb91d58 --- /dev/null +++ b/scenes/maps/sakura_crossing/map_meta.cfg @@ -0,0 +1,5 @@ +[map] +name="Sakura Crossing" +scene_path="res://scenes/maps/sakura_crossing/sakura_crossing.tscn" +color1=Color(0.984, 0.776, 0.847, 1.0) +color2=Color(0.561, 0.741, 0.918, 1.0) diff --git a/scenes/maps/sakura_crossing/sakura_crossing.tscn b/scenes/maps/sakura_crossing/sakura_crossing.tscn new file mode 100644 index 0000000..1623e75 --- /dev/null +++ b/scenes/maps/sakura_crossing/sakura_crossing.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://csakura7crossg"] + +[ext_resource type="Script" path="res://scenes/maps/sakura_crossing/sakura_crossing_builder.gd" id="1_sc"] + +[node name="SakuraCrossing" type="Node3D"] +script = ExtResource("1_sc") diff --git a/scenes/maps/sakura_crossing/sakura_crossing_builder.gd b/scenes/maps/sakura_crossing/sakura_crossing_builder.gd new file mode 100644 index 0000000..478d4a9 --- /dev/null +++ b/scenes/maps/sakura_crossing/sakura_crossing_builder.gd @@ -0,0 +1,4481 @@ +extends TestLevelBuilder + +## 桜踏切 — Sakura Crossing. +## +## A Japanese suburban level crossing in blossom season, built to the aesthetic +## of the Sakura Crossing reference: flat cel colour on real geometry, a narrow +## pale palette, hue-shifted violet shadow, and screen-space ink taken from the +## curvature of the depth buffer. +## +## ── The layout is three lanes and three crossings ──────────────────────────── +## +## Everything is arranged east-west, because the railway is, and the railway is +## what makes this place a place. North is -Z. +## +## Z = -62 ░ boundary wall, cedar treeline, distant hills ░ +## LANE 1 Z = -47..-10 the shopping street (商店街): two rows of shophouses +## facing a 9 m street, with a service alley behind the +## south row. Tight, roofed, full of stalls and vending +## machines. This is the CQC lane and its rooftops are +## the north half's high ground. +## LANE 2 Z = -7..7 THE RAILWAY. Open, dead flat, fenced both sides. A +## 160 m sightline with almost no cover — crossing it +## anywhere but at a marked point is a decision. +## LANE 3 Z = 9..60 the south side: a verge of cherry trees, a street, a +## house row behind garden walls, then the park and the +## shrine. Open, soft cover, long angles. +## Z = 62 ░ boundary ░ +## +## The lanes connect at exactly three places, and each plays differently, which +## is the whole design: +## +## X = -40 THE FOOTBRIDGE. Deck at 6.2 m spanning the tracks — the only high +## ground that sees the full length of the railway. Its two stairs +## run ALONG the lanes rather than across them, so the climb is +## exposed to the alley on one side and the verge on the other. +## X = 0 THE CROSSING. The signature image and the map's hot zone. At +## grade, so it is the fastest connection and the most contested; the +## barrier booms and the signal cabinets are the only hard cover in +## fourteen metres of open track. +## X = +42 THE STATION. A 1.1 m platform under a canopy, with a ramp up +## from the south street and a stair off the far end. Half cover, a +## roof worth fighting over, and the only crossing that is a room — +## so it plays as a hold where the crossing plays as a dash. +## +## ── Why the railway is at grade and not on an embankment ───────────────────── +## +## An embankment would be easier to build and would give free verticality, but a +## 踏切 IS the level crossing — barriers, an alarm, an X-shaped sign and a road +## running straight over the rails. Raise the track and none of that can exist, +## and the map stops being this place. Verticality comes from the footbridge, +## the shop roofs and the station canopy instead, which is where it comes from +## in the reference too. + +const HALF_X := 118.0 # playable half-extent, east-west +const HALF_Z := 88.0 # playable half-extent, north-south + +# ── The railway turns ──────────────────────────────────────────────────────── +# +# It runs dead straight through the crossing, the footbridge and the station — +# it has to, those are all built square to it — and then swings north-east once +# it is clear of them. The turn is what stops the east half of the map being the +# west half again: it opens a large wedge of land south of the line, which is +# where the housing district goes, and it gives the whole east end a different +# geometry to fight in. It also means the 160 m sightline down the track now +# ENDS somewhere, instead of running out of the map in both directions. +const CURVE_START_X := 62.0 # clear of the station's east ramp +const CURVE_RADIUS := 60.0 +const CURVE_SWEEP := 55.0 # degrees of turn, toward -Z + +const RAIL_HALF := 7.0 # railway corridor half-width +const RAIL_GAUGE := 1.435 # metres between rail heads, because it is +const TRACK_OFFSET := 2.6 # each track's centre, either side of Z = 0 +const BALLAST_H := 0.16 +const CROSS_Y := 0.36 # the crossing deck, flush with the rail heads + +const ROAD_HALF := 4.5 # the main street, north-south through X = 0 +const PAVE_W := 2.6 + +# ── Surface heights ────────────────────────────────────────────────────────── +# +# Every ground plane in the map gets an explicit height, and they are all +# different on purpose. Two coplanar slabs z-fight, and the fight is worst +# exactly where two roads cross — which here is the middle of the map. Ordering +# them also encodes which surface wins: the main street is drawn over the +# east-west streets because it is the through route, and grass is drawn under +# both because a lawn does not run across a road. +const Y_ROAD_EW := 0.060 +const Y_ROAD_NS := 0.075 +const Y_GRASS := 0.100 +const Y_PAVE := 0.200 + +const SHOP_ST_Z := -30.0 # centreline of the shopping street +const SHOP_ST_HALF := 4.5 +const SOUTH_ST_Z := 20.0 # centreline of the south residential street +const SOUTH_ST_HALF := 4.5 +const ALLEY_Z := -11.2 # service alley behind the south shop row +const ALLEY_W := 3.0 + +# ── The east district ──────────────────────────────────────────────────────── +# The residential block in the wedge the railway's turn opens up, with its own +# north-south and east-west streets. +const EAST_DIST_X0 := 10.0 +const EAST_DIST_X1 := 108.0 +const EAST_DIST_Z1 := 80.0 +const EAST_ST_X := 64.0 # the north-south street through it +const EAST_ST_Z := 54.0 # the east-west street it feeds +const EAST_ST_HALF := 4.5 + +## The shopping street stops short of the curve — past this the railway is +## swinging north across where the street would have run. +const SHOP_ST_X1 := 52.0 + +const BRIDGE_X := -40.0 +const BRIDGE_SPAN := 22.0 +const BRIDGE_DECK_Y := 6.2 +const STATION_X := 42.0 +const PARK_Z := 50.0 +const SHRINE_X := -22.0 # the shrine axis, deliberately NOT the main street + +const FLOOR_H := 3.4 # a shophouse storey + +## How far the ground reaches beyond the boundary walls. +## +## A player who gets over a wall — and this game has a grapple and a dash — +## used to run out of collision twenty metres later and fall out of the world. +## The combat volume and its five-second timer are the RULE that says they have +## left; the ground out here is what stops leaving from being a bug. They land +## on open country, see the warning, and have five seconds to get back. +const OUTFIELD := 300.0 + +## ── The outfield is three rings, in this order ─────────────────────────────── +## +## town → suburb → hills, and nothing from one ring is allowed into another. +## +## They used to be laid on top of each other: the hills were scattered across +## the whole margin with no idea where the city was, so buildings stood +## half-buried in hillsides and roads climbed into them and stopped. A road that +## disappears into a hill is the clearest possible way to tell a player the +## world is fake. +const CITY_REACH := 150.0 # how far the built-up area runs past the wall +const HILL_START := 178.0 # hills begin beyond it, never inside it + +var _rng := RandomNumberGenerator.new() +var _spawn_points: Array[Vector3] = [] +var _mesh_cache: Dictionary = {} +var _decor: Node3D # render-only children, kept out of physics +var M := {} + +## When set, everything built goes under this node instead of the level root. +## +## It exists so a whole assembly can be built in LOCAL coordinates and then +## placed and turned as one — see `_house`, which is written entirely around a +## street to its north and gets its mirror image for free by being built under a +## pivot rotated 180°. Hand-mirroring sixty offsets and half a dozen rotations +## is the kind of edit that looks right and is wrong in one place. +var _group: Node3D = null + + +func _attach(n: Node) -> void: + if _group != null: + _group.add_child(n) + else: + add_child(n) + + +func _build_geometry() -> void: + _rng.seed = 0x5AC124 # deterministic: the map is the same every match + _decor = Node3D.new() + _decor.name = "Decor" + add_child(_decor) + + _build_environment() + _build_ground() + _build_railway() + _build_crossing() + _build_shopping_street() + _build_south_side() + _build_footbridge() + _build_station() + _build_park() + _build_street_furniture() + _build_cover() + _build_boundary() + _build_petals() + + if _spawn_points.is_empty(): + _spawn_points.append(Vector3(0, 2, 0)) + _publish_spawn_markers() + + # The play volume follows the visible wall centreline exactly. The old eight + # metre allowance let a player stand outside the wall without being warned. + # genuinely left — over a wall with the grapple, or off the edge of the + # world. Below it there is nothing to land on, which is exactly the case + # this is here to catch. + CombatArea.add_to(self, + Vector3(HALF_X * 2.0, 60.0, HALF_Z * 2.0), + Vector3(0, 22.0, 0)) + + +## Mirror the spawn list into real nodes in the `spawn_points` group. +## +## The list itself is a private array, which is all _spawn_player needs — but it +## means nothing outside this script can see where a player will appear. +## map_preview_capture photographs a map from its spawns for the level-select +## card, and with no markers it fell back to a raised corner view scaled to the +## map's bounds. The markers cost nothing and make the spawn layout inspectable +## in the remote scene tree as well. +func _publish_spawn_markers() -> void: + var holder := Node3D.new() + holder.name = "SpawnPoints" + add_child(holder) + for i in range(_spawn_points.size()): + var m := Marker3D.new() + m.name = "Spawn%02d" % i + m.position = _spawn_points[i] + m.add_to_group("spawn_points") + holder.add_child(m) + + +func _build_environment() -> void: + # "sakura" carries the pale hand-painted sky, the four-light anime rig and + # the reduced ambient the cel ramp needs. See LevelEnvironment. + LevelEnvironment.add_to(self, "sakura") + + +# ── Primitives ─────────────────────────────────────────────────────────────── +# +# Meshes are cached by shape and the material goes on the INSTANCE, so a +# thousand sleepers or fence posts share one mesh resource. Anything that does +# not need to be collided with goes under Decor and never becomes a body — a +# level crossing has a great many small objects, and turning all of them into +# physics is how a stylised map ends up with a worse frame time than a realistic +# one. + +func _box_mesh(size: Vector3) -> BoxMesh: + var key := "b%.3f,%.3f,%.3f" % [size.x, size.y, size.z] + if _mesh_cache.has(key): + return _mesh_cache[key] + var m := BoxMesh.new() + m.size = size + _mesh_cache[key] = m + return m + + +func _cyl_mesh(bottom_r: float, top_r: float, height: float, segments: int = 8) -> CylinderMesh: + var key := "c%.3f,%.3f,%.3f,%d" % [bottom_r, top_r, height, segments] + if _mesh_cache.has(key): + return _mesh_cache[key] + var m := CylinderMesh.new() + m.bottom_radius = bottom_r + m.top_radius = top_r + m.height = height + m.radial_segments = segments + m.rings = 1 + _mesh_cache[key] = m + return m + + +func _sphere_mesh(radius: float, segments: int = 7, rings: int = 4) -> SphereMesh: + var key := "s%.3f,%d,%d" % [radius, segments, rings] + if _mesh_cache.has(key): + return _mesh_cache[key] + var m := SphereMesh.new() + m.radius = radius + m.height = radius * 2.0 + m.radial_segments = segments + m.rings = rings + _mesh_cache[key] = m + return m + + +func _prism_mesh(size: Vector3) -> PrismMesh: + var key := "p%.3f,%.3f,%.3f" % [size.x, size.y, size.z] + if _mesh_cache.has(key): + return _mesh_cache[key] + var m := PrismMesh.new() + m.size = size + _mesh_cache[key] = m + return m + + +## Render-only mesh. Goes under Decor, has no body, costs one draw call. +func _deco(mesh: Mesh, mat: Material, pos: Vector3, rot: Vector3 = Vector3.ZERO, + parent: Node = null) -> MeshInstance3D: + var mi := MeshInstance3D.new() + mi.mesh = mesh + mi.material_override = mat + mi.position = pos + if rot != Vector3.ZERO: + mi.rotation_degrees = rot + var host: Node = parent + if host == null: + host = _group if _group != null else _decor + host.add_child(mi) + return mi + + +## A solid box: mesh plus a matching box collider. +func _solid(pos: Vector3, size: Vector3, mat: Material, acoustic: String = "concrete", + rot: Vector3 = Vector3.ZERO, node_name: String = "") -> StaticBody3D: + var body := StaticBody3D.new() + body.name = node_name if node_name != "" else "S" + body.position = pos + if rot != Vector3.ZERO: + body.rotation_degrees = rot + if acoustic != "": + body.set_meta("acoustic_material", acoustic) + _attach(body) + var shape := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = size + shape.shape = box + body.add_child(shape) + var mi := MeshInstance3D.new() + mi.mesh = _box_mesh(size) + mi.material_override = mat + body.add_child(mi) + return body + + +## A collider with no mesh. For barriers whose VISIBLE form is a set of thin +## members — a fence, a railing — where one box the size of the whole barrier is +## the right physics and completely the wrong picture. +func _blocker(pos: Vector3, size: Vector3, acoustic: String = "metal") -> StaticBody3D: + var body := StaticBody3D.new() + body.name = "B" + body.position = pos + body.set_meta("acoustic_material", acoustic) + _attach(body) + var shape := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = size + shape.shape = box + body.add_child(shape) + return body + + +## A solid cylinder — posts, poles, trunks, bollards. +func _post(pos: Vector3, radius: float, height: float, mat: Material, + acoustic: String = "metal", segments: int = 8) -> StaticBody3D: + var body := StaticBody3D.new() + body.name = "P" + body.position = pos + body.set_meta("acoustic_material", acoustic) + _attach(body) + var shape := CollisionShape3D.new() + var cyl := CylinderShape3D.new() + cyl.radius = radius + cyl.height = height + shape.shape = cyl + body.add_child(shape) + var mi := MeshInstance3D.new() + mi.mesh = _cyl_mesh(radius, radius, height, segments) + mi.material_override = mat + body.add_child(mi) + return body + + +## Flat ground slab. Collides (you walk on it) but is only ever a fifth of a +## metre thick, so its sides are never seen. +func _slab(centre: Vector2, size: Vector2, y: float, mat: Material, + acoustic: String = "concrete", thickness: float = 0.2) -> StaticBody3D: + return _solid( + Vector3(centre.x, y - thickness * 0.5, centre.y), + Vector3(size.x, thickness, size.y), mat, acoustic) + + +## A painted line, a tactile strip, a drift of fallen blossom — anything laid +## flat on the ground. Render-only, and lifted a centimetre so it never +## z-fights the slab under it. +func _paint(centre: Vector2, size: Vector2, y: float, mat: Material) -> void: + _deco(_box_mesh(Vector3(size.x, 0.02, size.y)), mat, + Vector3(centre.x, y + 0.012, centre.y)) + + +## Split the run [a, b] around a list of [lo, hi] gaps. Used for every kerb, +## pavement and fence in the map: they all have to stop where a road, a crossing +## or a platform cuts through them, and a kerb that runs across a road is the +## sort of mistake that makes a whole street stop reading. +func _segments(a: float, b: float, gaps: Array) -> Array[Vector2]: + var sorted: Array = gaps.duplicate() + sorted.sort_custom(func(p, q): return p.x < q.x) + var out: Array[Vector2] = [] + var cursor := a + for g in sorted: + if g.x > cursor: + out.append(Vector2(cursor, minf(g.x, b))) + cursor = maxf(cursor, g.y) + if cursor >= b: + break + if cursor < b: + out.append(Vector2(cursor, b)) + var clean: Array[Vector2] = [] + for s in out: + if s.y - s.x > 0.5: + clean.append(s) + return clean + + +# ── Materials ──────────────────────────────────────────────────────────────── +# +# Named once here rather than inline, because the palette discipline only works +# if the same surface is the same colour everywhere. The ramp and shadow-tint +# choices ARE the art direction — see the note on each group. + +func _build_materials() -> void: + var P := SakuraPalette + M = { + # Ground. Four bands rather than three: the road and the pavement are the + # largest continuous surfaces in the map, and a three-step ramp puts a + # visible hard bar across them wherever the sun rakes. + "road": LevelMaterials.cel(P.ROAD, LevelMaterials.RAMP_4, P.TINT), + "road_dark": LevelMaterials.cel(P.ROAD_DARK, LevelMaterials.RAMP_4, P.TINT), + "pave": LevelMaterials.cel(P.SIDEWALK, LevelMaterials.RAMP_4, P.TINT), + "pave_alt": LevelMaterials.cel(P.SIDEWALK_ALT, LevelMaterials.RAMP_4, P.TINT), + "kerb": LevelMaterials.cel(P.CURB, LevelMaterials.RAMP_3, P.TINT, "trim"), + "line_white": LevelMaterials.unlit(P.LINE_WHITE), + "line_yellow": LevelMaterials.unlit(P.LINE_YELLOW), + "tactile": LevelMaterials.cel(P.TACTILE, LevelMaterials.RAMP_2, P.TINT_WARM), + # The dots sit ON the yellow strip, so they are a shade deeper than it — + # at this size the shading alone cannot separate them from their ground. + "tactile_dot": LevelMaterials.cel(P.YELLOW_DEEP, LevelMaterials.RAMP_2, P.TINT_WARM), + "drain_dark": LevelMaterials.cel(P.DRAIN, LevelMaterials.RAMP_2, P.TINT_COOL), + "gravel": LevelMaterials.cel(P.GRAVEL, LevelMaterials.RAMP_3, P.TINT), + "ballast": LevelMaterials.cel(P.BALLAST, LevelMaterials.RAMP_3, P.TINT_COOL), + "dirt": LevelMaterials.cel(P.DIRT, LevelMaterials.RAMP_3, P.TINT_WARM), + "dirt_dark": LevelMaterials.unlit(P.STONE_WARM.darkened(0.16)), + "grass": LevelMaterials.cel(P.GRASS, LevelMaterials.RAMP_3, P.TINT_FOLIAGE), + + # Concrete and stone. + "concrete": LevelMaterials.cel(P.CONCRETE, LevelMaterials.RAMP_3, P.TINT, "wall"), + "concrete_mid": LevelMaterials.cel(P.CONCRETE_MID, LevelMaterials.RAMP_3, P.TINT), + "concrete_dark": LevelMaterials.cel(P.CONCRETE_DARK, LevelMaterials.RAMP_3, P.TINT), + "trim_mat": LevelMaterials.cel(P.TRIM, LevelMaterials.RAMP_3, P.TINT), + "stone": LevelMaterials.cel(P.STONE, LevelMaterials.RAMP_3, P.TINT), + "stone_dark": LevelMaterials.cel(P.STONE_DARK, LevelMaterials.RAMP_3, P.TINT), + # Water is DRAWN, not lit. A cel painter puts a flat block in a pond; + # a lit surface at this scale reads as ice. + "water": LevelMaterials.unlit(P.WATER), + "wall_cream": LevelMaterials.cel(P.WALL_CREAM, LevelMaterials.RAMP_3, + P.TINT, "wall"), + "stone_warm": LevelMaterials.cel(P.STONE_WARM, LevelMaterials.RAMP_3, P.TINT_WARM), + + # Metal. The coolest shadow tint in the map — this is what makes rail, + # fence and shutter read as metal in a renderer with no specular at all. + "metal": LevelMaterials.cel(P.METAL, LevelMaterials.RAMP_3, P.TINT_COOL), + "metal_dark": LevelMaterials.cel(P.METAL_DARK, LevelMaterials.RAMP_3, P.TINT_COOL), + "metal_warm": LevelMaterials.cel(P.METAL_WARM, LevelMaterials.RAMP_3, P.TINT_WARM), + "rail": LevelMaterials.cel(P.RAIL_METAL, LevelMaterials.RAMP_2, P.TINT_COOL), + "rail_head": LevelMaterials.cel(P.RAIL_HEAD, LevelMaterials.RAMP_2, P.TINT_COOL), + "sleeper": LevelMaterials.cel(P.SLEEPER, LevelMaterials.RAMP_2, P.TINT), + "shutter": LevelMaterials.cel(P.SHUTTER, LevelMaterials.RAMP_3, P.TINT_COOL, "panel"), + "cabinet": LevelMaterials.cel(P.CABINET, LevelMaterials.RAMP_3, P.TINT, "panel"), + "cabinet_top": LevelMaterials.cel(P.CABINET_TOP, LevelMaterials.RAMP_3, P.TINT), + "mirror_back": LevelMaterials.cel(P.MIRROR_BACK, LevelMaterials.RAMP_2, P.TINT_WARM), + + # Timber. Warm tint: a cedar post pushed the full way to violet goes + # grey, and the shrine and the shopfronts are most of what says "here" + # rather than just "a town". + "wood": LevelMaterials.cel(P.SHRINE_WOOD, LevelMaterials.RAMP_3, P.TINT_WARM), + "wood_dark": LevelMaterials.cel(P.SHRINE_WOOD_DARK, LevelMaterials.RAMP_3, P.TINT_WARM), + "rope": LevelMaterials.cel(P.ROPE, LevelMaterials.RAMP_2, P.TINT_WARM), + + # Roofs. + "roof_slate": LevelMaterials.cel(P.ROOF_SLATE, LevelMaterials.RAMP_3, P.TINT_COOL), + "roof_shrine": LevelMaterials.cel(P.SHRINE_ROOF, LevelMaterials.RAMP_3, P.TINT_COOL), + + # Blossom. The high-key ramp, and it is not optional: on the standard + # ramp the shadow side of a canopy drops to 0.36 and a cherry tree turns + # into a storm cloud. SOFT3's darkest stop is 0.67, so the tree stays a + # pale pink mass whichever way it faces — which is the single most + # recognisable thing in the reference. + "blossom": LevelMaterials.cel(P.BLOSSOM, LevelMaterials.RAMP_SOFT3, P.TINT_WARM), + "blossom_light": LevelMaterials.cel(P.BLOSSOM_LIGHT, LevelMaterials.RAMP_SOFT, P.TINT_WARM), + "blossom_deep": LevelMaterials.cel(P.BLOSSOM_DEEP, LevelMaterials.RAMP_SOFT3, P.TINT_WARM), + "petal_ground": LevelMaterials.unlit(P.PETAL_DEEP), + "trunk": LevelMaterials.cel(P.TRUNK, LevelMaterials.RAMP_3, P.TINT_WARM), + "trunk_dark": LevelMaterials.cel(P.TRUNK_DARK, LevelMaterials.RAMP_3, P.TINT_WARM), + "leaf": LevelMaterials.cel(P.LEAF, LevelMaterials.RAMP_3, P.TINT_FOLIAGE), + "leaf_deep": LevelMaterials.cel(P.LEAF_DEEP, LevelMaterials.RAMP_3, P.TINT_FOLIAGE), + "cedar": LevelMaterials.cel(P.CEDAR, LevelMaterials.RAMP_3, P.TINT_FOLIAGE), + + # Accents. Two bands, because these are small saturated objects whose + # silhouette does the work, and extra steps on them only muddy the hue. + "red": LevelMaterials.cel(P.RED, LevelMaterials.RAMP_2, P.TINT_WARM), + "torii": LevelMaterials.cel(P.TORII, LevelMaterials.RAMP_2, P.TINT_WARM), + "yellow": LevelMaterials.cel(P.GATE_YELLOW, LevelMaterials.RAMP_2, P.TINT_WARM), + "black": LevelMaterials.cel(P.GATE_BLACK, LevelMaterials.RAMP_2, P.TINT_COOL), + "teal": LevelMaterials.cel(P.TEAL, LevelMaterials.RAMP_2, P.TINT_COOL), + "blue": LevelMaterials.cel(P.BLUE, LevelMaterials.RAMP_2, P.TINT_COOL), + "vend_white": LevelMaterials.cel(P.VEND_WHITE, LevelMaterials.RAMP_3, P.TINT, "panel"), + "vend_red": LevelMaterials.cel(P.VEND_RED, LevelMaterials.RAMP_2, P.TINT_WARM), + "vend_teal": LevelMaterials.cel(P.VEND_TEAL, LevelMaterials.RAMP_2, P.TINT_COOL), + + # Glass and lit panels are DRAWN, not lit. A cel painter puts a flat + # block of colour in a window; shading it is what makes stylised + # architecture read as a 3D model of a building. + "glass": LevelMaterials.unlit(P.GLASS), + "glass_dark": LevelMaterials.unlit(P.GLASS_DARK), + "lantern": LevelMaterials.unlit(P.LANTERN_LIT), + "signal_off": LevelMaterials.unlit(P.SIGNAL_OFF), + "signal_red": LevelMaterials.unlit(P.SIGNAL_RED), + "ink": LevelMaterials.unlit(P.INK), + "white_board": LevelMaterials.unlit(P.WALL_WHITE), + + # Distant landscape. Unlit and pale: a hill two hundred metres out is + # atmosphere, and lighting it would put a band edge across the horizon. + "hill": LevelMaterials.unlit(P.HILL), + "hill_far": LevelMaterials.unlit(P.HILL_FAR), + } + + +func _wall_mat(i: int) -> Material: + return LevelMaterials.cel( + SakuraPalette.WALLS[i % SakuraPalette.WALLS.size()], + LevelMaterials.RAMP_3, SakuraPalette.TINT, "wall") + + +func _roof_mat(i: int) -> Material: + return LevelMaterials.cel( + SakuraPalette.ROOFS[i % SakuraPalette.ROOFS.size()], + LevelMaterials.RAMP_3, SakuraPalette.TINT_COOL) + + +# ── Trim and lettering ─────────────────────────────────────────────────────── +# +# The second pass over this map, and the one that made it stop reading as a +# massing study. The first version had correct forms and correct colour and +# almost no SMALL structure: no sills, no downpipes, no meters, no manholes, and +# not one character of type anywhere in a Japanese suburb, which is a place made +# largely of lettering. +# +# Everything in this section is render-only and most of it is a box. That is the +# point — none of it needs to be clever, it needs to be THERE, because the third +# tier of visual structure is what the eye uses to judge whether a place is real. + + +## A sign board with lettering on it: the coloured panel, sized to its text, and +## the text a centimetre proud so it never z-fights the panel. +func _sign_board(centre: Vector3, facing: float, text: String, text_h: float, + board: Color, ink: Color, pad: float = 0.32) -> void: + var tw: float = SakuraSignage.text_width(text, text_h) + var yaw := 0.0 if facing > 0.0 else 180.0 + _deco(_box_mesh(Vector3(tw + pad * 2.0, text_h + pad, 0.09)), + LevelMaterials.unlit(board), centre) + SakuraSignage.label(_decor, text, text_h, ink, + centre + Vector3(0, 0, facing * 0.06), Vector3(0, yaw, 0)) + + +## A projecting side sign (袖看板): the box that stands off the wall, and the +## same vertical line of characters on both of its faces. These are what give a +## Japanese shopping street its vertical rhythm, and a row without them reads as +## a terrace of flat shopfronts. +func _vertical_sign(x: float, top_y: float, z: float, facing: float, + text: String, board: Color, ink: Color) -> void: + var ch := 0.30 + var height: float = float(text.length()) * ch * 1.34 + 0.28 + var depth := 0.9 + var centre := Vector3(x, top_y - height * 0.5, z + facing * depth * 0.5) + _deco(_box_mesh(Vector3(0.1, height, depth)), LevelMaterials.unlit(board), centre) + # Both faces: the sign exists to be read from up and down the street. + for s in [-1.0, 1.0]: + SakuraSignage.vertical_label(_decor, text, ch, ink, + Vector3(x + s * 0.06, top_y - 0.24, centre.z), + Vector3(0, 90.0 * s, 0)) + + +## A window sill and its lintel. Two boxes, and between them they are most of +## what stops a punched window reading as a sticker. +func _window_trim(centre: Vector3, w: float, h: float, facing: float, + mat: Material) -> void: + _deco(_box_mesh(Vector3(w + 0.22, 0.10, 0.20)), mat, + centre + Vector3(0, -h * 0.5 - 0.05, facing * 0.06)) + _deco(_box_mesh(Vector3(w + 0.18, 0.08, 0.14)), mat, + centre + Vector3(0, h * 0.5 + 0.04, facing * 0.04)) + + +## A rainwater downpipe with its brackets and a shoe at the bottom. Runs the full +## height of the building at a corner — a long vertical line on a broad flat +## facade, which is exactly the kind of small structure the first pass had none +## of. +func _downpipe(x: float, z: float, top: float, facing: float, mat: Material) -> void: + _deco(_cyl_mesh(0.055, 0.055, top - 0.3, 6), mat, + Vector3(x, (top - 0.3) * 0.5 + 0.2, z + facing * 0.08)) + var brackets := maxi(2, int(top / 2.2)) + for i in range(brackets): + _deco(_box_mesh(Vector3(0.16, 0.05, 0.10)), mat, + Vector3(x, 0.8 + float(i) * (top - 1.2) / float(brackets), z + facing * 0.03)) + # The shoe kicks out at the bottom, which is the bit you actually notice. + _deco(_box_mesh(Vector3(0.11, 0.30, 0.11)), mat, + Vector3(x, 0.24, z + facing * 0.16), Vector3(facing * 22.0, 0, 0)) + + +## A wall-mounted air-conditioning condenser and its pipe run. Ubiquitous, and +## it breaks up an upper storey the way nothing authored ever quite does. +func _ac_unit(pos: Vector3, facing: float) -> void: + _deco(_box_mesh(Vector3(0.78, 0.56, 0.30)), M["metal"], + pos + Vector3(0, 0, facing * 0.18)) + _deco(_cyl_mesh(0.20, 0.20, 0.06, 10), M["metal_dark"], + pos + Vector3(0, 0, facing * 0.34), Vector3(90, 0, 0)) + # Bracket under it, and the pipe run going back into the wall. + _deco(_box_mesh(Vector3(0.86, 0.06, 0.34)), M["metal_dark"], + pos + Vector3(0, -0.31, facing * 0.18)) + _deco(_box_mesh(Vector3(0.09, 0.9, 0.09)), M["concrete_dark"], + pos + Vector3(0.46, -0.6, facing * 0.06)) + + +## The meter cupboard by the door. Small, boxy, always slightly the wrong colour +## from the wall it is on. +func _meter_box(pos: Vector3, facing: float) -> void: + _deco(_box_mesh(Vector3(0.42, 0.56, 0.18)), M["cabinet"], + pos + Vector3(0, 0, facing * 0.11)) + _deco(_box_mesh(Vector3(0.46, 0.06, 0.22)), M["cabinet_top"], + pos + Vector3(0, 0.31, facing * 0.11)) + + +## An upper-storey balcony: a slab, a solid front panel and a thin capping rail. +func _balcony(cx: float, y: float, z: float, w: float, facing: float, + mat: Material) -> void: + var d := 1.05 + _deco(_box_mesh(Vector3(w, 0.12, d)), mat, Vector3(cx, y, z + facing * d * 0.5)) + _deco(_box_mesh(Vector3(w, 0.86, 0.08)), mat, Vector3(cx, y + 0.49, z + facing * d)) + _deco(_box_mesh(Vector3(w + 0.06, 0.07, 0.16)), M["metal"], + Vector3(cx, y + 0.94, z + facing * d)) + for s in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(0.08, 0.86, d)), mat, + Vector3(cx + s * w * 0.5, y + 0.49, z + facing * d * 0.5)) + + +## A manhole cover: a dark disc with a raised rim. Godot has no ring primitive, +## so it is two discs — the trick is that the rim is a hair PROUD of the road, +## because a manhole that sits flush reads as a painted circle. +func _manhole(x: float, z: float, y: float) -> void: + _deco(_cyl_mesh(0.36, 0.36, 0.04, 12), M["metal_dark"], Vector3(x, y + 0.02, z)) + _deco(_cyl_mesh(0.30, 0.30, 0.05, 12), M["drain_dark"], Vector3(x, y + 0.035, z)) + + +## A gully grate at the kerb line, drawn as a dark recess with bars across it. +func _drain_grate(x: float, z: float, y: float, along_x: bool) -> void: + var size := Vector3(0.7, 0.05, 0.34) if along_x else Vector3(0.34, 0.05, 0.7) + _deco(_box_mesh(size), M["drain_dark"], Vector3(x, y + 0.015, z)) + for i in range(5): + var t := (float(i) - 2.0) * 0.11 + var bar := Vector3(0.66, 0.03, 0.035) if along_x else Vector3(0.035, 0.03, 0.66) + var off := Vector3(0, 0, t) if along_x else Vector3(t, 0, 0) + _deco(_box_mesh(bar), M["metal_dark"], Vector3(x, y + 0.035, z) + off) + + +## Tactile paving (点字ブロック), as actual DOTS. +## +## The first pass drew these as a flat yellow rectangle, which is the one place +## in the map where a real, strongly patterned surface was replaced by a block of +## colour — and it is a surface every player walks over at the crossing and on +## the platform. A MultiMesh of a few hundred truncated cones costs one draw call +## and is the single highest detail-per-vertex thing in the level. +func _tactile_dots(centre: Vector2, size: Vector2, y: float) -> void: + var pitch := 0.15 + var nx := maxi(1, int(size.x / pitch)) + var nz := maxi(1, int(size.y / pitch)) + var mm := MultiMesh.new() + mm.transform_format = MultiMesh.TRANSFORM_3D + mm.mesh = _cyl_mesh(0.052, 0.040, 0.035, 6) + mm.instance_count = nx * nz + var i := 0 + for ix in range(nx): + for iz in range(nz): + var p := Vector3( + centre.x + (float(ix) - float(nx - 1) * 0.5) * pitch, + y + 0.018, + centre.y + (float(iz) - float(nz - 1) * 0.5) * pitch) + mm.set_instance_transform(i, Transform3D(Basis.IDENTITY, p)) + i += 1 + var mmi := MultiMeshInstance3D.new() + mmi.multimesh = mm + mmi.material_override = M["tactile_dot"] + _decor.add_child(mmi) + + +## Lettering painted on the road. Laid flat, and always read from the direction +## of travel, so `yaw` points it at the driver rather than at the map's north. +func _road_text(text: String, x: float, z: float, y: float, height: float, + colour: Color, yaw: float) -> void: + # `inked: false` — this is paint on tarmac, not an object. See + # SakuraSignage.label for what inking it did to the letterforms. + SakuraSignage.label(_decor, text, height, colour, + Vector3(x, y + 0.014, z), Vector3(-90, yaw, 0), false) + + +## A zebra crossing: bars across the carriageway, with a gap between each. +func _zebra(centre: Vector2, road_w: float, depth: float, along_x: bool, + y: float) -> void: + var bar := 0.55 + var gap := 0.45 + var n := int(road_w / (bar + gap)) + for i in range(n): + var t := (float(i) - float(n - 1) * 0.5) * (bar + gap) + if along_x: + _paint(Vector2(centre.x + t, centre.y), Vector2(bar, depth), y, M["line_white"]) + else: + _paint(Vector2(centre.x, centre.y + t), Vector2(depth, bar), y, M["line_white"]) + + +## A large painted advertisement on the exposed flank wall of a terrace. +## +## Set vertically, because that is how a tall narrow gable is used and because +## 縦書き on a wall is one of the most immediately Japanese things in the map. A +## painted panel behind the type, a hairline border, and the whole thing sitting +## a few centimetres proud so the ink pass draws it as signage rather than as a +## stain on the render. +func _wall_sign(x: float, z: float, h: float, d: float, facing_x: float, + idx: int, front_facing: float = -1.0) -> void: + var text: String = SakuraSignage.WALL_ADS[idx % SakuraSignage.WALL_ADS.size()] + var ch: float = minf(0.95, (h - 2.4) / float(text.length()) / 1.34) + var panel_h: float = float(text.length()) * ch * 1.34 + 0.7 + var panel_w := ch + 0.7 + var yaw := 90.0 * facing_x + var col: Color = SakuraPalette.DRINKS[(idx * 7) % SakuraPalette.DRINKS.size()] + var ink: Color = SakuraPalette.INK if col.get_luminance() > 0.42 \ + else SakuraPalette.WALL_WHITE + var top := h - 0.9 + # Toward the FRONT of the building, following its shopfront. Fixed at + # `z - d*0.22` the far row's signs sat on the back half of each gable, out + # of sight of the street they were advertising to. + var cz := z + front_facing * d * 0.22 + _deco(_box_mesh(Vector3(0.08, panel_h, panel_w)), LevelMaterials.unlit(col), + Vector3(x + facing_x * 0.05, top - panel_h * 0.5, cz)) + _deco(_box_mesh(Vector3(0.05, panel_h + 0.22, panel_w + 0.22)), + LevelMaterials.unlit(SakuraPalette.WALL_WHITE), + Vector3(x + facing_x * 0.03, top - panel_h * 0.5, cz)) + SakuraSignage.vertical_label(_decor, text, ch, ink, + Vector3(x + facing_x * 0.10, top - 0.55, cz), Vector3(0, yaw, 0)) + + +## The street name plate bolted to a pole, lettered on both faces. +func _street_plate(pos: Vector3, text: String, yaw: float) -> void: + var h := 0.17 + var tw: float = SakuraSignage.text_width(text, h) + _deco(_box_mesh(Vector3(tw + 0.24, h + 0.16, 0.05)), LevelMaterials.unlit( + SakuraPalette.WALL_WHITE), pos, Vector3(0, yaw, 0)) + for s in [-1.0, 1.0]: + SakuraSignage.label(_decor, text, h, SakuraPalette.INK, + pos + Vector3(sin(deg_to_rad(yaw)) * 0.04 * s, 0, cos(deg_to_rad(yaw)) * 0.04 * s), + Vector3(0, yaw + (0.0 if s > 0.0 else 180.0), 0)) + + +# ── Ground ─────────────────────────────────────────────────────────────────── + +func _build_ground() -> void: + # The base plate. Everything else is laid on top, so it is the one surface + # that has to be continuous. It reaches well past the boundary walls — see + # _build_outfield for why the edge of the world is not the edge of the + # collision. + # Grass, not the warm `dirt` it used to be. Once the map grew, the plate was + # the largest visible surface in every aerial and a pale ochre read as sand + # — the town looked like it had been dropped on a beach. Everything actually + # built sits above it, so this is only ever seen in the gaps, and turf is + # what is in the gaps of a Japanese suburb. + _slab(Vector2(0, 0), Vector2(HALF_X * 2 + OUTFIELD * 2, HALF_Z * 2 + OUTFIELD * 2), + 0.0, M["grass"], "grass", 1.2) + + # Roads: one north-south main street through the crossing, two east-west + # streets. The main street is drawn last and highest because it is the + # through route — see the note on the Y_ constants. + # The shopping street stops at SHOP_ST_X1: past that the railway is swinging + # north across where it would have run. + _slab(Vector2((-HALF_X + SHOP_ST_X1) * 0.5, SHOP_ST_Z), + Vector2(SHOP_ST_X1 + HALF_X, SHOP_ST_HALF * 2), Y_ROAD_EW, + M["road"], "concrete") + _slab(Vector2(0, SOUTH_ST_Z), Vector2(HALF_X * 2, SOUTH_ST_HALF * 2), Y_ROAD_EW, + M["road"], "concrete") + _slab(Vector2(0, 0), Vector2(ROAD_HALF * 2, HALF_Z * 2), Y_ROAD_NS, + M["road"], "concrete") + + # Kerb and pavement either side of every road, gapped wherever another road, + # the railway, or the crossing cuts through. The kerb is a real 12 cm step + # you can hear and feel, which is most of what makes a street feel like a + # street rather than like a floor. + var ns_gaps := [ + Vector2(-RAIL_HALF - 1.0, RAIL_HALF + 1.0), + Vector2(SHOP_ST_Z - SHOP_ST_HALF - PAVE_W - 0.3, SHOP_ST_Z + SHOP_ST_HALF + PAVE_W + 0.3), + Vector2(SOUTH_ST_Z - SOUTH_ST_HALF - PAVE_W - 0.3, SOUTH_ST_Z + SOUTH_ST_HALF + PAVE_W + 0.3), + # The east-district street is a T-junction into the main road. Leaving + # this out made the main-road kerb and pavement run straight across its + # mouth, so the neighbourhood road stopped behind a raised white strip. + Vector2(EAST_ST_Z - EAST_ST_HALF - PAVE_W - 0.3, + EAST_ST_Z + EAST_ST_HALF + PAVE_W + 0.3), + ] + var ew_gaps := [Vector2(-ROAD_HALF - PAVE_W - 0.3, ROAD_HALF + PAVE_W + 0.3)] + _street_edges(false, 0.0, ROAD_HALF, -HALF_Z, HALF_Z, ns_gaps) + _street_edges(true, SHOP_ST_Z, SHOP_ST_HALF, -HALF_X, SHOP_ST_X1, ew_gaps) + _street_edges(true, SOUTH_ST_Z, SOUTH_ST_HALF, -HALF_X, HALF_X, ew_gaps) + + # Markings. Broken centre lines on the through streets, a pair of stop bars + # short of the rails, and a zebra on the main street south of the crossing. + var z := -HALF_Z + while z < HALF_Z: + if absf(z) > RAIL_HALF + 2.5: + _paint(Vector2(0, z), Vector2(0.16, 3.0), Y_ROAD_NS, M["line_white"]) + z += 6.0 + var x := -HALF_X + while x < HALF_X: + if absf(x) > ROAD_HALF + 1.0: + if x < SHOP_ST_X1 - 2.0: + _paint(Vector2(x, SHOP_ST_Z), Vector2(3.0, 0.16), Y_ROAD_EW, M["line_white"]) + _paint(Vector2(x, SOUTH_ST_Z), Vector2(3.0, 0.16), Y_ROAD_EW, M["line_white"]) + x += 6.0 + for s in [-1.0, 1.0]: + _paint(Vector2(ROAD_HALF * 0.5 * s, (RAIL_HALF + 2.0) * s), + Vector2(ROAD_HALF - 0.4, 0.4), Y_ROAD_NS, M["line_white"]) + _build_road_markings() + + +## Everything painted on or set into the road surface. +## +## This is the tier of detail the first pass had none of, and the ground is half +## of every frame in a first-person game — a road with two dashed lines on it and +## nothing else is a corridor floor with a stripe. +func _build_road_markings() -> void: + # 止まれ on both approaches to the crossing, read from the direction of + # travel: northbound traffic reads it from the south side and vice versa. + for s in [-1.0, 1.0]: + _road_text(SakuraSignage.ROAD_STOP, -ROAD_HALF * 0.45 * s, + s * (RAIL_HALF + 6.2), Y_ROAD_NS, 1.5, SakuraPalette.LINE_WHITE, + 0.0 if s > 0.0 else 180.0) + _road_text(SakuraSignage.ROAD_SLOW, -ROAD_HALF * 0.45 * s, + s * (RAIL_HALF + 13.0), Y_ROAD_NS, 1.1, SakuraPalette.LINE_WHITE, + 0.0 if s > 0.0 else 180.0) + # Zebra crossings on the far side of each junction. + _zebra(Vector2(0, s * (SHOP_ST_Z if s < 0.0 else SOUTH_ST_Z) + s * 7.4), + ROAD_HALF * 2.0 - 0.8, 2.4, true, Y_ROAD_NS) + + # Zebras across the two east-west streets at the main junction. + for z in [SHOP_ST_Z, SOUTH_ST_Z]: + for s in [-1.0, 1.0]: + _zebra(Vector2(s * (ROAD_HALF + 3.2), z), SHOP_ST_HALF * 2.0 - 0.8, + 2.4, false, Y_ROAD_EW) + + # Manholes and gullies. Manholes sit in the running lane; gullies sit tight + # against the kerb, which is the only place water goes. + var z := -HALF_Z + 14.0 + while z < HALF_Z - 8.0: + if absf(z) > RAIL_HALF + 3.0: + _manhole(ROAD_HALF * -0.42, z, Y_ROAD_NS) + _drain_grate(ROAD_HALF - 0.42, z + 5.0, Y_ROAD_NS, false) + _drain_grate(-ROAD_HALF + 0.42, z + 5.0, Y_ROAD_NS, false) + z += 17.0 + var x := -HALF_X + 12.0 + while x < HALF_X - 8.0: + if absf(x) > ROAD_HALF + 3.0: + for street_z in [SHOP_ST_Z, SOUTH_ST_Z]: + _manhole(x, street_z + 1.4, Y_ROAD_EW) + _drain_grate(x + 6.0, street_z - SHOP_ST_HALF + 0.42, Y_ROAD_EW, true) + x += 21.0 + + # Kerb ramps at every corner of the main junction, plus their tactile pad — + # a dropped kerb with no 点字ブロック on it is the one detail a Japanese + # street never omits. + for sx in [-1.0, 1.0]: + for street_z in [SHOP_ST_Z, SOUTH_ST_Z]: + for sz in [-1.0, 1.0]: + var cx: float = sx * (ROAD_HALF + 1.3) + var cz: float = street_z + sz * (SHOP_ST_HALF + 1.3) + # Fill the complete corner quadrant. The old 2.35 m square ended + # 0.425 m before both long pavements, leaving every tactile pad on + # a visually isolated concrete island. + var apron_span := PAVE_W + 0.60 + var apron_cx: float = sx * (ROAD_HALF + apron_span * 0.5) + var apron_cz: float = street_z + sz * (SHOP_ST_HALF + apron_span * 0.5) + var connector := _slab(Vector2(apron_cx, apron_cz), + Vector2(apron_span, apron_span), Y_PAVE, M["pave"], "concrete") + connector.name = "SidewalkConnector" + connector.set_meta("sidewalk_connector", true) + _tactile_dots(Vector2(cx, cz), Vector2(1.5, 1.5), Y_PAVE) + + +## Kerb + pavement along a road. `ew` picks the axis; `gaps` are runs to skip. +func _street_edges(ew: bool, centre: float, road_half: float, + a: float, b: float, gaps: Array) -> void: + for s in [-1.0, 1.0]: + var edge: float = centre + road_half * s + for seg in _segments(a, b, gaps): + var mid := (seg.x + seg.y) * 0.5 + var length := seg.y - seg.x + var pave_c: float = edge + (PAVE_W * 0.5 + 0.12) * s + if ew: + _solid(Vector3(mid, 0.10, edge + 0.06 * s), + Vector3(length, 0.20, 0.12), M["kerb"], "concrete") + _slab(Vector2(mid, pave_c), Vector2(length, PAVE_W), Y_PAVE, + M["pave"], "concrete") + else: + _solid(Vector3(edge + 0.06 * s, 0.10, mid), + Vector3(0.12, 0.20, length), M["kerb"], "concrete") + _slab(Vector2(pave_c, mid), Vector2(PAVE_W, length), Y_PAVE, + M["pave"], "concrete") + + +# ── Railway ────────────────────────────────────────────────────────────────── +# +# The line is a POLYLINE, not an axis. Everything below walks that centreline +# and places itself relative to the local tangent and normal, so the ballast, +# the sleepers, the rails, the fence, the contact wire and the catenary all +# follow the turn without any of them knowing where the turn is. +# +# That rewrite is also what fixed the catenary. It used to be a single mast with +# a cantilever arm reaching HALF way across the corridor, and two insulators +# hung at the track centres — so one insulator sat under the arm, the other +# floated in clear air two metres past its end, and neither of them actually +# touched it (the arm sat at 6.86-6.94, the insulators at 6.49-6.83). It is now +# a portal: a mast each side, a beam over both tracks, droppers down from the +# beam, and a contact wire running the length of the line that the droppers +# land on. Nothing is left hanging. + +var _rail_pts: Array = [] + + +## The centreline, sampled at sleeper pitch: [Vector2 position, Vector2 tangent]. +## +## Straight in from the west, an arc of CURVE_SWEEP degrees starting at +## CURVE_START_X, then straight out to the edge of the world on the new heading. +func _rail_path() -> Array: + if not _rail_pts.is_empty(): + return _rail_pts + var step := 0.62 + var pts: Array = [] + var x := -HALF_X - OUTFIELD + while x < CURVE_START_X: + pts.append([Vector2(x, 0.0), Vector2(1.0, 0.0)]) + x += step + # The arc. Centre is one radius to the north of where the turn begins, so + # the tangent at the start is still due east and the line bends toward -Z. + var c := Vector2(CURVE_START_X, -CURVE_RADIUS) + var sweep := deg_to_rad(CURVE_SWEEP) + var n := maxi(2, int(CURVE_RADIUS * sweep / step)) + for i in range(n + 1): + var th := sweep * float(i) / float(n) + pts.append([c + Vector2(sin(th), cos(th)) * CURVE_RADIUS, + Vector2(cos(th), -sin(th))]) + var ep: Vector2 = pts[pts.size() - 1][0] + var et: Vector2 = pts[pts.size() - 1][1] + var d := 0.0 + while d < OUTFIELD + 70.0: + d += step + pts.append([ep + et * d, et]) + _rail_pts = pts + return pts + + +## Yaw that turns a box's local +X onto the given tangent. +func _yaw_of(t: Vector2) -> float: + return rad_to_deg(atan2(-t.y, t.x)) + + +## A solid box spanning exactly from `a` to `b`, `width` across and `h` tall, +## centred vertically on `y`. Length and heading come from the chord, so a run +## of these butts end to end however the line curves. +func _chord_solid(a: Vector2, b: Vector2, y: float, h: float, width: float, + mat: Material, acoustic: String) -> StaticBody3D: + var mid := (a + b) * 0.5 + var d := b - a + return _solid(Vector3(mid.x, y, mid.y), Vector3(d.length(), h, width), + mat, acoustic, Vector3(0, _yaw_of(d.normalized()), 0)) + + +## Collision-only twin of _chord_solid, for barriers whose visible form is a set +## of thin members. See _blocker. +func _chord_blocker(a: Vector2, b: Vector2, y: float, h: float, width: float, + acoustic: String = "metal") -> void: + var mid := (a + b) * 0.5 + var d := b - a + _blocker(Vector3(mid.x, y, mid.y), Vector3(d.length(), h, width), + acoustic).rotation_degrees = Vector3(0, _yaw_of(d.normalized()), 0) + + +## Render-only twin of _chord_solid. +func _chord_deco(a: Vector2, b: Vector2, y: float, h: float, width: float, + mat: Material) -> void: + var mid := (a + b) * 0.5 + var d := b - a + _deco(_box_mesh(Vector3(d.length(), h, width)), mat, + Vector3(mid.x, y, mid.y), Vector3(0, _yaw_of(d.normalized()), 0)) + + +## Does this chord stay clear of the level crossing's road deck? +func _chord_clear_of_crossing(a: Vector2, b: Vector2) -> bool: + var mid := (a + b) * 0.5 + return absf(mid.x) > ROAD_HALF + 0.9 or absf(mid.y) > 3.0 + + +## Left-hand normal of a tangent, in the XZ plane. +func _normal_of(t: Vector2) -> Vector2: + return Vector2(-t.y, t.x) + + +## Is this stretch of lineside left open? The crossing for the road, the station +## for its platform, the footbridge for its piers. +func _rail_open(p: Vector2, side: float) -> bool: + if absf(p.y) < 3.0 and absf(p.x) < ROAD_HALF + 1.2: + return true + if absf(p.y) < 3.0 and absf(p.x - BRIDGE_X) < 2.2: + return true + if side > 0.0 and absf(p.y) < 3.0 and absf(p.x - STATION_X) < 17.0: + return true + return false + + +func _build_railway() -> void: + var path := _rail_path() + var step := 0.62 + + # ── Ballast ────────────────────────────────────────────────────────────── + # Solid, because players walk on it, so it is one rotated body every few + # metres rather than a MultiMesh. Skipped through the crossing, where the + # road deck takes over. + # ── Segments are CHORDS, not fixed-length boxes ────────────────────────── + # + # A 6.2 m box dropped every 6.0 m along a centreline is fine on the + # straight and wrong on the curve: the box follows the TANGENT while the + # line follows the arc, so consecutive segments splay apart on the outside + # and drive into each other on the inside. Spanning each segment between two + # actual path samples makes its length and heading the chord itself, so they + # meet end to end all the way round the turn. + var bal_every := int(6.0 / step) + var i := 0 + while i + bal_every < path.size(): + var a: Vector2 = path[i][0] + var b: Vector2 = path[i + bal_every][0] + if _chord_clear_of_crossing(a, b): + _chord_solid(a, b, BALLAST_H - 0.25, 0.5, RAIL_HALF * 2.0 - 2.0, + M["ballast"], "gravel") + i += bal_every + + # ── Sleepers ───────────────────────────────────────────────────────────── + _path_multimesh(_box_mesh(Vector3(0.22, 0.16, 2.3)), + M["sleeper"], path, 1, BALLAST_H + 0.04, [-TRACK_OFFSET, TRACK_OFFSET], 0.0) + + # ── Rails ──────────────────────────────────────────────────────────────── + # Two boxes per rail: a dark web and a bright head. At any distance a player + # sees them from, a rail IS a dark line with a light line on top of it. + var rail_every := 3 # ~1.9 m segments: smooth enough around a 60 m radius + var offs: Array = [] + for tr in [-TRACK_OFFSET, TRACK_OFFSET]: + offs.append(tr - RAIL_GAUGE * 0.5) + offs.append(tr + RAIL_GAUGE * 0.5) + _path_multimesh(_box_mesh(Vector3(step * float(rail_every) + 0.05, 0.10, 0.07)), + M["rail"], path, rail_every, BALLAST_H + 0.13, offs, 0.0) + _path_multimesh(_box_mesh(Vector3(step * float(rail_every) + 0.05, 0.05, 0.09)), + M["rail_head"], path, rail_every, BALLAST_H + 0.20, offs, 0.0) + + # ── Contact wire ───────────────────────────────────────────────────────── + # Continuous, above each track, at the height the portal droppers reach down + # to. This is the thing the old insulators were hanging next to nothing for + # want of. + _path_multimesh(_box_mesh(Vector3(step * float(rail_every) + 0.05, 0.05, 0.05)), + M["ink"], path, rail_every, 6.20, [-TRACK_OFFSET, TRACK_OFFSET], 0.0) + + # ── Lineside fence ─────────────────────────────────────────────────────── + _rail_fence(path, step) + + # ── Catenary portals ───────────────────────────────────────────────────── + var portal_every := int(22.0 / step) + var j := portal_every + while j < path.size(): + var p: Vector2 = path[j][0] + var t: Vector2 = path[j][1] + if absf(p.x) > ROAD_HALF + 4.0 and absf(p.x - BRIDGE_X) > 4.0 \ + and absf(p.x - STATION_X) > 17.0: + _catenary_portal(p, t) + j += portal_every + + +## Lay one mesh repeatedly along the path, at a set of lateral offsets. +## Returns the instance count placed. +func _path_multimesh(mesh: Mesh, mat: Material, path: Array, every: int, + y: float, offsets: Array, _pad: float) -> int: + var xforms: Array = [] + var i := 0 + while i < path.size(): + var p: Vector2 = path[i][0] + var t: Vector2 = path[i][1] + var nrm := _normal_of(t) + var basis := Basis(Vector3.UP, deg_to_rad(_yaw_of(t))) + for o in offsets: + var q: Vector2 = p + nrm * float(o) + xforms.append(Transform3D(basis, Vector3(q.x, y, q.y))) + i += every + var mm := MultiMesh.new() + mm.transform_format = MultiMesh.TRANSFORM_3D + mm.mesh = mesh + mm.instance_count = xforms.size() + for k in range(xforms.size()): + mm.set_instance_transform(k, xforms[k]) + var mmi := MultiMeshInstance3D.new() + mmi.multimesh = mm + mmi.material_override = mat + _decor.add_child(mmi) + return xforms.size() + + +## Posts and two bars, with an invisible barrier doing the blocking. +## +## The physics and the picture are separated on purpose: one solid box the size +## of the whole fence is the right shape to stop a player and completely the +## wrong picture — an early build drew exactly that and walled the railway off +## behind a hundred and sixty metres of blank grey parapet. +func _rail_fence(path: Array, step: float) -> void: + var post_every := int(2.4 / step) + var block_every := int(6.0 / step) + for side in [-1.0, 1.0]: + var off: float = side * (RAIL_HALF - 0.4) + # Posts. + var posts: Array = [] + var i := post_every / 2 + while i < path.size(): + var p: Vector2 = path[i][0] + var t: Vector2 = path[i][1] + if not _rail_open(p, side): + var q: Vector2 = p + _normal_of(t) * off + posts.append(Transform3D(Basis.IDENTITY, Vector3(q.x, 0.75, q.y))) + i += post_every + var mm := MultiMesh.new() + mm.transform_format = MultiMesh.TRANSFORM_3D + mm.mesh = _cyl_mesh(0.05, 0.05, 1.5, 6) + mm.instance_count = posts.size() + for k in range(posts.size()): + mm.set_instance_transform(k, posts[k]) + var mmi := MultiMeshInstance3D.new() + mmi.multimesh = mm + mmi.material_override = M["metal_dark"] + _decor.add_child(mmi) + + # Bars, and the barrier behind them — chord-spanned for the same reason + # the ballast is. Fixed 6.25 m bars stepped every 6.0 m along the arc + # were the fence "cutting into itself" on the curve: each bar ran along + # its own tangent, so on the inside of the turn they overlapped and + # speared through one another. + var j := 0 + while j + block_every < path.size(): + var pa: Vector2 = path[j][0] + var pb: Vector2 = path[j + block_every][0] + var qa: Vector2 = pa + _normal_of(path[j][1]) * off + var qb: Vector2 = pb + _normal_of(path[j + block_every][1]) * off + if not _rail_open((pa + pb) * 0.5, side): + _chord_blocker(qa, qb, 0.75, 1.5, 0.10) + for bar_y in [0.62, 1.42]: + _chord_deco(qa, qb, bar_y, 0.07, 0.07, M["metal_dark"]) + j += block_every + + +## A catenary portal: a mast each side, a beam across both tracks, and a dropper +## from the beam down to the contact wire above each track. +## +## Everything here TOUCHES something. The previous version's insulators were +## placed at the track centres while the arm that was supposed to carry them +## reached only half way across the corridor, so one of them hung unattached in +## mid-air — which is exactly what it looked like. +func _catenary_portal(p: Vector2, t: Vector2) -> void: + var nrm := _normal_of(t) + var yaw := _yaw_of(t) + var reach := RAIL_HALF - 1.2 + var beam_y := 7.10 + var wire_y := 6.20 + + for side in [-1.0, 1.0]: + var q: Vector2 = p + nrm * (reach * side) + _post(Vector3(q.x, 3.6, q.y), 0.11, 7.2, M["metal_dark"], "metal", 6) + + # The beam, spanning mast to mast. Its length is the full corridor, not half + # of it, so both tracks are actually under it. + _deco(_box_mesh(Vector3(0.14, 0.16, reach * 2.0 + 0.3)), M["metal_dark"], + Vector3(p.x, beam_y, p.y), Vector3(0, yaw, 0)) + + # Droppers, from the beam down to the wire, with the registration insulator + # at the bottom sitting ON the wire rather than beside it. + for tr in [-TRACK_OFFSET, TRACK_OFFSET]: + var q: Vector2 = p + nrm * tr + var drop := beam_y - wire_y + _deco(_box_mesh(Vector3(0.06, drop, 0.06)), M["metal_dark"], + Vector3(q.x, wire_y + drop * 0.5, q.y), Vector3(0, yaw, 0)) + _deco(_box_mesh(Vector3(0.13, 0.22, 0.13)), M["metal"], + Vector3(q.x, wire_y + 0.13, q.y), Vector3(0, yaw, 0)) + + + +# ── The crossing ───────────────────────────────────────────────────────────── + +func _build_crossing() -> void: + # The road surface over the rails: a concrete panel deck flush with the rail + # heads, with the flangeway gaps drawn as dark slots. This is the map's + # centre and the thing the place is named for, so it is the most detailed + # square metre in it. + var deck_w := ROAD_HALF * 2 + 1.2 + _slab(Vector2(0, 0), Vector2(deck_w, RAIL_HALF * 2 - 1.6), CROSS_Y, + M["concrete_mid"], "concrete", 0.5) + + # Approach ramps. Without these the deck is a 28 cm wall across the road: + # real crossings rise to meet the rails and so does this one, over three and + # a half metres, which is about five degrees and invisible. + var rise := CROSS_Y - Y_ROAD_NS + var run := 3.5 + var ramp_len := sqrt(rise * rise + run * run) + var ramp_a := rad_to_deg(atan2(rise, run)) + for s in [-1.0, 1.0]: + var zc: float = s * (RAIL_HALF - 0.8 + run * 0.5) + _solid(Vector3(0, Y_ROAD_NS + rise * 0.5 - 0.1, zc), + Vector3(deck_w, 0.2, ramp_len), M["concrete_mid"], "concrete", + Vector3(ramp_a * s, 0, 0)) + + for t in [-TRACK_OFFSET, TRACK_OFFSET]: + for r in [-RAIL_GAUGE * 0.5, RAIL_GAUGE * 0.5]: + _paint(Vector2(0, t + r), Vector2(deck_w, 0.10), CROSS_Y, M["ink"]) + _deco(_box_mesh(Vector3(deck_w, 0.05, 0.09)), M["rail_head"], + Vector3(0, CROSS_Y - 0.01, t + r)) + + # Tactile paving where the pavement meets the track — the one saturated + # yellow at ground level, and it reads from right across the map. The strip + # is the ground colour; the DOTS on it are what make it 点字ブロック rather + # than a painted band. + for s in [-1.0, 1.0]: + _paint(Vector2(0, s * (RAIL_HALF + 1.0)), Vector2(ROAD_HALF * 2, 0.7), + Y_ROAD_NS, M["tactile"]) + _tactile_dots(Vector2(0, s * (RAIL_HALF + 1.0)), Vector2(ROAD_HALF * 2 - 0.2, 0.6), + Y_ROAD_NS + 0.02) + # とまれ, right at the rails, which is where it is actually painted. + _road_text(SakuraSignage.CROSSING_STOP, ROAD_HALF * 0.45 * s, + s * (RAIL_HALF + 3.0), Y_ROAD_NS, 1.05, SakuraPalette.LINE_WHITE, + 0.0 if s > 0.0 else 180.0) + + # Crossing gear on all four corners: mast, barrier boom, alarm head, X sign. + for sx in [-1.0, 1.0]: + for sz in [-1.0, 1.0]: + _crossing_gear( + Vector3(sx * (ROAD_HALF + 0.9), Y_ROAD_NS, sz * (RAIL_HALF + 0.9)), sx, sz) + + # Signal relay cabinets. Chest high, hard, and placed so they give the only + # real cover inside the crossing — the map's most important piece of + # gameplay furniture, and also a thing that is genuinely there. + for sx in [-1.0, 1.0]: + for sz in [-1.0, 1.0]: + var c := Vector3(sx * (ROAD_HALF + 3.6), Y_ROAD_NS, sz * (RAIL_HALF + 2.0)) + _solid(c + Vector3(0, 0.62, 0), Vector3(1.5, 1.24, 0.75), M["cabinet"], "metal") + _deco(_box_mesh(Vector3(1.62, 0.09, 0.87)), M["cabinet_top"], + c + Vector3(0, 1.28, 0)) + + _spawn_points.append(Vector3(0, 1.6, -RAIL_HALF - 7.0)) + _spawn_points.append(Vector3(0, 1.6, RAIL_HALF + 7.0)) + + +func _crossing_gear(base: Vector3, sx: float, sz: float) -> void: + _post(base + Vector3(0, 1.6, 0), 0.10, 3.2, M["black"], "metal", 6) + + # The boom, down across the road, striped. Alternating boxes rather than a + # texture, because the whole map is untextured and a painted stripe would be + # the only thing in it that was not geometry. + var boom_len := ROAD_HALF + 1.4 + var stripes := 7 + var seg := boom_len / float(stripes) + for i in range(stripes): + var mat: Material = M["yellow"] if i % 2 == 0 else M["black"] + _deco(_box_mesh(Vector3(seg, 0.14, 0.10)), mat, + Vector3(base.x - sx * (seg * (float(i) + 0.5)), base.y + 1.05, base.z)) + _deco(_box_mesh(Vector3(0.36, 0.36, 0.30)), M["black"], + base + Vector3(sx * 0.24, 1.05, 0)) + + # The alarm head: two lamps and the X. One lamp lit and one dark — a + # crossing alarm alternates, and freezing it mid-alternation is what makes a + # still frame of it look like a frame of animation rather than like a model. + var head := base + Vector3(0, 3.0, 0) + _deco(_box_mesh(Vector3(0.9, 0.22, 0.14)), M["black"], head) + _deco(_cyl_mesh(0.16, 0.16, 0.10, 8), M["signal_red"], + head + Vector3(-0.3, 0, -sz * 0.1), Vector3(90, 0, 0)) + _deco(_cyl_mesh(0.16, 0.16, 0.10, 8), M["signal_off"], + head + Vector3(0.3, 0, -sz * 0.1), Vector3(90, 0, 0)) + for a in [42.0, -42.0]: + _deco(_box_mesh(Vector3(1.15, 0.13, 0.06)), M["yellow"], + head + Vector3(0, 0.62, -sz * 0.12), Vector3(0, 0, a)) + # 踏切注意 on a plate under the alarm head, facing the road it warns. + _sign_board(base + Vector3(0, 2.34, -sz * 0.13), -sz, + SakuraSignage.CROSSING_WARN, 0.20, SakuraPalette.GATE_YELLOW, + SakuraPalette.INK, 0.12) + # The reflective bands every crossing mast carries, low down where a car's + # lights catch them. + for i in range(3): + _deco(_box_mesh(Vector3(0.23, 0.13, 0.23)), M["white_board"], + base + Vector3(0, 0.42 + float(i) * 0.42, 0)) + + +# ── The shopping street ────────────────────────────────────────────────────── + +func _build_shopping_street() -> void: + # Two rows of shophouses facing each other across a 9 m street. The railway + # row also fronts the alley behind it, so it is the only building line in + # the map read from both sides — which is why it carries the fire escapes, + # and why its roofline is worth taking. + # The railway row is DOUBLE-FRONTED: shopfronts on the shopping street AND + # on the railway side. + # + # It was single-fronted first, and the render showed why that could not + # stand: the view north from the crossing — the map's signature shot, the + # one the whole place is arranged around — was a hundred and sixty metres of + # blank rear wall. A block between a station and a shopping street trades on + # both faces in real life for exactly the reason it has to here, which is + # that both sides are where the people are. + var pave_edge := SHOP_ST_HALF + PAVE_W + 0.12 + _shop_row(SHOP_ST_Z + pave_edge, -1.0, true) # railway block + _shop_row(SHOP_ST_Z - pave_edge, 1.0, false) # far side + + # The service alley between the railway row and the line. Split around the + # main street, which crosses it. + # ── The alley ends with the shops it serves ────────────────────────────── + # + # It used to run the full width of the map, and the railway's turn crosses + # z = -11.2 at x = 96.9 — so the far end of the alley ran out under the + # tracks. It serves the shop row, the shop row stops at SHOP_ST_X1, and so + # does this: no walkway, nothing to bridge. + for seg in _segments(-HALF_X, SHOP_ST_X1 + 4.0, + [Vector2(-ROAD_HALF - 0.2, ROAD_HALF + 0.2)]): + _slab(Vector2((seg.x + seg.y) * 0.5, ALLEY_Z), Vector2(seg.y - seg.x, ALLEY_W), + Y_GRASS, M["concrete_mid"], "concrete") + + # ── The bridge approach ────────────────────────────────────────────────── + # + # The gap the two rows leave at BRIDGE_X is made into a real side street, + # running from the shopping street through to the alley and the footbridge + # stair. Without it that gap is just a hole in a terrace; with it the bridge + # has an honest way on and off from the north, and the shopping street gains + # a second connection to the line. + # North end first: the shopping street sits at a MORE negative Z than the + # alley, so taking them in the other order gave the slab a negative length. + var app_z0 := SHOP_ST_Z + SHOP_ST_HALF + PAVE_W + var app_z1 := ALLEY_Z + ALLEY_W * 0.5 + _slab(Vector2(BRIDGE_X, (app_z0 + app_z1) * 0.5), Vector2(7.0, app_z1 - app_z0), + Y_GRASS, M["pave_alt"], "concrete") + # A kerb each side, so it reads as a street rather than as a missing tooth. + for s in [-1.0, 1.0]: + _solid(Vector3(BRIDGE_X + s * 3.55, Y_GRASS + 0.06, + (app_z0 + app_z1) * 0.5), + Vector3(0.12, 0.24, app_z1 - app_z0), M["kerb"], "concrete") + _spawn_points.append(Vector3(BRIDGE_X, 1.6, ALLEY_Z - 4.0)) + + _spawn_points.append(Vector3(-26, 1.6, ALLEY_Z)) + _spawn_points.append(Vector3(30, 1.6, ALLEY_Z)) + _spawn_points.append(Vector3(-10, 1.6, SHOP_ST_Z)) + _spawn_points.append(Vector3(28, 1.6, SHOP_ST_Z)) + + +## `facing` is +1 if the shopfronts look toward +Z, -1 if they look toward -Z. +## +## ── Unit width is the whole character of the street ───────────────────────── +## +## The first version divided the row into EIGHT units, which at this map's +## length made each shop 19.5 m wide — and a 19.5 m shop is not a shop, it is a +## department store. The render showed the consequence plainly: one awning, one +## sign and one noren stretched across sixty feet of frontage, so the street read +## as four enormous buildings rather than as a 商店街, and the noren in +## particular spanned the entire elevation like a stage curtain. +## +## A real shopping street is made of SMALL businesses — six to ten metres of +## frontage each — and the rhythm of many narrow units is most of what the place +## is. Twenty units a side instead of eight means twenty awning colours, twenty +## names, twenty projecting signs and twenty doorways, which is where all the +## detail in this kind of street actually lives. +func _shop_row(front_z: float, facing: float, double_fronted: bool) -> void: + var depth := 10.0 + + # Lay the units out first, so each one can be told whether its neighbours + # exist. A unit with a missing neighbour turns a blank flank wall to the + # street, and those walls need a painted sign — see _wall_sign. + var units: Array = [] # [cx, w, present] + var x := -HALF_X + 4.0 + while x < SHOP_ST_X1 - 8.0: + var unit := _rng.randf_range(6.0, 10.0) + var cx := x + unit * 0.5 + # ── Two gaps in the terrace ────────────────────────────────────────── + # + # The crossing approach, and — new — the FOOTBRIDGE approach. The + # railway row used to run straight past the bridge, and its fire escapes + # land in the same service alley the bridge's north stair descends into: + # the two intersected, so the way off the bridge ran through somebody's + # staircase. Twelve metres of clearance is set by reach, not by looks — + # a fire escape extends about six metres back from its unit and the + # stair run is seven, so anything closer than that overlaps again. + var clear := absf(cx) > ROAD_HALF + 4.0 + if double_fronted and absf(cx - BRIDGE_X) < 12.0: + clear = false + elif not double_fronted and absf(cx - BRIDGE_X) < 8.0: + clear = false + units.append([cx, unit - 0.35, clear]) + x += unit + + for i in range(units.size()): + if not units[i][2]: + continue + var prev_gone: bool = i == 0 or not units[i - 1][2] + var next_gone: bool = i == units.size() - 1 or not units[i + 1][2] + # The body sits BEHIND its shopfront, i.e. against -facing. The 0.35 m + # taken off the width is the party-wall joint between neighbours, and + # the ink pass draws it as the line that separates one business from + # the next. + _shop(Vector3(units[i][0], 0.0, front_z - facing * depth * 0.5), + units[i][1], depth, facing, i, double_fronted, prev_gone, next_gone) + + +## The shop generator. One function, and the variety comes out of a handful of +## numbers the way the reference's `makeShop` does: storeys, wall tone, awning +## colour, whether it has a noren, a roof tank, a fire escape. +func _shop(centre: Vector3, w: float, d: float, facing: float, idx: int, + double_fronted: bool = false, expose_minus_x: bool = false, + expose_plus_x: bool = false) -> void: + var storeys := 2 if _rng.randf() < 0.68 else 3 + var h := float(storeys) * FLOOR_H + + # Flank walls that face the street get a painted sign. These are the biggest + # blank surfaces in the map without one. + if expose_minus_x: + _wall_sign(centre.x - w * 0.5, centre.z, h, d, -1.0, idx, facing) + if expose_plus_x: + _wall_sign(centre.x + w * 0.5, centre.z, h, d, 1.0, idx + 3, facing) + + # The body. Its top face IS the roof deck — nothing extra to stand on. + _solid(centre + Vector3(0, h * 0.5, 0), Vector3(w, h, d), _wall_mat(idx), "concrete") + + # Parapet on all four edges. This is what makes a roof a place you can fight + # from rather than a plane you slide off, so it is a real collider. + for s in [-1.0, 1.0]: + _solid(centre + Vector3(0, h + 0.35, s * (d * 0.5 - 0.12)), + Vector3(w + 0.3, 0.7, 0.25), _roof_mat(idx), "concrete") + _solid(centre + Vector3(s * (w * 0.5 - 0.12), h + 0.35, 0), + Vector3(0.25, 0.7, d), _roof_mat(idx), "concrete") + + # Every third unit is a SERVICE unit: no rear shopfront, a fire escape + # instead. That is what gives the row its way onto the roof, and it is also + # why the rear elevation is not simply the front one mirrored — a terrace + # where every single unit trades on both faces has no back of house at all, + # which is the sort of too-tidy that reads as generated. + # Every third unit, AND every three-storey unit whatever its index. + # + # The frequency used to be index-only, and the walkability probe showed what + # that cost: the tall roofs came out as the biggest unreachable islands in + # the map, because a 10.2 m roof is three and a half metres above its + # two-storey neighbours and nothing else in the row can climb that. A block + # that stands a storey over the street should be the best position on it, + # not scenery. + var rear_service := idx % 3 == 1 or storeys == 3 + _shop_facade(centre.x, centre.z + facing * d * 0.5, facing, w, h, storeys, idx) + if double_fronted and not rear_service: + # Offset the index so the two faces are not the same shop drawn twice. + _shop_facade(centre.x, centre.z - facing * d * 0.5, -facing, w, h, storeys, + idx + 4) + + # Roof clutter — a water tank, an AC unit, a vent stack. Half the reason a + # roofline is worth being on is that there is something up there to stand + # behind. + if _rng.randf() < 0.7: + _solid(centre + Vector3(w * 0.2, h + 1.1, facing * d * 0.15), + Vector3(1.8, 1.4, 1.6), M["metal"], "metal") + _deco(_cyl_mesh(0.14, 0.14, 1.2, 6), M["metal_dark"], + centre + Vector3(w * 0.2 - 0.7, h + 1.9, facing * d * 0.15)) + _solid(centre + Vector3(-w * 0.25, h + 0.5, -facing * d * 0.2), + Vector3(1.2, 0.9, 0.9), M["metal_dark"], "metal") + + # The way up, on the service unit's back wall — the alley for the railway + # row, the rear lane for the far one — so the climb onto the roofline is + # never made in view of the street that roofline overlooks. + if rear_service: + _fire_escape(Vector3(centre.x + w * 0.5 - 1.0, Y_GRASS, + centre.z - facing * d * 0.5), h, -facing) + # A roof spawn every few units, not on every one. With forty shops a row a + # spawn per roof would make the rooflines four fifths of the spawn table and + # nearly every round would open with everyone above the street. + if idx % 5 == 2: + _spawn_points.append(Vector3(centre.x, h + 1.4, centre.z)) + + +## One elevation of a shop: the glazed bay, the awning and its valance, an +## optional noren, a corner signboard, and a fascia band plus window band per +## upper storey. +## +## `facing` is OUTWARD — away from the building, toward the street — and every +## offset in here is `front + facing * k`. That one convention is what lets the +## same function draw the shopping-street elevation and the railway elevation of +## the same block without a second code path. +func _shop_facade(cx: float, front: float, facing: float, w: float, h: float, + storeys: int, idx: int) -> void: + var bay_w := w - 1.4 + _deco(_box_mesh(Vector3(bay_w, 2.5, 0.1)), M["glass"], + Vector3(cx, 1.45, front + facing * 0.06)) + # The shopfront frame: a head member, a stallriser under the glass, and two + # mullions. A glazed bay with no frame is a hole in a wall. + _deco(_box_mesh(Vector3(bay_w + 0.3, 0.3, 0.2)), M["wood_dark"], + Vector3(cx, 2.85, front + facing * 0.08)) + _deco(_box_mesh(Vector3(bay_w + 0.24, 0.44, 0.16)), M["wood_dark"], + Vector3(cx, 0.30, front + facing * 0.07)) + for s in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(0.12, 2.5, 0.14)), M["wood_dark"], + Vector3(cx + s * bay_w * 0.5, 1.45, front + facing * 0.07)) + _deco(_box_mesh(Vector3(0.10, 2.5, 0.11)), M["wood"], + Vector3(cx + s * bay_w * 0.22, 1.45, front + facing * 0.05)) + # The shutter box over the bay — every one of these shops has one, and it is + # the deepest horizontal shadow line on the ground floor. + _deco(_box_mesh(Vector3(bay_w + 0.42, 0.34, 0.3)), M["shutter"], + Vector3(cx, 3.18, front + facing * 0.13)) + + # The awning is the row's colour rhythm and the thing that makes the street + # read as 商店街 rather than as a terrace. Sloped — a flat one reads as a + # shelf — with a valance hanging off its front edge. + var awn: Color = SakuraPalette.AWNINGS[(idx * 3 + 1) % SakuraPalette.AWNINGS.size()] + var awn_mat := LevelMaterials.cel(awn, LevelMaterials.RAMP_2, SakuraPalette.TINT_WARM) + # Rotating about X by +θ drops the +Z end, so the slope has to follow the + # facing or half the awnings in the map tilt back into their own shopfronts. + _deco(_box_mesh(Vector3(bay_w + 0.6, 0.12, 2.0)), awn_mat, + Vector3(cx, 3.15, front + facing * 0.9), Vector3(facing * 14.0, 0, 0)) + _deco(_box_mesh(Vector3(bay_w + 0.6, 0.42, 0.06)), awn_mat, + Vector3(cx, 2.86, front + facing * 1.86)) + if idx % 5 != 2: + var noren: Color = [SakuraPalette.NOREN, SakuraPalette.NOREN_RED, + SakuraPalette.NOREN_CREAM][idx % 3] + var noren_ink: Color = SakuraPalette.NOREN_CREAM if noren != SakuraPalette.NOREN_CREAM \ + else SakuraPalette.INK + # A noren hangs over the DOORWAY and is about 1.5 m across — it is not + # sized to the shopfront. Scaling it to the bay made it a stage curtain + # spanning the whole elevation, which is the single most conspicuous + # thing wrong with the first render of this street. + var nor_x := cx - bay_w * 0.5 + 1.1 + for j in range(3): + _deco(_box_mesh(Vector3(0.46, 0.95, 0.03)), + LevelMaterials.unlit(noren, true), + Vector3(nor_x + (float(j) - 1.0) * 0.5, 2.28, front + facing * 0.12)) + # A noren carries one or two characters, never a whole shop name. + var nt: String = SakuraSignage.NOREN_TEXT[idx % SakuraSignage.NOREN_TEXT.size()] + SakuraSignage.label(_decor, nt, 0.20, noren_ink, + Vector3(nor_x, 2.34, front + facing * 0.15), + Vector3(0, 0.0 if facing > 0.0 else 180.0, 0)) + + # ── The lettering ──────────────────────────────────────────────────────── + # + # The fascia name board and the projecting side sign. Between them these are + # the loudest thing on a Japanese shopping street and the single largest + # missing piece from the first pass of this map — a 商店街 without type on it + # is a terrace, and the row read as one. + var name: String = SakuraSignage.SHOP_NAMES[idx % SakuraSignage.SHOP_NAMES.size()] + var sign_col: Color = SakuraPalette.DRINKS[(idx * 5) % SakuraPalette.DRINKS.size()] + var sign_ink: Color = SakuraPalette.INK if sign_col.get_luminance() > 0.42 \ + else SakuraPalette.WALL_WHITE + # Tight padding and large type: on a real fascia the lettering fills the + # board almost edge to edge, and a small line floating in a big coloured + # rectangle reads as a placeholder. + _sign_board(Vector3(cx, 3.74, front + facing * 0.12), facing, name, 0.46, + sign_col, sign_ink, 0.16) + + # The 袖看板, standing off the corner of the facade. + var vert: String = SakuraSignage.SHOP_VERTICAL[idx % SakuraSignage.SHOP_VERTICAL.size()] + _vertical_sign(cx + w * 0.5 - 0.55, h - 0.5, front + facing * 0.12, + facing, vert, sign_col, sign_ink) + + # The entrance: a recessed door under the noren, with a frame and a step. + var door_x := cx - bay_w * 0.5 + 1.1 + _deco(_box_mesh(Vector3(1.05, 2.1, 0.06)), M["glass_dark"], + Vector3(door_x, 1.05, front + facing * 0.09)) + _deco(_box_mesh(Vector3(1.24, 2.26, 0.11)), M["wood_dark"], + Vector3(door_x, 1.13, front + facing * 0.07)) + _deco(_box_mesh(Vector3(1.4, 0.12, 0.5)), M["stone"], + Vector3(door_x, 0.06, front + facing * 0.28)) + + for s_i in range(1, storeys): + var band_y := float(s_i) * FLOOR_H + 0.55 + _deco(_box_mesh(Vector3(w - 0.6, 0.5, 0.1)), M["white_board"], + Vector3(cx, band_y, front + facing * 0.08)) + # A string course under the band, which is what gives an upper storey a + # horizontal line to sit on. + _deco(_box_mesh(Vector3(w - 0.3, 0.11, 0.17)), M["concrete_dark"], + Vector3(cx, band_y - 0.34, front + facing * 0.1)) + var wins := maxi(2, int(w / 2.6)) + var win_w := (w - 1.0) / float(wins) - 0.5 + for k in range(wins): + var wx := cx + (float(k) - float(wins - 1) * 0.5) * (w - 1.0) / float(wins) + var wc := Vector3(wx, float(s_i) * FLOOR_H + 1.9, front + facing * 0.05) + _deco(_box_mesh(Vector3(win_w, 1.5, 0.08)), M["glass_dark"], wc) + # Sill, lintel, and a centre mullion in the opening. + _window_trim(wc, win_w, 1.5, facing, M["concrete_dark"]) + _deco(_box_mesh(Vector3(0.07, 1.5, 0.10)), M["trim_mat"], wc + Vector3(0, 0, facing * 0.02)) + # One balcony per upper storey, on alternating units. + if (idx + s_i) % 2 == 0: + _balcony(cx, float(s_i) * FLOOR_H + 0.95, front, w * 0.52, facing, + M["concrete_mid"]) + # Condenser units on the storeys without one. + else: + _ac_unit(Vector3(cx + w * 0.28, float(s_i) * FLOOR_H + 1.1, front), facing) + + # Ground-floor service kit and the pipe that drains the whole elevation. + _meter_box(Vector3(cx + w * 0.5 - 0.55, 1.15, front), facing) + _downpipe(cx - (w * 0.5 - 0.28), front + facing * 0.02, h, facing, + M["metal_dark"]) + # The parapet coping throws the roofline's own shadow line down the facade. + _deco(_box_mesh(Vector3(w + 0.5, 0.16, 0.34)), M["concrete_dark"], + Vector3(cx, h + 0.72, front + facing * 0.1)) + + +## ── A flight of stairs ─────────────────────────────────────────────────────── +## +## `top` is a point on the LANDING the flight serves, at that landing's walking +## height. The flight descends OUTWARD from there — along Z for `axis` 0, along +## X for `axis` 1, in the direction of `out_dir` — until it reaches `bottom_y`. +## Returns the foot of the flight so the caller can put a landing, a ramp or a +## second flight where it lands. +## +## ── Why it is written top-down ─────────────────────────────────────────────── +## +## The version this replaces took the BOTTOM and a climb direction, and every +## one of its three call sites got the relationship backwards: the base was put +## directly under the landing and the flight then rose *away* from it. The +## footbridge's stairs climbed six metres into open air beside the deck they +## were supposed to reach, with their lowest step tucked underneath it — which +## is the thing that reads, correctly, as "broken and disconnected". +## +## Anchoring on the landing removes the whole class of error. A flight cannot be +## built without naming the thing it connects to, and the arithmetic that used +## to live (wrongly) in every caller lives here once. +## +## The step geometry follows from that: with N risers between the two levels +## there are N-1 treads, the first sitting one rise below the landing and the +## last one rise above the ground. Getting that off by one is what leaves a +## flight ending in a step to nowhere. +func _stair_flight(top: Vector3, bottom_y: float, out_dir: float, width: float, + axis: int, going: float = 0.40) -> Vector3: + var axis_vec := Vector3(0, 0, out_dir) if axis == 0 else Vector3(out_dir, 0, 0) + var height := top.y - bottom_y + if height <= 0.05: + return Vector3(top.x, bottom_y, top.z) + + # Even risers. The controller steps up to 0.95 m (state_ground), so 0.32 is + # comfortably inside its budget with room for the rounding below. + var risers := maxi(1, int(ceil(height / 0.32))) + var rise := height / float(risers) + var run := float(risers - 1) * going + var foot := Vector3(top.x, bottom_y, top.z) + axis_vec * run + + for i in range(1, risers): + var tread_top := top.y - float(i) * rise + var travel := (float(i) - 0.5) * going + var c := Vector3(top.x, tread_top - rise * 0.5, top.z) + axis_vec * travel + var size := Vector3(width, rise, going + 0.02) if axis == 0 \ + else Vector3(going + 0.02, rise, width) + _solid(c, size, M["concrete_mid"], "concrete") + + if run <= 0.01: + return foot + + # The soffit: one sloped slab closing the underside. Without it a flight is + # a floating staircase you can see the sky through, which no stair in this + # map is meant to be. + var mid := (Vector3(top.x, top.y, top.z) + foot) * 0.5 + var slope := atan2(height, run) + var length := sqrt(height * height + run * run) + var rot := Vector3(out_dir * rad_to_deg(slope), 0, 0) if axis == 0 \ + else Vector3(0, 0, -out_dir * rad_to_deg(slope)) + var soffit_size := Vector3(width, 0.26, length) if axis == 0 \ + else Vector3(length, 0.26, width) + _solid(mid - Vector3(0, 0.30, 0), soffit_size, M["concrete_mid"], "concrete", rot) + + # Handrails: a rail parallel to the flight on each side, with balusters. + for s in [-1.0, 1.0]: + var side := Vector3(s * width * 0.5, 0, 0) if axis == 0 \ + else Vector3(0, 0, s * width * 0.5) + var rail_size := Vector3(0.08, 0.08, length) if axis == 0 \ + else Vector3(length, 0.08, 0.08) + _deco(_box_mesh(rail_size), M["metal"], mid + side + Vector3(0, 1.02, 0), rot) + var posts := maxi(2, int(run / 1.5)) + for i in range(posts + 1): + var t := float(i) / float(posts) + var p := Vector3(top.x, top.y, top.z).lerp(foot, t) + side + _deco(_box_mesh(Vector3(0.08, 1.02, 0.08)), M["metal"], p + Vector3(0, 0.51, 0)) + + return foot + + +## ── A fire escape ──────────────────────────────────────────────────────────── +## +## A switchback, not a single flight, and that is forced by arithmetic rather +## than by taste: a two-storey shop is 6.8 m up, which is 24 risers, which at a +## walkable going is nine metres of run — longer than most of the units in the +## row and far longer than the three-metre alley behind them. Folding it in half +## puts the whole thing inside a five-metre footprint against the back wall. +## +## The top landing deliberately sits ABOVE the parapet and reaches back over it, +## so a player walks off the landing and steps DOWN onto the roof. The previous +## version stopped at a platform 3.4 m out from the wall at roof height, with +## nothing between it and the building. +## The two flights are also offset in Z rather than stacked. Doubling straight +## back underneath left about 1.2 m of headroom on the lower flight once the +## upper one's soffit was accounted for — enough to trap a 1.8 m player under +## his own staircase. Side by side, the whole switchback still fits the three +## metre alley. +func _fire_escape(base: Vector3, roof_y: float, out_dir: float) -> void: + var land_y := roof_y + 0.78 # parapet top is roof_y + 0.7 + var z_up := base.z + out_dir * 1.15 # upper flight, nearest the wall + var z_down := base.z + out_dir * 2.55 # lower flight, one flight further out + var mid_y := (land_y + base.y) * 0.5 + + # The top landing, bridging the parapet: it starts a metre inside the roof + # and runs out clear of the wall, so a player walks off it and steps DOWN + # onto the roof deck. + _solid(Vector3(base.x, land_y - 0.1, base.z + out_dir * 0.35), + Vector3(1.8, 0.2, 2.7), M["metal_dark"], "metal") + + # Upper flight: off the landing, running -X down to the half-way level. + var mid_foot := _stair_flight(Vector3(base.x - 0.9, land_y, z_up), mid_y, + -1.0, 1.3, 1, 0.34) + # The half landing turns the corner between the two flights. + _solid(Vector3(mid_foot.x, mid_y - 0.1, base.z + out_dir * 1.85), + Vector3(1.5, 0.2, 2.9), M["metal_dark"], "metal") + # Lower flight: doubling back +X, one flight further from the wall. + _stair_flight(Vector3(mid_foot.x, mid_y, z_down), base.y, 1.0, 1.3, 1, 0.34) + + +# ── The south side ─────────────────────────────────────────────────────────── + +func _build_south_side() -> void: + # The verge between the railway fence and the south street: a row of cherry + # trees, so the south approach to the crossing is soft cover rather than + # open ground. It stops where the line turns away north — past that the + # verge is not beside anything. + var tx := -HALF_X + 10.0 + while tx < CURVE_START_X - 4.0: + if absf(tx) > ROAD_HALF + 4.0 and absf(tx - STATION_X) > 18.0 \ + and absf(tx - BRIDGE_X) > 9.0: + _cherry_tree(Vector3(tx, 0.0, 11.4), _rng.randf_range(0.85, 1.25)) + tx += _rng.randf_range(9.0, 13.0) + + # A row of detached houses behind garden walls, fronting the south street. + # Lower and further apart than the shophouses, so the south lane plays long + # where the north lane plays close. + # Stop where the east district begins. That district's north row backs onto + # this street; continuing the old row behind it placed two full houses in the + # same five metres of depth. + _house_row(-HALF_X + 8.0, EAST_DIST_X0 - 4.0, 35.5, -1.0, 0) + _build_east_district() + _build_north_park() + _build_park_road() + _build_shrine_precinct() + + +## A run of detached houses along one side of a street, skipping anything that +## would block the shrine axis, the crossing, or the station forecourt. +func _house_row(x0: float, x1: float, z: float, facing: float, seed_i: int) -> void: + var x := x0 + var i := seed_i + while x < x1: + var w := _rng.randf_range(9.0, 12.0) + # The plot, not the building, advances the row. Garden walls used to + # extend past a house whose next centre was spaced from the body width, + # which let neighbouring walls and even eaves overlap. + var plot_w := w + _rng.randf_range(4.0, 5.5) + var cx := x + plot_w * 0.5 + # Nothing is built across the shrine's axis: the 参道 used to dead-end + # into somebody's garden wall, with the torii pointing at the back of a + # house. Ten metres, not seven — a plot is up to twelve wide and its + # boundary wall reaches most of the way to its neighbour. + if absf(cx) > ROAD_HALF + 7.0 and absf(cx - STATION_X) > 16.0 \ + and absf(cx - SHRINE_X) > 10.0 \ + and absf(cx - EAST_ST_X) > plot_w * 0.5 + EAST_ST_HALF + PAVE_W + 0.4: + _house(Vector3(cx, 0, z), w, i, facing, plot_w) + x += plot_w + i += 1 + + +## ── The north park ─────────────────────────────────────────────────────────── +## +## This was market gardens: furrowed plots and sheds, put there to stop the +## strip behind the shopping street being dead ground. It is a public park now, +## which does the same job and does it better — a park has paths, so it reads as +## somewhere people go rather than somewhere they work, and a pond and a stand +## of trees give it interior structure that a grid of plots never had. +## +## It is also what the shopping street now runs around. See _park_road_path. +const NPARK_X0 := -96.0 +const NPARK_X1 := 92.0 +const NPARK_Z0 := -84.0 +const NPARK_Z1 := -46.0 + + +func _build_north_park() -> void: + var cz := (NPARK_Z0 + NPARK_Z1) * 0.5 + # Lawn, split around the main street. + for seg in _segments(NPARK_X0, NPARK_X1, + [Vector2(-ROAD_HALF - PAVE_W - 0.3, ROAD_HALF + PAVE_W + 0.3)]): + _slab(Vector2((seg.x + seg.y) * 0.5, cz), + Vector2(seg.y - seg.x, NPARK_Z1 - NPARK_Z0), Y_GRASS, M["grass"], "grass") + + # The main path: a gravel spine down the middle, and two spurs to the + # shopping street so the park is entered rather than merely bordered. + for seg in _segments(NPARK_X0 + 4.0, NPARK_X1 - 4.0, + [Vector2(-ROAD_HALF - 0.3, ROAD_HALF + 0.3)]): + _slab(Vector2((seg.x + seg.y) * 0.5, cz), Vector2(seg.y - seg.x, 3.6), + Y_GRASS + 0.02, M["gravel"], "gravel") + for sx in [-62.0, 26.0]: + _slab(Vector2(sx, (cz + NPARK_Z1) * 0.5), Vector2(3.2, NPARK_Z1 - cz), + Y_GRASS + 0.02, M["gravel"], "gravel") + # A second route loops around the pond instead of making every visit an + # out-and-back along the central spine. It joins the two park entrances via + # the water, pavilion and open east lawn. + _slab(Vector2(-34.0, NPARK_Z0 + 2.2), Vector2(40.0, 2.4), + Y_GRASS + 0.025, M["gravel"], "gravel") + for lx in [-54.0, -14.0]: + _slab(Vector2(lx, (NPARK_Z0 + 2.2 + cz) * 0.5), + Vector2(2.4, cz - (NPARK_Z0 + 2.2)), + Y_GRASS + 0.025, M["gravel"], "gravel") + _slab(Vector2(24.0, (cz + cz - 9.0) * 0.5), Vector2(2.4, 9.0), + Y_GRASS + 0.025, M["gravel"], "gravel") + + # ── The pond ───────────────────────────────────────────────────────────── + # Sunk a little, with a stone rim. Water is drawn flat and unlit, the way + # the reference draws it — a lit surface at this scale reads as ice. + var pond := Vector2(-34.0, cz - 8.0) + _slab(pond, Vector2(30.0, 15.0), Y_GRASS + 0.01, M["stone"], "concrete") + _slab(pond, Vector2(27.0, 12.4), Y_GRASS - 0.06, M["water"], "concrete") + for s in [-1.0, 1.0]: + _solid(Vector3(pond.x, Y_GRASS + 0.18, pond.y + s * 7.6), + Vector3(30.4, 0.36, 0.7), M["stone_dark"], "concrete") + _solid(Vector3(pond.x + s * 15.2, Y_GRASS + 0.18, pond.y), + Vector3(0.7, 0.36, 15.4), M["stone_dark"], "concrete") + # Trees frame the side banks, away from the cedar boundary strip. The old + # north-bank row occupied z=-83.5 while the cedars occupied z=-81, so the two + # species visibly grew through one another. + for p in [Vector2(pond.x - 18.2, pond.y), Vector2(pond.x + 18.2, pond.y)]: + _cherry_tree(Vector3(p.x, Y_GRASS, p.y), 1.08) + for p in [Vector3(pond.x - 13.8, Y_GRASS, pond.y + 8.5), + Vector3(pond.x + 13.8, Y_GRASS, pond.y + 8.5)]: + _shrub(p) + # A low wooden footbridge closes the walking loop and makes the pond an + # interactive route rather than scenery seen from one side. + var pond_bridge := _solid(Vector3(pond.x, Y_GRASS + 0.34, pond.y), + Vector3(2.2, 0.26, 16.8), M["wood"], "wood") + pond_bridge.name = "PondBridge" + for sx in [-1.0, 1.0]: + _solid(Vector3(pond.x + sx * 1.0, Y_GRASS + 0.90, pond.y), + Vector3(0.10, 0.75, 16.8), M["wood_dark"], "wood") + for z in [-6.8, -3.4, 0.0, 3.4, 6.8]: + for sx in [-1.0, 1.0]: + _post(Vector3(pond.x + sx, Y_GRASS + 0.72, pond.y + z), 0.07, 1.1, + M["wood_dark"], "wood", 6) + + # ── The pavilion (四阿) ─────────────────────────────────────────────────── + # A roof on posts in the middle of a lawn: the park's one piece of hard + # cover and the thing that gives the space a centre. + var pav := Vector2(24.0, cz - 9.0) + _slab(pav, Vector2(7.0, 7.0), Y_GRASS + 0.12, M["stone"], "concrete") + for sx in [-1.0, 1.0]: + for sz in [-1.0, 1.0]: + _post(Vector3(pav.x + sx * 2.7, 1.4, pav.y + sz * 2.7), 0.16, 2.8, + M["wood_dark"], "wood", 6) + _solid(Vector3(pav.x, 2.92, pav.y), Vector3(8.0, 0.24, 8.0), + M["roof_shrine"], "wood") + _deco(_prism_mesh(Vector3(8.4, 1.5, 8.4)), M["roof_shrine"], + Vector3(pav.x, 3.75, pav.y)) + _solid(Vector3(pav.x, 0.55, pav.y - 2.6), Vector3(5.0, 0.14, 0.55), + M["wood"], "wood") + + # Trees: cherry along the paths, cedar massed at the back against the wall. + var tx := NPARK_X0 + 8.0 + while tx < NPARK_X1 - 8.0: + if absf(tx) > ROAD_HALF + 7.0 and absf(tx - pond.x) > 17.0 \ + and absf(tx - pav.x) > 8.0: + _cherry_tree(Vector3(tx, Y_GRASS, cz + 7.0), _rng.randf_range(0.9, 1.35)) + tx += _rng.randf_range(11.0, 17.0) + var bx := NPARK_X0 + 5.0 + while bx < NPARK_X1 - 5.0: + # Keep the pond loop open and push the evergreen mass back toward the + # boundary wall, clearly separate from the ornamental cherries. + if not _on_any_road(Vector2(bx, NPARK_Z0 - 1.2)) \ + and absf(bx - pond.x) > 24.0: + _cedar(Vector3(bx, 0.0, NPARK_Z0 - 1.2), _rng.randf_range(0.85, 1.25)) + bx += _rng.randf_range(7.0, 12.0) + + # Benches along the spine, and hedges for cover across the open lawn. + for i in range(6): + var bxx := NPARK_X0 + 18.0 + float(i) * 26.0 + if absf(bxx) < ROAD_HALF + 7.0: + continue + _solid(Vector3(bxx, 0.55, cz + 2.6), Vector3(2.4, 0.12, 0.5), M["wood"], "wood") + for s in [-0.95, 0.95]: + _deco(_box_mesh(Vector3(0.14, 0.5, 0.5)), M["metal_dark"], + Vector3(bxx + s, 0.35, cz + 2.6)) + var hx := NPARK_X0 + 12.0 + while hx < NPARK_X1 - 14.0: + if absf(hx) > ROAD_HALF + 9.0 and absf(hx - pond.x) > 18.0: + _hedge(Vector2(hx, cz - 14.0), Vector2(hx + 11.0, cz - 14.0)) + hx += _rng.randf_range(24.0, 34.0) + + # Social and service assets are grouped at path nodes, which makes the park + # read as a sequence of usable places rather than props scattered on lawn. + _picnic_table(Vector3(43.0, Y_GRASS, cz - 7.0), 90.0) + _picnic_table(Vector3(57.0, Y_GRASS, cz + 7.0), 0.0) + _park_bin(Vector3(28.5, Y_GRASS, cz - 4.2)) + _park_bin(Vector3(-59.0, Y_GRASS, cz + 2.8)) + for p in [Vector3(-58.0, Y_GRASS, cz - 2.8), + Vector3(-10.0, Y_GRASS, cz - 2.8), + Vector3(31.0, Y_GRASS, cz + 2.8), + Vector3(70.0, Y_GRASS, cz - 2.8)]: + _park_lamp(p) + # Simple calisthenics bars on the east lawn, common in neighbourhood parks. + for x in [67.0, 70.0, 73.0]: + _post(Vector3(x, 1.25, cz + 8.0), 0.08, 2.5, M["metal_dark"], "metal", 8) + for x in [68.5, 71.5]: + _solid(Vector3(x, 2.25, cz + 8.0), Vector3(3.0, 0.10, 0.10), + M["red"], "metal") + + _spawn_points.append(Vector3(-40, 1.6, cz)) + _spawn_points.append(Vector3(46, 1.6, cz)) + _spawn_points.append(Vector3(pav.x, 1.6, pav.y)) + + +## ── The road round the park ────────────────────────────────────────────────── +## +## The shopping street used to stop dead at x = 52 because the railway's turn +## occupies everything east of it — a carriageway ending in mid-air, which is +## the same "the world is fake" tell as a road running into a hill. +## +## It now carries on east, turns north around the park's eastern end, and leaves +## through the north wall. That keeps it clear of the line (which is outside the +## map for every z north of -35) and gives the park a proper edge on two sides. +const PARK_RD_TURN_X := 76.0 +const PARK_RD_R := 26.0 +const PARK_RD_EXIT_X := PARK_RD_TURN_X + PARK_RD_R # 102 + + +## Centreline of that road: straight, a quarter circle, then straight north. +func _park_road_path() -> Array: + var pts: Array = [] + var step := 2.0 + var x := SHOP_ST_X1 - 6.0 + while x < PARK_RD_TURN_X: + pts.append([Vector2(x, SHOP_ST_Z), Vector2(1.0, 0.0)]) + x += step + # Turning left, toward -Z. Centre one radius north of where the turn starts. + var c := Vector2(PARK_RD_TURN_X, SHOP_ST_Z - PARK_RD_R) + var n := int(PARK_RD_R * PI * 0.5 / step) + for i in range(n + 1): + var th := (PI * 0.5) * float(i) / float(n) + pts.append([c + Vector2(sin(th), cos(th)) * PARK_RD_R, + Vector2(cos(th), -sin(th))]) + var z := SHOP_ST_Z - PARK_RD_R + while z > -HALF_Z - OUTFIELD * 0.4: + z -= step + pts.append([Vector2(PARK_RD_EXIT_X, z), Vector2(0.0, -1.0)]) + return pts + + +func _build_park_road() -> void: + var path := _park_road_path() + var half := SHOP_ST_HALF + for i in range(path.size() - 1): + var a: Vector2 = path[i][0] + var b: Vector2 = path[i + 1][0] + _chord_solid(a, b, Y_ROAD_EW - 0.1, 0.2, half * 2.0, M["road"], "concrete") + # Kerbs, offset along the local normal so they follow the turn. + var na := _normal_of(path[i][1]) + var nb := _normal_of(path[i + 1][1]) + for s in [-1.0, 1.0]: + _chord_solid(a + na * (half * s), b + nb * (half * s), 0.10, 0.20, + 0.12, M["kerb"], "concrete") + # Centre line. + for i in range(0, path.size() - 2, 3): + var a: Vector2 = path[i][0] + var b: Vector2 = path[i + 1][0] + _chord_deco(a, b, Y_ROAD_EW + 0.012, 0.02, 0.16, M["line_white"]) + + +## Is this point on the park road? Scenery has to know about it like any other. +func _near_park_road(p: Vector2, margin: float = SHOP_ST_HALF + 3.0) -> bool: + for e in _park_road_path(): + var q: Vector2 = e[0] + if absf(q.x - p.x) > margin or absf(q.y - p.y) > margin: + continue + if q.distance_to(p) < margin: + return true + return false + + +## ── The shrine precinct ────────────────────────────────────────────────────── +## +## Behind the shrine hall there was thirty metres of empty grass to the wall. +## It is the rest of the precinct now: an inner court behind a second torii, +## with the buildings a working shrine actually has — a 手水舎 to rinse at, a +## 社務所 that sells the charms, a rack for the 絵馬 — and a cedar grove closing +## it off. It gives the south-west a destination with a shape instead of a lawn. +const PRECINCT_Z0 := 62.0 +const PRECINCT_Z1 := 84.0 + + +func _build_shrine_precinct() -> void: + var cz := (PRECINCT_Z0 + PRECINCT_Z1) * 0.5 + var x0 := -HALF_X + 16.0 + # West of the main street, which runs the full length of the map at x = 0. + # Taken to +2 the gravel court was laid straight across the carriageway. + var x1 := -ROAD_HALF - PAVE_W - 2.0 + + # The court: gravel, with a stone-flagged path on the shrine's own axis. + _slab(Vector2((x0 + x1) * 0.5, cz), Vector2(x1 - x0, PRECINCT_Z1 - PRECINCT_Z0), + Y_GRASS, M["gravel"], "gravel") + _slab(Vector2(SHRINE_X, cz), Vector2(6.0, PRECINCT_Z1 - PRECINCT_Z0), + Y_GRASS + 0.02, M["stone"], "concrete") + # A cross-axis connects the office, purification pavilion, ema and fortune + # rack to the processional path. These used to sit as islands in open gravel. + _slab(Vector2(SHRINE_X - 14.0, PRECINCT_Z0 + 8.0), Vector2(28.0, 2.6), + Y_GRASS + 0.025, M["stone"], "concrete") + _slab(Vector2(SHRINE_X + 7.0, PRECINCT_Z0 + 8.0), Vector2(14.0, 2.6), + Y_GRASS + 0.025, M["stone"], "concrete") + + # The inner torii, on the same axis as the outer one. + _torii(Vector3(SHRINE_X, Y_GRASS, PRECINCT_Z0 + 2.0), 0.95) + # The hall belongs at the end of the processional axis. It used to stand in + # front of this court, leaving the detailed precinct hidden behind a blank + # rear wall and making the second torii point at nothing. + _build_shrine_hall(Vector3(SHRINE_X, Y_GRASS, PRECINCT_Z1 - 8.0)) + + # 手水舎 — the water pavilion: a roof on four posts over a stone basin. + var chz := Vector2(SHRINE_X - 13.0, cz - 4.0) + _slab(chz, Vector2(5.0, 5.0), Y_GRASS + 0.1, M["stone"], "concrete") + for sx in [-1.0, 1.0]: + for sz in [-1.0, 1.0]: + _post(Vector3(chz.x + sx * 1.9, 1.25, chz.y + sz * 1.9), 0.13, 2.5, + M["wood_dark"], "wood", 6) + _solid(Vector3(chz.x, 2.62, chz.y), Vector3(5.6, 0.2, 5.6), M["roof_shrine"], "wood") + _deco(_prism_mesh(Vector3(6.0, 1.2, 6.0)), M["roof_shrine"], + Vector3(chz.x, 3.3, chz.y)) + _solid(Vector3(chz.x, 0.55, chz.y), Vector3(2.6, 0.9, 1.5), M["stone_dark"], "concrete") + _deco(_box_mesh(Vector3(2.3, 0.12, 1.2)), M["water"], Vector3(chz.x, 1.02, chz.y)) + # Bamboo ladles rest across the basin instead of leaving it as an empty tub. + for z in [-0.34, 0.34]: + _deco(_cyl_mesh(0.035, 0.035, 1.65, 6), M["wood"], + Vector3(chz.x, 1.18, chz.y + z), Vector3(0, 0, 90)) + _deco(_cyl_mesh(0.16, 0.13, 0.16, 8), M["wood"], + Vector3(chz.x + 0.88, 1.18, chz.y + z)) + + # 社務所 — the shrine office. A long single-storey building with a deep + # verandah, closing the west side of the court. + var off := Vector2(SHRINE_X - 26.0, cz + 3.0) + _solid(Vector3(off.x, 1.7, off.y), Vector3(9.0, 3.4, 7.0), M["wall_cream"], "wood") + _deco(_prism_mesh(Vector3(10.4, 1.9, 8.4)), M["roof_shrine"], + Vector3(off.x, 4.35, off.y)) + _deco(_box_mesh(Vector3(10.6, 0.18, 8.6)), M["roof_shrine"], + Vector3(off.x, 3.46, off.y)) + _deco(_box_mesh(Vector3(7.4, 1.7, 0.1)), M["glass"], + Vector3(off.x, 1.85, off.y - 3.55)) + _solid(Vector3(off.x, 0.5, off.y - 4.4), Vector3(9.4, 0.16, 1.8), M["wood"], "wood") + for t in [-3.6, 0.0, 3.6]: + _post(Vector3(off.x + t, 1.9, off.y - 5.2), 0.12, 3.0, M["wood_dark"], "wood", 6) + SakuraSignage.label(_decor, "社務所", 0.34, SakuraPalette.INK, + Vector3(off.x, 2.9, off.y - 3.62), Vector3(0, 180, 0)) + # Ceremonial sake barrels and a charm counter animate the office frontage. + for i in range(4): + var barrel_x := off.x - 3.0 + float(i) * 2.0 + _solid(Vector3(barrel_x, 0.72, off.y - 5.4), Vector3(1.35, 1.35, 1.15), + M["wall_cream"], "wood") + for y in [0.22, 1.22]: + _deco(_box_mesh(Vector3(1.42, 0.10, 1.22)), M["wood_dark"], + Vector3(barrel_x, y, off.y - 5.4)) + _solid(Vector3(off.x + 5.8, 1.02, off.y - 5.35), Vector3(2.2, 0.12, 0.70), + M["wood"], "wood") + + # 絵馬 rack — a roofed frame hung with votive tablets. + var ema := Vector2(SHRINE_X + 12.0, cz - 2.0) + for sx in [-1.0, 1.0]: + _post(Vector3(ema.x + sx * 2.4, 1.1, ema.y), 0.11, 2.2, M["wood_dark"], "wood", 6) + _deco(_box_mesh(Vector3(5.6, 0.16, 1.0)), M["roof_shrine"], + Vector3(ema.x, 2.28, ema.y)) + for i in range(9): + _deco(_box_mesh(Vector3(0.34, 0.26, 0.04)), LevelMaterials.unlit(SakuraPalette.EMA), + Vector3(ema.x - 2.0 + float(i) * 0.5, 1.72, ema.y)) + _omikuji_rack(Vector3(SHRINE_X + 13.0, Y_GRASS, cz + 4.0)) + + # Guardians and banners establish a human-scale threshold behind the inner + # torii, while remaining outside the six-metre-wide central path. + for s in [-1.0, 1.0]: + _komainu(Vector3(SHRINE_X + s * 4.8, Y_GRASS, PRECINCT_Z0 + 5.5), 1.0) + _shrine_banner(Vector3(SHRINE_X + s * 7.2, Y_GRASS, PRECINCT_Z0 + 9.5), + "奉納" if s < 0.0 else "桜守") + + # Lanterns down the axis, and a cedar grove closing the back. + for i in range(5): + for s in [-1.0, 1.0]: + _stone_lantern(Vector3(SHRINE_X + s * 4.6, Y_GRASS, + PRECINCT_Z0 + 6.0 + float(i) * 4.0)) + var gx := x0 + 4.0 + while gx < x1 + 10.0: + if absf(gx - SHRINE_X) > 8.0 \ + and not _near_road_for_tree(Vector2(gx, PRECINCT_Z1 - 1.0)): + _cedar(Vector3(gx, 0.0, PRECINCT_Z1 - 1.0), _rng.randf_range(1.0, 1.5)) + gx += _rng.randf_range(6.0, 10.0) + + # A block wall along the precinct's east side, which is what separates a + # shrine's ground from the town beside it. + _block_wall(Vector2(x1 + 1.0, PRECINCT_Z0), Vector2(x1 + 1.0, PRECINCT_Z1), 2.0) + for z in [PRECINCT_Z0 + 4.0, PRECINCT_Z0 + 12.0, PRECINCT_Z0 + 18.0]: + _shrub(Vector3(x1 - 1.1, Y_GRASS, z)) + + _spawn_points.append(Vector3(SHRINE_X, 1.6, cz)) + _spawn_points.append(Vector3(off.x, 1.6, off.y - 7.0)) + _spawn_points.append(Vector3(SHRINE_X, 2.9, PRECINCT_Z1 - 11.0)) + + +func _build_shrine_hall(base: Vector3) -> void: + # Raised neighbourhood haiden: stone plinth, deep eaves, open veranda and a + # layered timber front. The small human-scale details are what keep this from + # reading as a generic brown house under a large roof. + _solid(base + Vector3(0, 0.62, 0), Vector3(11.4, 1.24, 8.6), + M["stone"], "concrete") + _solid(base + Vector3(0, 2.58, 1.15), Vector3(7.8, 2.8, 5.2), + M["wood"], "wood") + _deco(_box_mesh(Vector3(11.3, 0.22, 8.8)), M["wood_dark"], + base + Vector3(0, 4.02, 1.05)) + _deco(_prism_mesh(Vector3(10.8, 2.7, 8.4)), M["roof_shrine"], + base + Vector3(0, 5.34, 1.05)) + _deco(_box_mesh(Vector3(0.30, 0.24, 8.8)), M["metal_dark"], + base + Vector3(0, 6.70, 1.05)) + + # Veranda and post rhythm across the front elevation. + _solid(base + Vector3(0, 1.30, -2.72), Vector3(9.2, 0.20, 1.40), + M["wood_dark"], "wood") + for px in [-3.25, -1.10, 1.10, 3.25]: + _post(base + Vector3(px, 2.60, -2.02), 0.17, 4.0, + M["wood_dark"], "wood", 6) + # Sliding doors and transom lattice behind the veranda. + for px in [-2.55, -0.85, 0.85, 2.55]: + _deco(_box_mesh(Vector3(1.52, 2.20, 0.10)), M["wall_cream"], + base + Vector3(px, 2.52, -1.52)) + _deco(_box_mesh(Vector3(0.08, 2.28, 0.14)), M["wood_dark"], + base + Vector3(px - 0.80, 2.52, -1.58)) + for y in [1.72, 2.26, 2.80, 3.34]: + _deco(_box_mesh(Vector3(7.0, 0.07, 0.15)), M["wood_dark"], + base + Vector3(0, y, -1.60)) + + # Four broad stone steps lead onto the veranda. + for i in range(4): + _solid(base + Vector3(0, 0.15 + float(i) * 0.30, + -4.55 + float(i) * 0.46), Vector3(5.4, 0.30, 0.58), + M["stone"], "concrete") + + # Offering box, bell rope and the shrine name board at the focal point. + _solid(base + Vector3(0, 1.62, -2.96), Vector3(1.75, 0.82, 0.95), + M["wood_dark"], "wood") + for x in [-0.62, -0.31, 0.0, 0.31, 0.62]: + _deco(_box_mesh(Vector3(0.10, 0.07, 0.84)), M["wood"], + base + Vector3(x, 2.07, -2.96)) + _deco(_cyl_mesh(0.055, 0.055, 1.75, 8), M["rope"], + base + Vector3(0, 3.12, -3.00)) + _deco(_sphere_mesh(0.18, 8, 4), M["metal_warm"], + base + Vector3(0, 2.20, -3.00)) + _sign_board(base + Vector3(0, 3.78, -1.66), -1.0, "桜守神社", 0.30, + SakuraPalette.SHRINE_WOOD_DARK, SakuraPalette.EMA, 0.22) + + +## ── The east district ──────────────────────────────────────────────────────── +## +## The land the railway's turn opens up. North of the curve there is nothing but +## line; south of it there is a wedge big enough for a proper residential block, +## which is what stops the east half of the map being a second copy of the west +## half. It has its own two streets and three rows of houses, and the middle +## street is fronted from BOTH sides — the only two-sided residential street in +## the map, and the reason `_house` learned to face either way. +func _build_east_district() -> void: + # The north-south street off the south street, and the east-west one it + # feeds. Their pavements and kerbs come from the same helper every other + # street in the map uses. + var link_x0 := ROAD_HALF - 0.15 + _slab(Vector2(EAST_ST_X, (SOUTH_ST_Z + EAST_DIST_Z1) * 0.5), + Vector2(EAST_ST_HALF * 2, EAST_DIST_Z1 - SOUTH_ST_Z), Y_ROAD_NS, + M["road"], "concrete") + # Carry the branch all the way into the main-road edge. It previously began + # at EAST_DIST_X0 (x=10), leaving 5.5 m of grass and pavement between two + # pieces of road that were meant to form one T-junction. + var east_link := _slab(Vector2((link_x0 + EAST_DIST_X1) * 0.5, EAST_ST_Z), + Vector2(EAST_DIST_X1 - link_x0, EAST_ST_HALF * 2), Y_ROAD_EW, + M["road"], "concrete") + east_link.name = "EastStreetLink" + east_link.set_meta("east_street_link", true) + _street_edges(false, EAST_ST_X, EAST_ST_HALF, SOUTH_ST_Z, EAST_DIST_Z1, + [Vector2(EAST_ST_Z - EAST_ST_HALF - PAVE_W - 0.3, + EAST_ST_Z + EAST_ST_HALF + PAVE_W + 0.3), + Vector2(SOUTH_ST_Z - SOUTH_ST_HALF - PAVE_W - 0.3, + SOUTH_ST_Z + SOUTH_ST_HALF + PAVE_W + 0.3)]) + _street_edges(true, EAST_ST_Z, EAST_ST_HALF, link_x0, EAST_DIST_X1, + [Vector2(EAST_ST_X - EAST_ST_HALF - PAVE_W - 0.3, + EAST_ST_X + EAST_ST_HALF + PAVE_W + 0.3)]) + + # Match the established junction language: connected corner paving, tactile + # pads and a zebra across the side street. + var apron_span := PAVE_W + 0.60 + for sz in [-1.0, 1.0]: + var apron := Vector2(ROAD_HALF + apron_span * 0.5, + EAST_ST_Z + sz * (EAST_ST_HALF + apron_span * 0.5)) + var connector := _slab(apron, Vector2(apron_span, apron_span), Y_PAVE, + M["pave"], "concrete") + connector.name = "SidewalkConnector" + connector.set_meta("sidewalk_connector", true) + _tactile_dots(Vector2(ROAD_HALF + 1.3, + EAST_ST_Z + sz * (EAST_ST_HALF + 1.3)), Vector2(1.5, 1.5), Y_PAVE) + _zebra(Vector2(ROAD_HALF + 3.2, EAST_ST_Z), EAST_ST_HALF * 2.0 - 0.8, + 2.4, false, Y_ROAD_EW) + + # ── Two rows facing each other, set back far enough to have gardens ────── + # + # A house is not just its body: in front of it are 4.25 m of plot, a garden + # 3.4 m deep and a boundary wall — about 7.8 m all told. Placed ten metres + # off the centreline, the body cleared the road and the GARDEN did not, so + # the front walls and their cherry trees stood in the carriageway. The + # offsets below are measured from the far edge of the pavement, not from + # the middle of the street. + var garden_reach := 7.8 + var pave_edge := EAST_ST_HALF + PAVE_W + 0.2 + _house_row(EAST_DIST_X0 + 4.0, EAST_DIST_X1 - 10.0, + EAST_ST_Z - pave_edge - garden_reach, 1.0, 5) + _house_row(EAST_DIST_X0 + 8.0, EAST_DIST_X1 - 10.0, + EAST_ST_Z + pave_edge + garden_reach, -1.0, 11) + + # Street trees and a couple of vending machines, so the district reads as + # lived in rather than as a housing estate diagram. + var tx := EAST_DIST_X0 + 8.0 + while tx < EAST_DIST_X1 - 8.0: + if absf(tx - EAST_ST_X) > 8.0: + _cherry_tree(Vector3(tx, 0.0, EAST_ST_Z - EAST_ST_HALF - PAVE_W - 1.4), + _rng.randf_range(0.9, 1.3)) + tx += _rng.randf_range(11.0, 16.0) + _vending_on_street(true, EAST_ST_Z, EAST_ST_HALF, 1.0, EAST_DIST_X0 + 18.0, 1) + _vending_on_street(false, EAST_ST_X, EAST_ST_HALF, -1.0, EAST_ST_Z + 22.0, 2) + + _spawn_points.append(Vector3(EAST_ST_X, 1.6, EAST_ST_Z)) + _spawn_points.append(Vector3(EAST_DIST_X0 + 20.0, 1.6, EAST_ST_Z)) + _spawn_points.append(Vector3(EAST_DIST_X1 - 20.0, 1.6, EAST_DIST_Z1 - 16.0)) + + +## The detached-house generator: a two-storey box, a pitched roof with deep +## eaves, a garden wall with a gate, and a front garden. Same principle as the +## shop — a house is a handful of numbers and everything else is derived from +## its footprint. +## `facing` is the direction the front door looks. Everything below is written +## for a street to the NORTH (-Z); a house that fronts +Z is the same house +## built under a pivot turned 180°, which is why the whole body is in local +## coordinates about the origin. +func _house(centre: Vector3, w: float, idx: int, facing: float = -1.0, + plot_w: float = -1.0) -> void: + var pivot := Node3D.new() + pivot.name = "House" + pivot.position = centre + if facing > 0.0: + pivot.rotation_degrees = Vector3(0, 180, 0) + add_child(pivot) + var prev := _group + _group = pivot + _house_local(w, idx, w + 4.6 if plot_w < 0.0 else plot_w) + _group = prev + + +func _house_local(w: float, idx: int, plot_w: float) -> void: + var centre := Vector3.ZERO + var d := 8.5 + var h := 6.2 + _solid(centre + Vector3(0, h * 0.5, 0), Vector3(w, h, d), + _wall_mat(idx + 3), "concrete", Vector3.ZERO, "HouseBody") + + # The roof. A prism gives a clean faceted pitch with no UV work, and its + # ridge runs along Z so the gable faces the street. + var roof := _roof_mat(idx + 1) + _deco(_prism_mesh(Vector3(w + 1.2, 2.0, d + 1.2)), roof, centre + Vector3(0, h + 1.0, 0)) + # ── and the pitch has to be SOLID, not just drawn ──────────────────────── + # + # That prism is render-only, so for a while the only thing a player could + # stand on up here was the body's own flat top — which is INSIDE the visible + # roof. Anyone who grappled onto a house ended up standing in the middle of + # its tiles. Two rotated slabs give the pitch real collision and seal the + # flat top underneath, where nothing can now reach it. + var pitch_hw := (w + 1.2) * 0.5 + var pitch_angle := rad_to_deg(atan2(2.0, pitch_hw)) + var pitch_len := sqrt(pitch_hw * pitch_hw + 4.0) + var plot_half := plot_w * 0.5 + for s in [-1.0, 1.0]: + # Rotating about Z by +θ lifts the +X end, so the -X pitch — the one + # that rises toward the ridge at +X — takes +θ, and its mirror takes -θ. + _solid(centre + Vector3(-s * pitch_hw * 0.5, h + 0.9, 0), + Vector3(pitch_len, 0.3, d + 1.2), roof, "wood", + Vector3(0, 0, s * pitch_angle)) + # Eaves: a thin overhanging slab, and the deepest single shadow line on the + # building. A Japanese house is read by its eaves more than by its walls. + _deco(_box_mesh(Vector3(w + 1.4, 0.18, d + 1.4)), roof, centre + Vector3(0, h + 0.05, 0)) + + # The ridge cap and the barge boards on the gable — a pitched roof with no + # edge members reads as a solid wedge dropped on a box. + _deco(_box_mesh(Vector3(0.34, 0.22, d + 1.5)), M["concrete_dark"], + centre + Vector3(0, h + 2.02, 0)) + for s in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(w + 1.5, 0.14, 0.16)), M["trim_mat"], + centre + Vector3(0, h + 0.16, s * (d * 0.5 + 0.6))) + # The gutter along the eaves, with a downpipe off each end. + for s in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(w + 1.3, 0.13, 0.13)), M["metal"], + centre + Vector3(0, h - 0.02, s * (d * 0.5 + 0.66))) + _downpipe(centre.x + w * 0.5 + 0.1, centre.z - d * 0.5 - 0.66, h, -1.0, M["metal"]) + + # The street face. + var f := centre.z - d * 0.5 - 0.05 + var ground_win := Vector3(centre.x, 1.35, f) + var upper_win := Vector3(centre.x, 4.3, f) + _deco(_box_mesh(Vector3(w * 0.42, 1.9, 0.08)), M["glass"], ground_win) + _deco(_box_mesh(Vector3(w * 0.5, 1.3, 0.08)), M["glass_dark"], upper_win) + _window_trim(ground_win, w * 0.42, 1.9, -1.0, M["trim_mat"]) + _window_trim(upper_win, w * 0.5, 1.3, -1.0, M["trim_mat"]) + # Sliding-door and window mullions. + for t in [-0.34, 0.0, 0.34]: + _deco(_box_mesh(Vector3(0.07, 1.9, 0.10)), M["trim_mat"], + ground_win + Vector3(w * 0.42 * t, 0, -0.02)) + # The entrance: a door, its frame, a small canopy over it and a nameplate. + var door_x := centre.x - w * 0.34 + _deco(_box_mesh(Vector3(0.9, 2.0, 0.1)), M["wood"], Vector3(door_x, 1.0, f)) + _deco(_box_mesh(Vector3(1.06, 2.16, 0.14)), M["wood_dark"], Vector3(door_x, 1.06, f + 0.03)) + _deco(_box_mesh(Vector3(1.5, 0.1, 0.8)), _roof_mat(idx + 1), + Vector3(door_x, 2.3, f - 0.35), Vector3(-9, 0, 0)) + _deco(_box_mesh(Vector3(0.30, 0.13, 0.04)), M["white_board"], + Vector3(door_x + 0.66, 1.62, f - 0.06)) + # A ground-floor condenser and a meter, both by the door where they go. + _ac_unit(Vector3(centre.x + w * 0.36, 1.5, f), -1.0) + _meter_box(Vector3(door_x - 0.9, 1.2, f), -1.0) + + # ── The flank walls ────────────────────────────────────────────────────── + # + # A detached house is read from the side as often as from the front, and the + # render showed the gables as some of the largest empty surfaces left in the + # map. Unlike a shop these do not take an advertisement — a house is not + # advertising anything — so they get what a house actually has: a small + # high window on each floor, a downpipe, and the garden wall returning up + # the plot boundary to cut the wall off at the bottom. + for s in [-1.0, 1.0]: + var sx: float = centre.x + s * (w * 0.5 + 0.05) + for wy in [1.6, 4.4]: + var wc := Vector3(sx, wy, centre.z - d * 0.18) + _deco(_box_mesh(Vector3(0.08, 0.95, 1.3)), M["glass_dark"], wc) + # Sill and lintel, turned to lie on a wall facing along X. + _deco(_box_mesh(Vector3(0.20, 0.09, 1.5)), M["trim_mat"], + wc + Vector3(s * 0.05, -0.52, 0)) + _deco(_box_mesh(Vector3(0.14, 0.07, 1.46)), M["trim_mat"], + wc + Vector3(s * 0.04, 0.51, 0)) + _deco(_cyl_mesh(0.05, 0.05, h - 0.3, 6), M["metal"], + Vector3(sx + s * 0.07, (h - 0.3) * 0.5 + 0.2, centre.z + d * 0.42)) + # The boundary wall returning down the side of the plot. + _solid(Vector3(centre.x + s * plot_half, 0.72, + centre.z - d * 0.5 - 1.7), Vector3(0.24, 1.44, 3.4), + M["concrete"], "concrete") + + # Garden wall along the street, with a gap for the gate. Chest high, so it + # is cover you can shoot over but not run through — the most useful height + # there is, and also what every one of these houses actually has. + var wall_z := centre.z - d * 0.5 - 3.4 + var gate_x := centre.x + w * 0.22 + for seg in [Vector2(centre.x - plot_half, gate_x - 1.1), + Vector2(gate_x + 1.1, centre.x + plot_half)]: + if seg.y - seg.x < 0.6: + continue + _solid(Vector3((seg.x + seg.y) * 0.5, 0.72, wall_z), + Vector3(seg.y - seg.x, 1.44, 0.28), M["concrete"], "concrete") + _deco(_box_mesh(Vector3(seg.y - seg.x + 0.12, 0.1, 0.4)), M["concrete_dark"], + Vector3((seg.x + seg.y) * 0.5, 1.48, wall_z)) + for gs in [-1.0, 1.0]: + _post(Vector3(gate_x + gs * 1.1, 0.85, wall_z), 0.16, 1.7, + M["concrete_mid"], "concrete", 4) + + # The front garden, with a cherry over the wall on every third plot so the + # canopy overhangs the pavement. + _slab(Vector2(centre.x, centre.z - d * 0.5 - 1.8), Vector2(plot_w - 0.3, 3.0), + Y_GRASS, M["grass"], "grass") + if idx % 3 == 0: + _cherry_tree(Vector3(centre.x - w * 0.3, Y_GRASS, centre.z - d * 0.5 - 2.0), 1.1) + else: + _shrub(Vector3(centre.x - w * 0.3, Y_GRASS, centre.z - d * 0.5 - 2.0)) + + +func _shrub(pos: Vector3) -> void: + for i in range(3): + var r := _rng.randf_range(0.55, 0.85) + _deco(_sphere_mesh(r, 6, 3), M["leaf"] if i % 2 == 0 else M["leaf_deep"], + pos + Vector3(_rng.randf_range(-0.4, 0.4), r * 0.8, _rng.randf_range(-0.4, 0.4))) + + +# ── Cherry trees ───────────────────────────────────────────────────────────── + +## The one thing in the map that has to be right. +## +## A cherry tree in this style is NOT a trunk with a green ball on it recoloured +## pink. Three things make it read: +## +## 1. The canopy is a CLUSTER of faceted blobs, not one sphere. Overlapping +## spheres at different radii give the lumpy silhouette the ink pass then +## draws around — and the ink is doing most of the work here, because a +## second difference fires on every blob boundary INSIDE the canopy as well +## as on its outline, which is exactly where an animator draws. +## 2. It is lit on the high-key ramp (see the `blossom` material), so its +## shadow side stays pale pink instead of going grey. +## 3. The canopy is WIDE and LOW relative to the trunk. A cherry is not a +## conifer; a tall narrow one reads as a lollipop. +func _cherry_tree(base: Vector3, scale: float = 1.0) -> void: + var trunk_h := 2.6 * scale + var body := _post(base + Vector3(0, trunk_h * 0.5, 0), 0.24 * scale, trunk_h, + M["trunk_dark"], "wood", 6) + body.name = "Cherry" + + # Two or three main limbs, leaning out. They carry the canopy and they show + # through it from underneath, which is where a player standing under the tree + # is looking. + var limbs := 3 + for i in range(limbs): + var a := TAU * float(i) / float(limbs) + _rng.randf_range(-0.4, 0.4) + var lean := _rng.randf_range(28.0, 44.0) + var l := 1.9 * scale + _deco(_cyl_mesh(0.15 * scale, 0.09 * scale, l, 5), M["trunk"], + base + Vector3(cos(a) * 0.45 * scale, trunk_h + l * 0.34, sin(a) * 0.45 * scale), + Vector3(cos(a) * lean, 0, -sin(a) * lean)) + + # The canopy: one broad low centre blob, a ring of smaller ones pushed out + # and slightly down, and a couple of light-toned caps on top. The reference + # gets its blossom variation from three tones of pink rather than from a + # gradient, and so does this. + var top := base.y + trunk_h + 1.5 * scale + _deco(_sphere_mesh(2.5 * scale, 7, 4), M["blossom"], Vector3(base.x, top, base.z)) + var blobs := 7 + for i in range(blobs): + var a := TAU * float(i) / float(blobs) + _rng.randf_range(-0.3, 0.3) + var r := _rng.randf_range(1.3, 1.9) * scale + var dist := _rng.randf_range(1.6, 2.3) * scale + var mat: Material = M["blossom_deep"] if i % 3 == 0 else M["blossom"] + _deco(_sphere_mesh(r, 6, 3), mat, + Vector3(base.x + cos(a) * dist, + top + _rng.randf_range(-0.7, 0.5) * scale, + base.z + sin(a) * dist)) + for i in range(2): + _deco(_sphere_mesh(_rng.randf_range(1.0, 1.4) * scale, 6, 3), M["blossom_light"], + Vector3(base.x + _rng.randf_range(-1.0, 1.0) * scale, + top + 1.1 * scale, + base.z + _rng.randf_range(-1.0, 1.0) * scale)) + + # Fallen blossom underneath. A drift, not a disc: the ink pass draws its + # edge, so it wants an irregular outline, which overlapping rectangles give + # for nothing. + for i in range(4): + var a := _rng.randf_range(0.0, TAU) + var dd := _rng.randf_range(0.0, 1.8) * scale + _paint(Vector2(base.x + cos(a) * dd, base.z + sin(a) * dd), + Vector2(_rng.randf_range(1.6, 2.8) * scale, _rng.randf_range(1.6, 2.8) * scale), + base.y, M["petal_ground"]) + + +# ── The footbridge ─────────────────────────────────────────────────────────── + +func _build_footbridge() -> void: + # The map's high ground, and the only place that sees the whole railway. + # + # Its two staircases run EAST-WEST, along the lanes, not north-south across + # them. That is a gameplay decision as much as a real-bridge one: a stair + # facing the tracks would let a player climb under cover of the bridge deck + # itself, and the whole point of the strongest position on the map is that + # getting to it should be seen. + var y := BRIDGE_DECK_Y + _solid(Vector3(BRIDGE_X, y - 0.15, 0), Vector3(2.6, 0.3, BRIDGE_SPAN), + M["concrete_mid"], "metal") + + # ── Where each stair leaves the deck ───────────────────────────────────── + # + # The landings sit at the two ends and each stair leaves SIDEWAYS, along X. + # That means the parapet — which runs the length of the deck — has to open + # where a stair meets it. It did not, in the first build: the parapets ran + # unbroken for the full 22 m, so even a correctly-built flight would have + # arrived at a solid waist-high wall. + var land_z := BRIDGE_SPAN * 0.5 - 1.0 # ±10.0 + var gap_lo := land_z - 1.1 + var gap_hi := land_z + 1.1 + # North landing (-Z) exits WEST down the service alley; south (+Z) exits + # EAST along the verge. So each parapet opens at one end only. + var west_gap := Vector2(-gap_hi, -gap_lo) + var east_gap := Vector2(gap_lo, gap_hi) + + for s in [-1.0, 1.0]: + var gap: Vector2 = east_gap if s > 0.0 else west_gap + for seg in _segments(-BRIDGE_SPAN * 0.5, BRIDGE_SPAN * 0.5, [gap]): + _solid(Vector3(BRIDGE_X + s * 1.3, y + 0.5, (seg.x + seg.y) * 0.5), + Vector3(0.16, 1.0, seg.y - seg.x), M["metal"], "metal") + + # A light roof on posts. It reads the bridge from a distance, and it stops a + # player on the deck being visible from directly above. Posts skip the + # openings, for the same reason the parapet does. + for i in range(7): + var z := -BRIDGE_SPAN * 0.5 + 2.0 + float(i) * (BRIDGE_SPAN - 4.0) / 6.0 + for s in [-1.0, 1.0]: + var gap: Vector2 = east_gap if s > 0.0 else west_gap + if z > gap.x - 0.4 and z < gap.y + 0.4: + continue + _deco(_cyl_mesh(0.07, 0.07, 2.2, 5), M["metal_dark"], + Vector3(BRIDGE_X + s * 1.2, y + 1.3, z)) + _deco(_box_mesh(Vector3(3.0, 0.12, BRIDGE_SPAN)), M["metal"], + Vector3(BRIDGE_X, y + 2.45, 0)) + + # Piers, clear of the track either side. + for z in [-RAIL_HALF - 0.6, RAIL_HALF + 0.6]: + _solid(Vector3(BRIDGE_X, y * 0.5, z), Vector3(1.2, y, 1.2), M["concrete"], "concrete") + + # The landings, and the flights hanging off them. Each flight starts at the + # landing's outer edge and descends to the ground it serves. + for s in [-1.0, 1.0]: + var lz: float = s * land_z + _solid(Vector3(BRIDGE_X, y - 0.15, lz), Vector3(2.6, 0.3, 2.2), + M["concrete_mid"], "concrete") + var out_x := 1.0 if s > 0.0 else -1.0 + var foot := _stair_flight( + Vector3(BRIDGE_X + out_x * 1.3, y, lz), Y_PAVE, out_x, 2.4, 1) + # A slab of pavement at the foot, so the flight lands on something + # level whatever the ground under it happens to be. + _slab(Vector2(foot.x + out_x * 1.2, foot.z), Vector2(3.4, 2.8), Y_PAVE, + M["pave_alt"], "concrete") + + _spawn_points.append(Vector3(BRIDGE_X, y + 1.4, 0)) + + +# ── The station ────────────────────────────────────────────────────────────── + +func _build_station() -> void: + # A single side platform on the south track, with a canopy, a waiting room, + # a ramp up from the south street and a stair off the far end. The third + # crossing, and the only one that is a room — so it plays as a hold where + # the crossing plays as a dash. + var plat_y := 1.1 + var z0 := RAIL_HALF - 1.0 + var pd := 5.6 + var pz := z0 + pd * 0.5 + var plen := 30.0 + + _solid(Vector3(STATION_X, plat_y * 0.5, pz), Vector3(plen, plat_y, pd), + M["concrete"], "concrete") + _paint(Vector2(STATION_X, z0 + 0.45), Vector2(plen, 0.9), plat_y, M["tactile"]) + _paint(Vector2(STATION_X, z0 + 1.15), Vector2(plen, 0.1), plat_y, M["line_white"]) + # The platform face, dark, so the platform reads as raised from across the + # tracks — the angle most players will first see it from. + _deco(_box_mesh(Vector3(plen, plat_y, 0.1)), M["concrete_dark"], + Vector3(STATION_X, plat_y * 0.5, z0 - 0.05)) + + # ── The canopy, and why it is shorter than the platform ────────────────── + # + # It used to run the platform's full length, and that made the roof + # unreachable no matter where the access stair went: a stair rising to the + # canopy from underneath has to pass THROUGH the canopy to get on top of it. + # Leaving five metres of open platform at the east end gives the stair + # somewhere to stand, which is also how a real canopy is built — it stops + # short of the ramp end. + var can_len := 24.0 + var can_x := STATION_X - 2.0 + var can_e := can_x + can_len * 0.5 # the open end, x = 52 + for i in range(6): + var x := can_x - can_len * 0.46 + float(i) * (can_len * 0.92 / 5.0) + for s in [0.0, 1.0]: + _post(Vector3(x, plat_y + 1.6, z0 + 0.9 + s * 3.4), 0.1, 3.2, + M["metal_dark"], "metal", 6) + _solid(Vector3(can_x, plat_y + 3.4, pz + 0.2), + Vector3(can_len, 0.22, pd - 0.4), M["roof_slate"], "metal") + # A fascia along the track edge of the canopy — where a station's name board + # goes, and where the eye lands from the crossing. + _deco(_box_mesh(Vector3(can_len, 0.5, 0.1)), M["white_board"], + Vector3(can_x, plat_y + 3.1, z0 + 0.85)) + SakuraSignage.label(_decor, SakuraSignage.STATION_KANJI, 0.34, + SakuraPalette.INK, Vector3(STATION_X, plat_y + 3.1, z0 + 0.79), + Vector3(0, 180, 0)) + + # ── 駅名標 ──────────────────────────────────────────────────────────────── + # + # The station name board, and it is the most-read object on any Japanese + # platform: the name large in kana, the kanji and the romaji under it, and + # the neighbouring stations either side with arrows. Two of them, at + # opposite ends, facing the train — which is to say facing the crossing, + # which is where the player reads them from. + for s in [-1.0, 1.0]: + var bx: float = STATION_X + s * plen * 0.28 + var by := plat_y + 2.0 + var bz := z0 + 0.55 + _deco(_box_mesh(Vector3(3.5, 1.0, 0.1)), M["white_board"], Vector3(bx, by, bz)) + _deco(_box_mesh(Vector3(3.62, 0.11, 0.14)), LevelMaterials.unlit(SakuraPalette.TEAL), + Vector3(bx, by - 0.55, bz)) + var yaw := 180.0 + SakuraSignage.label(_decor, SakuraSignage.STATION_KANA, 0.30, + SakuraPalette.INK, Vector3(bx, by + 0.20, bz - 0.06), Vector3(0, yaw, 0)) + SakuraSignage.label(_decor, SakuraSignage.STATION_ROMAJI, 0.15, + SakuraPalette.INK, Vector3(bx, by - 0.14, bz - 0.06), Vector3(0, yaw, 0)) + # The neighbours, in the corners, with an arrow toward each. + SakuraSignage.label(_decor, "← " + SakuraSignage.STATION_PREV, 0.12, + SakuraPalette.TRIM, Vector3(bx + 1.05, by - 0.38, bz - 0.06), Vector3(0, yaw, 0)) + SakuraSignage.label(_decor, SakuraSignage.STATION_NEXT + " →", 0.12, + SakuraPalette.TRIM, Vector3(bx - 1.05, by - 0.38, bz - 0.06), Vector3(0, yaw, 0)) + # The post it stands on. + _deco(_cyl_mesh(0.06, 0.06, 1.5, 6), M["metal_dark"], + Vector3(bx, plat_y + 0.75, bz + 0.02)) + + # Platform number, hanging under the canopy, and a timetable board on the + # waiting-room wall. + _deco(_box_mesh(Vector3(0.62, 0.62, 0.08)), LevelMaterials.unlit(SakuraPalette.BLUE), + Vector3(STATION_X + 6.0, plat_y + 2.66, z0 + 1.4)) + SakuraSignage.label(_decor, "1", 0.36, SakuraPalette.WALL_WHITE, + Vector3(STATION_X + 6.0, plat_y + 2.66, z0 + 1.35), Vector3(0, 180, 0)) + + # The tactile strip along the platform edge, with its dots. + _tactile_dots(Vector2(STATION_X, z0 + 0.45), Vector2(plen - 0.6, 0.8), plat_y + 0.02) + + # Boarding marks painted on the platform: a bracket where each set of doors + # stops, with 乗車口 beside it. More lettering on the floor, and the detail + # that most says "platform" rather than "raised walkway". + for i in range(4): + var mx := STATION_X - 10.5 + float(i) * 7.0 + _paint(Vector2(mx, z0 + 1.5), Vector2(1.5, 0.09), plat_y, M["line_white"]) + for s in [-1.0, 1.0]: + _paint(Vector2(mx + s * 0.71, z0 + 1.72), Vector2(0.09, 0.45), + plat_y, M["line_white"]) + _road_text(SakuraSignage.PLATFORM_MARK, mx, z0 + 2.25, plat_y, 0.34, + SakuraPalette.LINE_WHITE, 0.0) + + # The waiting room: a small glazed box and the map's only fully enclosed + # interior, with two open ends so it cannot become a hole to hide in. + var wr := Vector3(STATION_X - 8.0, plat_y, pz + 0.6) + _solid(wr + Vector3(0, 1.35, 1.4), Vector3(5.0, 2.7, 0.16), M["concrete"], "glass") + for s in [-1.0, 1.0]: + _solid(wr + Vector3(s * 2.42, 1.35, 0.4), Vector3(0.16, 2.7, 2.0), + M["concrete"], "glass") + _deco(_box_mesh(Vector3(4.7, 1.5, 0.06)), M["glass"], wr + Vector3(0, 1.7, 1.31)) + _solid(wr + Vector3(0, 2.78, 0.6), Vector3(5.4, 0.16, 3.2), M["metal"], "metal") + # A bench, which is also the only crouch-height cover on the platform. + _solid(wr + Vector3(0, 0.45, 0.6), Vector3(3.4, 0.12, 0.5), M["wood"], "wood") + for s in [-1.4, 1.4]: + _deco(_box_mesh(Vector3(0.12, 0.45, 0.45)), M["metal_dark"], wr + Vector3(s, 0.22, 0.6)) + + # Ramp up from the south street at one end, stair off the other, so the + # platform always has two ways out of it. + _ramp_up(Vector3(STATION_X + plen * 0.5 - 3.0, Y_PAVE, pz + pd * 0.5 + 3.4), + plat_y - Y_PAVE, 3.0, 3.4) + # The stair hangs off the platform's own southern edge and descends away + # from it — it used to start at that edge and climb outward, so its top step + # ended a metre clear of the platform in mid-air. + _stair_flight(Vector3(STATION_X - plen * 0.5 + 2.0, plat_y, pz + pd * 0.5), + Y_PAVE, 1.0, 2.6, 0) + + # ── Onto the canopy ────────────────────────────────────────────────────── + # + # The docstring at the top of this function calls the station "a roof to + # fight over", and for a while that was simply untrue: the walkability probe + # found the canopy as a 46-cell island with no route onto it at all. A + # maintenance stair at the east end fixes that, and a caged access stair up + # the end of a platform canopy is a real thing rather than an apology. + var canopy_top := plat_y + 3.51 + # The flight stands on the OPEN platform beyond the canopy's east end and + # climbs back onto it, so nothing has to pass through the roof it serves. + _stair_flight(Vector3(can_e + 0.2, canopy_top, pz + 0.2), + plat_y, 1.0, 1.6, 1, 0.34) + # A guard rail round the canopy edge, so the roof is a position and not a + # slide off the side into the track. Open at the east end, where the stair + # arrives. + for s in [-1.0, 1.0]: + _solid(Vector3(can_x, canopy_top + 0.45, pz + 0.2 + s * (pd - 0.4) * 0.5), + Vector3(can_len, 0.9, 0.1), M["metal"], "metal") + _solid(Vector3(can_x - can_len * 0.5, canopy_top + 0.45, pz + 0.2), + Vector3(0.1, 0.9, pd - 0.4), M["metal"], "metal") + + _spawn_points.append(Vector3(STATION_X, plat_y + 1.6, pz)) + _spawn_points.append(Vector3(STATION_X + 10.0, plat_y + 1.6, pz + 1.5)) + + +## A shallow ramp — one rotated slab, for wheeled access and for anything that +## wants to be slid down at speed. +func _ramp_up(base: Vector3, height: float, width: float, run: float) -> void: + var angle := rad_to_deg(atan2(height, run)) + var length := sqrt(height * height + run * run) + _solid(base + Vector3(0, height * 0.5, -run * 0.5), Vector3(width, 0.3, length), + M["concrete_mid"], "concrete", Vector3(angle, 0, 0)) + for s in [-1.0, 1.0]: + _solid(base + Vector3(s * (width * 0.5 + 0.1), height * 0.5 + 0.5, -run * 0.5), + Vector3(0.16, 0.9, length), M["metal"], "metal", Vector3(angle, 0, 0)) + + +# ── The park and the shrine ────────────────────────────────────────────────── + +func _park_lamp(base: Vector3) -> void: + var pole := _post(base + Vector3(0, 1.65, 0), 0.09, 3.3, + M["metal_dark"], "metal", 8) + pole.name = "ParkLamp" + _deco(_box_mesh(Vector3(0.72, 0.18, 0.72)), M["metal_dark"], + base + Vector3(0, 3.26, 0)) + _deco(_box_mesh(Vector3(0.50, 0.42, 0.50)), M["lantern"], + base + Vector3(0, 3.02, 0)) + _deco(_cyl_mesh(0.13, 0.03, 0.18, 6), M["metal_dark"], + base + Vector3(0, 3.48, 0)) + + +func _park_bin(base: Vector3) -> void: + var body := _solid(base + Vector3(0, 0.46, 0), Vector3(0.64, 0.92, 0.58), + M["metal"], "metal") + body.name = "ParkBin" + _deco(_box_mesh(Vector3(0.50, 0.10, 0.42)), M["drain_dark"], + base + Vector3(0, 0.90, 0)) + _deco(_box_mesh(Vector3(0.30, 0.16, 0.04)), M["white_board"], + base + Vector3(0, 0.56, -0.31)) + + +func _picnic_table(base: Vector3, yaw: float = 0.0) -> void: + var pivot := Node3D.new() + pivot.position = base + pivot.rotation_degrees.y = yaw + _attach(pivot) + pivot.name = "PicnicTable" + var prev := _group + _group = pivot + _solid(Vector3(0, 0.78, 0), Vector3(3.1, 0.16, 1.05), M["wood"], "wood") + for z in [-1.02, 1.02]: + _solid(Vector3(0, 0.48, z), Vector3(3.2, 0.14, 0.48), M["wood"], "wood") + for x in [-1.05, 1.05]: + for z in [-0.56, 0.56]: + _solid(Vector3(x, 0.38, z), Vector3(0.16, 0.76, 0.16), + M["metal_dark"], "metal", Vector3(0, 0, 12.0 * signf(z))) + _group = prev + + +func _seesaw(base: Vector3, yaw: float = 0.0) -> void: + var pivot := Node3D.new() + pivot.name = "Seesaw" + pivot.position = base + pivot.rotation_degrees.y = yaw + _attach(pivot) + var prev := _group + _group = pivot + _post(Vector3(0, 0.52, 0), 0.18, 1.04, M["metal_dark"], "metal", 8) + _solid(Vector3(0, 0.98, 0), Vector3(5.0, 0.18, 0.34), + M["red"], "wood", Vector3(0, 0, 6.0)) + for x in [-2.05, 2.05]: + _solid(Vector3(x, 1.24 + x * 0.045, 0), Vector3(0.54, 0.12, 0.62), + M["wood"], "wood", Vector3(0, 0, 6.0)) + _post(Vector3(x - signf(x) * 0.35, 1.50 + x * 0.045, 0), 0.05, 0.55, + M["metal_dark"], "metal", 6) + _group = prev + + +func _climbing_frame(base: Vector3) -> void: + var marker := Node3D.new() + marker.name = "ClimbingFrame" + marker.position = base + _attach(marker) + for x in [-1.55, 1.55]: + for z in [-1.30, 1.30]: + _post(base + Vector3(x, 1.25, z), 0.08, 2.5, M["metal_dark"], "metal", 8) + for z in [-1.30, 1.30]: + _solid(base + Vector3(0, 2.46, z), Vector3(3.2, 0.13, 0.13), + M["red"], "metal") + for x in [-1.55, 1.55]: + _solid(base + Vector3(x, 2.46, 0), Vector3(0.13, 0.13, 2.7), + M["red"], "metal") + for i in range(5): + var x := -1.20 + float(i) * 0.60 + _solid(base + Vector3(x, 2.46, 0), Vector3(0.08, 0.08, 2.6), + M["metal"], "metal") + + +func _komainu(base: Vector3, facing: float = -1.0) -> void: + var pedestal := _solid(base + Vector3(0, 0.34, 0), Vector3(1.10, 0.68, 1.10), + M["stone"], "concrete") + pedestal.name = "Komainu" + _deco(_sphere_mesh(0.48, 7, 4), M["stone_warm"], base + Vector3(0, 1.05, 0)) + _deco(_sphere_mesh(0.37, 7, 4), M["stone_warm"], + base + Vector3(0, 1.55, facing * 0.15)) + for x in [-0.22, 0.22]: + _deco(_cyl_mesh(0.09, 0.03, 0.23, 4), M["stone_dark"], + base + Vector3(x, 1.88, facing * 0.12)) + _deco(_box_mesh(Vector3(0.34, 0.10, 0.16)), M["stone_dark"], + base + Vector3(0, 1.40, facing * 0.48)) + + +func _shrine_banner(base: Vector3, text: String = "奉納") -> void: + var pole := _post(base + Vector3(0, 1.65, 0), 0.055, 3.3, + M["wood_dark"], "wood", 6) + pole.name = "ShrineBanner" + _deco(_box_mesh(Vector3(0.78, 2.05, 0.04)), M["wall_cream"], + base + Vector3(0.44, 2.15, 0)) + _deco(_box_mesh(Vector3(0.10, 2.05, 0.06)), M["torii"], + base + Vector3(0.09, 2.15, 0)) + SakuraSignage.vertical_label(_decor, text, 0.23, SakuraPalette.RED_DEEP, + base + Vector3(0.45, 2.94, -0.035), Vector3(0, 180, 0)) + + +func _omikuji_rack(base: Vector3) -> void: + var marker := Node3D.new() + marker.name = "OmikujiRack" + marker.position = base + _attach(marker) + for x in [-1.8, 1.8]: + _post(base + Vector3(x, 1.1, 0), 0.09, 2.2, M["wood_dark"], "wood", 6) + for y in [0.75, 1.18, 1.61, 2.04]: + _solid(base + Vector3(0, y, 0), Vector3(3.7, 0.06, 0.08), M["wood"], "wood") + for i in range(15): + var x := -1.55 + float(i % 8) * 0.44 + var y := 0.89 + float(i / 8) * 0.66 + _deco(_box_mesh(Vector3(0.07, 0.34, 0.03)), M["white_board"], + base + Vector3(x, y, -0.06)) + + +func _mini_shrine(base: Vector3) -> void: + var body := _solid(base + Vector3(0, 1.05, 0), Vector3(2.2, 1.8, 1.8), + M["wood"], "wood") + body.name = "Hokora" + _deco(_prism_mesh(Vector3(3.0, 1.15, 2.7)), M["roof_shrine"], + base + Vector3(0, 2.45, 0)) + _deco(_box_mesh(Vector3(3.2, 0.15, 2.9)), M["roof_shrine"], + base + Vector3(0, 1.90, 0)) + _deco(_box_mesh(Vector3(0.95, 1.25, 0.10)), M["wall_cream"], + base + Vector3(0, 1.05, -0.96)) + _solid(base + Vector3(0, 0.16, -1.35), Vector3(2.8, 0.32, 1.4), + M["stone"], "concrete") + + +func _sacred_tree(base: Vector3) -> void: + var trunk := _post(base + Vector3(0, 2.5, 0), 0.62, 5.0, + M["trunk_dark"], "wood", 9) + trunk.name = "SacredTree" + for p in [Vector3(-1.5, 5.0, 0), Vector3(1.3, 5.3, 0.2), Vector3(0, 6.0, -0.4)]: + _deco(_sphere_mesh(2.5, 8, 4), M["leaf_deep"], base + p) + _deco(_cyl_mesh(0.72, 0.72, 0.16, 10), M["rope"], base + Vector3(0, 2.55, 0)) + for x in [-0.42, 0.0, 0.42]: + _deco(_box_mesh(Vector3(0.16, 0.52, 0.04)), M["white_board"], + base + Vector3(x, 2.18, -0.64), Vector3(0, 0, 12.0 * signf(x))) + + +func _build_park() -> void: + # Grass, split around the main street: a lawn does not run across a road. + # The park is the WEST half of the south side now — the east half is the + # housing district the railway's turn opened up, and the two would otherwise + # be laid on top of each other. + for seg in _segments(-HALF_X + 12.0, EAST_DIST_X0 - 4.0, + [Vector2(-ROAD_HALF - PAVE_W - 0.3, ROAD_HALF + PAVE_W + 0.3)]): + _slab(Vector2((seg.x + seg.y) * 0.5, PARK_Z), Vector2(seg.y - seg.x, 20.0), + Y_GRASS, M["grass"], "grass") + + # The playground, shrine approach and south-street pavement now form one + # continuous pedestrian sequence instead of three isolated clearings. + var play_entry_z := PARK_Z - 8.0 + _slab(Vector2((-72.0 + SHRINE_X) * 0.5, play_entry_z), + Vector2(absf(-72.0 - SHRINE_X), 3.0), Y_GRASS + 0.025, + M["gravel"], "gravel") + _slab(Vector2(SHRINE_X, play_entry_z), Vector2(9.0, 7.0), + Y_GRASS + 0.03, M["stone"], "concrete") + + # The shrine sits on its own axis, deliberately NOT the main street's: an + # approach you have to turn off the road to find is worth more than one you + # can see down from the crossing, and it gives the south-west corner a + # destination of its own. + # The 参道 runs the whole way from the south street's pavement to the shrine + # steps — 27.2 is the outer edge of that pavement. It used to start at the + # park boundary, which left the approach beginning nowhere and reachable + # only by walking around the houses. + var approach_z0 := 27.2 + var approach_z1 := PRECINCT_Z1 - 7.0 + _slab(Vector2(SHRINE_X, (approach_z0 + approach_z1) * 0.5), + Vector2(5.0, approach_z1 - approach_z0), Y_GRASS + 0.02, + M["gravel"], "gravel") + # Grass either side of the stretch that crosses the old house row, so the + # approach reads as part of the shrine's ground rather than as a path laid + # over bare earth. + for s in [-1.0, 1.0]: + _slab(Vector2(SHRINE_X + s * 6.0, (approach_z0 + 40.0) * 0.5), + Vector2(7.0, 40.0 - approach_z0), Y_GRASS, M["grass"], "grass") + # Low flanking walls, which is what a 参道 actually has, and which stop the + # approach reading as a strip of gravel in an open field. + for s in [-1.0, 1.0]: + _solid(Vector3(SHRINE_X + s * 2.8, Y_GRASS + 0.35, + (approach_z0 + PARK_Z - 9.0) * 0.5), + Vector3(0.35, 0.7, PARK_Z - 9.0 - approach_z0), + M["stone"], "concrete") + + # The torii straddling the path. The single most saturated object in the map + # and the only one allowed to be — it is what the eye goes to from anywhere + # on the south side. + _torii(Vector3(SHRINE_X, Y_GRASS, PARK_Z - 8.0), 1.15) + for s in [-1.0, 1.0]: + _komainu(Vector3(SHRINE_X + s * 5.0, Y_GRASS, PARK_Z - 4.5), 1.0) + _shrine_banner(Vector3(SHRINE_X + s * 6.2, Y_GRASS, PARK_Z + 1.0), + "奉納" if s < 0.0 else "桜守") + + # The hall itself now closes the precinct at the far end of this axis; keeping + # this first lawn open lets the torii and lantern procession read together. + # Stone lanterns flanking the path — small, dark, hard props that break up + # an otherwise open lawn. + for i in range(4): + for s in [-1.0, 1.0]: + _stone_lantern(Vector3(SHRINE_X + s * 4.2, Y_GRASS, PARK_Z - 6.0 + float(i) * 3.6)) + + # The cherry grove: the densest blossom in the map, deliberately, so the + # south lane's long angles are broken by cover you can move through but not + # see through. + for i in range(12): + var a := TAU * float(i) / 12.0 + var r := _rng.randf_range(8.0, 12.0) + var p := Vector3(cos(a) * r * 2.2, Y_GRASS, PARK_Z + sin(a) * r * 0.7) + # The grove has to stay inside the park's own ground. It still spanned + # x = +-26 after the park lawn was cut back to the west half, so its + # eastern trees were growing in the east district's street. + if absf(p.x) < 8.0 or absf(p.x - SHRINE_X) < 3.6 or p.x > EAST_DIST_X0 - 6.0 or p.x < -HALF_X + 14.0: + continue + _cherry_tree(p, _rng.randf_range(0.95, 1.45)) + + for i in range(4): + var bx := -14.0 + float(i) * 14.0 + if absf(bx) < 8.0: + continue + _solid(Vector3(bx, 0.55, PARK_Z - 7.0), Vector3(2.4, 0.12, 0.5), M["wood"], "wood") + for s in [-0.95, 0.95]: + _deco(_box_mesh(Vector3(0.14, 0.5, 0.5)), M["metal_dark"], + Vector3(bx + s, 0.35, PARK_Z - 7.0)) + + # Secondary destinations fill the two lawns without compromising the clear + # ceremonial axis: a sacred tree and hokora to discover, and a picnic node + # linking the playground to the shrine. + _sacred_tree(Vector3(-46.0, Y_GRASS, PARK_Z + 6.0)) + _mini_shrine(Vector3(-11.5, Y_GRASS, PARK_Z + 7.0)) + _picnic_table(Vector3(-48.0, Y_GRASS, PARK_Z - 3.0), 90.0) + _park_bin(Vector3(-55.0, Y_GRASS, play_entry_z + 2.3)) + for x in [-62.0, -49.0, -36.0]: + _park_lamp(Vector3(x, Y_GRASS, play_entry_z - 2.3)) + + _build_park_playground() + + _spawn_points.append(Vector3(-16, 1.6, PARK_Z)) + _spawn_points.append(Vector3(18, 1.6, PARK_Z + 4.0)) + + +func _build_park_playground() -> void: + # A compact neighbourhood playground with a real entrance, perimeter and + # activity zones. Every moving/play surface is derived from shared datums so + # chains meet beams, slides meet platforms and borders close at their corners. + var c := Vector2(-72.0, PARK_Z + 1.0) + _slab(c, Vector2(30.0, 17.0), Y_GRASS + 0.015, M["dirt"], "gravel") + _slab(Vector2(-72.0, (27.2 + PARK_Z - 7.0) * 0.5), + Vector2(3.2, PARK_Z - 7.0 - 27.2), Y_GRASS + 0.025, + M["gravel"], "gravel") + + # The name board sits in a planted verge west of the entrance, never across + # the entry path or on the south-street pavement. + for x in [-97.2, -90.8]: + var post := _post(Vector3(x, 1.15, PARK_Z - 6.5), 0.10, 2.3, + M["metal_dark"], "metal", 6) + post.name = "ParkSign" + post.set_meta("ground_sign", true) + _sign_board(Vector3(-94.0, 1.72, PARK_Z - 6.5), -1.0, + "さくら児童公園", 0.28, SakuraPalette.WALL_SAGE, SakuraPalette.INK, 0.26) + + # Low perimeter rail with an eight-metre opening aligned to the entry path. + for z in [PARK_Z - 7.5, PARK_Z + 9.5]: + var spans := [Vector2(-87.0, -76.0), Vector2(-68.0, -57.0)] \ + if z < PARK_Z else [Vector2(-87.0, -57.0)] + for span in spans: + _solid(Vector3((span.x + span.y) * 0.5, 0.72, z), + Vector3(span.y - span.x, 0.10, 0.10), M["wood_dark"], "wood") + for x in [span.x, span.y]: + _post(Vector3(x, 0.62, z), 0.07, 1.24, M["wood_dark"], "wood", 6) + for x in [-87.0, -57.0]: + _solid(Vector3(x, 0.72, PARK_Z + 1.0), Vector3(0.10, 0.10, 17.0), + M["wood_dark"], "wood") + for z in [PARK_Z - 7.5, PARK_Z + 1.0, PARK_Z + 9.5]: + _post(Vector3(x, 0.62, z), 0.07, 1.24, M["wood_dark"], "wood", 6) + + # Two-bay swing. A single overhead beam establishes the chain positions; + # four supports land directly below its ends and every seat uses two chains. + var swing_z := PARK_Z + 0.5 + var swing_x0 := -85.0 + var swing_x1 := -76.0 + for x in [swing_x0, swing_x1]: + for z in [swing_z - 1.15, swing_z + 1.15]: + _post(Vector3(x, 1.55, z), 0.11, 3.1, M["metal_dark"], "metal", 8) + var swing_beam := _solid(Vector3((swing_x0 + swing_x1) * 0.5, 3.05, swing_z), + Vector3(swing_x1 - swing_x0 + 0.3, 0.18, 0.18), M["metal_dark"], "metal") + swing_beam.name = "SwingBeam" + for seat_x in [-82.5, -78.5]: + for chain_x in [seat_x - 0.52, seat_x + 0.52]: + var chain := _deco(_cyl_mesh(0.025, 0.025, 1.55, 6), M["metal"], + Vector3(chain_x, 2.24, swing_z)) + chain.name = "SwingChain" + var seat := _solid(Vector3(seat_x, 1.45, swing_z), + Vector3(1.25, 0.10, 0.50), M["wood"], "wood") + seat.name = "SwingSeat" + + # Slide, platform and ladder share the same top height and z datum. + var slide_x := -61.5 + var slide_top_z := PARK_Z + 3.0 + var slide_top := 1.35 + var platform_depth := 2.2 + var slide_platform := _solid(Vector3(slide_x, slide_top, slide_top_z), + Vector3(2.4, 0.18, platform_depth), M["metal"], "metal") + slide_platform.name = "SlidePlatform" + for x in [slide_x - 1.0, slide_x + 1.0]: + _post(Vector3(x, slide_top * 0.5, slide_top_z), 0.09, slide_top, + M["metal_dark"], "metal", 8) + var run := 4.6 + var drop := slide_top - 0.13 + var slope_len := sqrt(run * run + drop * drop) + # Put the incline's high endpoint on the near platform edge instead of + # burying half of the slide inside the platform. + var slope_cz := slide_top_z - platform_depth * 0.5 - run * 0.5 + var slide_slope := _solid(Vector3(slide_x, slide_top * 0.5 + 0.06, slope_cz), + Vector3(1.1, 0.14, slope_len), + M["metal"], "metal", Vector3(-rad_to_deg(atan2(drop, run)), 0, 0)) + slide_slope.name = "SlideSlope" + for x in [slide_x - 0.58, slide_x + 0.58]: + _deco(_box_mesh(Vector3(0.09, 0.34, slope_len)), M["red"], + Vector3(x, slide_top * 0.5 + 0.22, slope_cz), + Vector3(-rad_to_deg(atan2(drop, run)), 0, 0)) + # Ladder behind the platform, with rungs landing between its two uprights. + for x in [slide_x - 0.75, slide_x + 0.75]: + _post(Vector3(x, slide_top * 0.5, slide_top_z + 1.45), 0.06, + slide_top + 0.3, M["metal_dark"], "metal", 8) + for y in [0.35, 0.68, 1.01, 1.34]: + _solid(Vector3(slide_x, y, slide_top_z + 1.45), Vector3(1.55, 0.07, 0.07), + M["metal"], "metal") + + # Sandpit with a closed four-sided timber curb. + var sand := Vector2(-63.0, PARK_Z + 7.2) + var sandpit := _slab(sand, Vector2(8.0, 3.6), + Y_GRASS + 0.025, M["stone_warm"], "gravel") + sandpit.name = "Sandpit" + for s in [-1.0, 1.0]: + _solid(Vector3(sand.x + s * 4.0, 0.18, sand.y), + Vector3(0.18, 0.34, 4.2), M["wood"], "wood") + _solid(Vector3(sand.x, 0.18, sand.y + s * 1.8), + Vector3(8.2, 0.34, 0.18), M["wood"], "wood") + + # More than one kind of play: climbing frame, seesaw and stepping pods form + # a central activity band without blocking the entrance or slide runout. + _climbing_frame(Vector3(-71.5, Y_GRASS, PARK_Z - 2.0)) + _seesaw(Vector3(-73.0, Y_GRASS, PARK_Z + 6.3), 8.0) + for i in range(5): + _deco(_cyl_mesh(0.42, 0.42, 0.10, 10), + M["red"] if i % 2 == 0 else M["yellow"], + Vector3(-68.5 + float(i) * 1.2, Y_GRASS + 0.07, PARK_Z - 5.2)) + + # Drinking fountain, two benches, bins and lamps support the play space. + _solid(Vector3(-84.8, 0.70, PARK_Z + 7.0), Vector3(0.65, 1.35, 0.65), + M["stone"], "concrete") + _deco(_box_mesh(Vector3(0.45, 0.10, 0.45)), M["water"], + Vector3(-84.8, 1.40, PARK_Z + 7.0)) + for bench_x in [-82.0, -62.0]: + _solid(Vector3(bench_x, 0.58, PARK_Z - 6.0), Vector3(3.6, 0.14, 0.56), + M["wood"], "wood") + for x in [bench_x - 1.45, bench_x + 1.45]: + _deco(_box_mesh(Vector3(0.14, 0.55, 0.52)), M["metal_dark"], + Vector3(x, 0.35, PARK_Z - 6.0)) + _park_bin(Vector3(-76.0, Y_GRASS, PARK_Z - 5.8)) + _park_lamp(Vector3(-85.0, Y_GRASS, PARK_Z - 5.8)) + _park_lamp(Vector3(-59.0, Y_GRASS, PARK_Z - 5.8)) + + +func _torii(base: Vector3, scale: float) -> void: + var h := 5.4 * scale + var w := 4.6 * scale + for s in [-1.0, 1.0]: + _post(base + Vector3(s * w * 0.5, h * 0.5, 0), 0.24 * scale, h, M["torii"], "wood", 8) + # The kasagi — the top lintel, wider than the pillars, in two members. + _deco(_box_mesh(Vector3(w + 1.9 * scale, 0.34 * scale, 0.6 * scale)), M["torii"], + base + Vector3(0, h + 0.1 * scale, 0)) + _deco(_box_mesh(Vector3(w + 2.3 * scale, 0.2 * scale, 0.7 * scale)), M["torii"], + base + Vector3(0, h + 0.34 * scale, 0)) + # The nuki — the lower tie beam — and the strut between the two. + _deco(_box_mesh(Vector3(w + 0.7 * scale, 0.26 * scale, 0.4 * scale)), M["torii"], + base + Vector3(0, h - 1.1 * scale, 0)) + _deco(_box_mesh(Vector3(0.4 * scale, 1.2 * scale, 0.34 * scale)), M["torii"], + base + Vector3(0, h - 0.5 * scale, 0)) + + +func _stone_lantern(base: Vector3) -> void: + _deco(_cyl_mesh(0.42, 0.34, 0.3, 6), M["stone"], base + Vector3(0, 0.15, 0)) + _deco(_cyl_mesh(0.17, 0.17, 1.0, 6), M["stone"], base + Vector3(0, 0.8, 0)) + _deco(_box_mesh(Vector3(0.62, 0.5, 0.62)), M["stone"], base + Vector3(0, 1.55, 0)) + # The light box is drawn, not lit: a flat warm block inside a stone frame. + _deco(_box_mesh(Vector3(0.44, 0.36, 0.44)), M["lantern"], base + Vector3(0, 1.55, 0)) + # A four-sided cone is a pyramid, which is the cap a 灯籠 actually has. + _deco(_cyl_mesh(0.62, 0.06, 0.36, 4), M["stone_warm"], base + Vector3(0, 1.98, 0)) + _deco(_sphere_mesh(0.13, 6, 3), M["stone_warm"], base + Vector3(0, 2.2, 0)) + + +# ── Street furniture ───────────────────────────────────────────────────────── + +func _build_street_furniture() -> void: + # Vending machines. The reference is right that these are the single most + # characteristic prop of a Japanese street, and they are also perfect cover: + # 1.9 m tall, 1.1 wide, hard, and they glow, so they read at any distance. + # ── Where a vending machine actually stands ────────────────────────────── + # + # Against the BACK of the pavement, with its lit face turned to the road. + # The first pass hand-placed them at "street centre ± (road half + 1.4)", + # which is the exact middle of a 2.6 m pavement — so every one of them stood + # in the walking line with its back to half the people passing. They belong + # against the boundary they are served from, like every other piece of + # street furniture in the map. + # + # [east-west?, street centre, road half, side, distance along] + var spots := [ + [false, 0.0, ROAD_HALF, -1.0, -RAIL_HALF - 5.0], + [false, 0.0, ROAD_HALF, 1.0, RAIL_HALF + 5.0], + [true, SHOP_ST_Z, SHOP_ST_HALF, 1.0, -24.0], + [true, SHOP_ST_Z, SHOP_ST_HALF, -1.0, 18.0], + [true, SOUTH_ST_Z, SOUTH_ST_HALF, -1.0, STATION_X - 18.0], + [true, SOUTH_ST_Z, SOUTH_ST_HALF, 1.0, -34.0], + [true, SOUTH_ST_Z, SOUTH_ST_HALF, 1.0, 26.0], + ] + for i in range(spots.size()): + _vending_on_street(bool(spots[i][0]), float(spots[i][1]), float(spots[i][2]), + float(spots[i][3]), float(spots[i][4]), (i * 2) % 3) + + # Utility poles and their wires. These are the map's vertical grid — the + # reference leans on them hard, and without them a low suburban skyline has + # nothing crossing it. + var prev := Vector3.ZERO + var have_prev := false + var px := -HALF_X + 6.0 + while px < HALF_X: + if absf(px) > ROAD_HALF + 2.0: + var top := _utility_pole(Vector3(px, Y_PAVE, SOUTH_ST_Z + SOUTH_ST_HALF + 1.9)) + if have_prev: + _wire(prev, top) + prev = top + have_prev = true + else: + have_prev = false + px += 16.0 + + # Bollards along the crossing approach, and a convex traffic mirror on the + # corner — both real, both small hard silhouettes at eye level. + for s in [-1.0, 1.0]: + for i in range(4): + for sx in [-1.0, 1.0]: + _deco(_cyl_mesh(0.09, 0.09, 0.85, 6), M["yellow"], + Vector3(sx * (ROAD_HALF + 0.5), Y_PAVE + 0.42, + s * (RAIL_HALF + 2.8 + float(i) * 2.2))) + _traffic_mirror(Vector3(-ROAD_HALF - 1.8, Y_PAVE, -RAIL_HALF - 2.4)) + + # Bicycles in a rack outside the station. + # West of the station ramp, not on it: the rack used to run x = 54 to 57.5 + # at z = 13.6, and the ramp occupies x = 54 +- 1.5 over z = 11.6 to 15.0. + for i in range(6): + _bicycle(Vector3(STATION_X - 14.0 + float(i) * 0.7, Y_PAVE, SOUTH_ST_Z - 6.4)) + + # Street name plates at the junctions. Small, high, and the thing that tells + # a player which road they are actually on. Their poles stand in the verge + # BEHIND the pavement; the old offsets put all four directly on sidewalk + # corners, including one beside a zebra crossing. + var main_verge := ROAD_HALF + PAVE_W + 1.05 + var plates := [ + [Vector3(main_verge, 2.5, + SHOP_ST_Z + SHOP_ST_HALF + PAVE_W + 1.05), 0.0, 0], + [Vector3(-main_verge, 2.5, + SOUTH_ST_Z - SOUTH_ST_HALF - PAVE_W - 1.05), 0.0, 1], + [Vector3(main_verge, 2.5, RAIL_HALF + 4.0), 90.0, 2], + [Vector3(-main_verge, 2.5, -RAIL_HALF - 4.0), 90.0, 3], + ] + for p in plates: + var pos: Vector3 = p[0] + var yaw: float = p[1] + var sign_post := _post(pos - Vector3(0, 1.25, 0), 0.05, 2.5, + M["metal"], "metal", 6) + sign_post.name = "StreetSign" + sign_post.set_meta("ground_sign", true) + _street_plate(pos, SakuraSignage.STREET_NAMES[int(p[2])], yaw) + + # A public notice board by the crossing, and a litter bin on the platform + # approach. Both are the sort of thing that is always there and is never + # modelled, which is precisely why the absence of them is felt. + _deco(_box_mesh(Vector3(2.2, 1.3, 0.1)), M["white_board"], + Vector3(-ROAD_HALF - 4.6, 1.5, -RAIL_HALF - 4.2)) + _deco(_box_mesh(Vector3(2.36, 0.14, 0.16)), M["wood_dark"], + Vector3(-ROAD_HALF - 4.6, 2.22, -RAIL_HALF - 4.2)) + SakuraSignage.label(_decor, "お知らせ", 0.19, SakuraPalette.INK, + Vector3(-ROAD_HALF - 4.6, 1.94, -RAIL_HALF - 4.28), Vector3(0, 180, 0)) + for s in [-1.0, 1.0]: + _post(Vector3(-ROAD_HALF - 4.6 + s * 1.0, 0.6, -RAIL_HALF - 4.2), 0.06, 1.2, + M["wood_dark"], "wood", 6) + + +## Stand a machine against the back edge of a pavement, facing the road. +## +## `side` is which side of the street: for an east-west street +1 is the south +## pavement, for a north-south street +1 is the east one. The yaw follows from +## that, so a machine can never end up with its back to the carriageway. +func _vending_on_street(east_west: bool, centre: float, road_half: float, + side: float, along: float, kind: int) -> void: + # Back edge of the pavement, less half the machine's depth. + var off := (road_half + 0.12 + PAVE_W - 0.45) * side + if east_west: + # side +1 sits south of the road, so it looks north: yaw 180. + _vending(Vector3(along, Y_PAVE, centre + off), kind, + 180.0 if side > 0.0 else 0.0) + else: + # side +1 sits east of the road, so it looks west: yaw 270. + _vending(Vector3(centre + off, Y_PAVE, along), kind, + 270.0 if side > 0.0 else 90.0) + + +func _vending(base: Vector3, kind: int, yaw: float) -> void: + var panel: Material = [M["vend_red"], M["vend_teal"], M["blue"]][kind] + var b := _solid(base + Vector3(0, 0.95, 0), Vector3(1.1, 1.9, 0.72), + M["vend_white"], "metal", Vector3(0, yaw, 0)) + b.name = "Vending" + var yr := Vector3(0, yaw, 0) + var fwd := Vector3(sin(deg_to_rad(yaw)), 0, cos(deg_to_rad(yaw))) + # The lit display: a flat bright panel with a grid of drink blocks on it. + _deco(_box_mesh(Vector3(0.92, 1.05, 0.04)), panel, + base + Vector3(0, 1.32, 0) + fwd * 0.37, yr) + for r in range(3): + for c in range(4): + var col: Color = SakuraPalette.DRINKS[ + (kind * 5 + r * 4 + c) % SakuraPalette.DRINKS.size()] + var off := Vector3((float(c) - 1.5) * 0.21, 1.62 - float(r) * 0.3, 0) + off = off.rotated(Vector3.UP, deg_to_rad(yaw)) + _deco(_box_mesh(Vector3(0.16, 0.22, 0.03)), LevelMaterials.unlit(col), + base + off + fwd * 0.40, yr) + # The price strip under each shelf — the reference calls these out + # by name, and they are most of what makes a vending machine read as + # a vending machine rather than as a lit box. + var price: String = SakuraSignage.PRICES[(r + c) % SakuraSignage.PRICES.size()] + SakuraSignage.label(_decor, price, 0.055, SakuraPalette.INK, + base + off + Vector3(0, -0.135, 0).rotated(Vector3.UP, deg_to_rad(yaw)) + + fwd * 0.42, yr) + _deco(_box_mesh(Vector3(0.92, 0.2, 0.05)), M["ink"], + base + Vector3(0, 0.42, 0) + fwd * 0.37, yr) + _deco(_box_mesh(Vector3(0.92, 0.1, 0.04)), M["lantern"], + base + Vector3(0, 1.86, 0) + fwd * 0.37, yr) + # The brand band across the top, and the coin/return furniture down the side + # — small, dark, and what stops the front being one flat lit panel. + SakuraSignage.label(_decor, "自販機", 0.13, SakuraPalette.WALL_WHITE, + base + Vector3(0, 0.73, 0) + fwd * 0.40, yr) + _deco(_box_mesh(Vector3(0.20, 0.30, 0.04)), M["metal_dark"], + base + Vector3(0.34, 1.00, 0).rotated(Vector3.UP, deg_to_rad(yaw)) + fwd * 0.38, yr) + _deco(_box_mesh(Vector3(0.30, 0.10, 0.04)), M["metal"], + base + Vector3(-0.24, 0.94, 0).rotated(Vector3.UP, deg_to_rad(yaw)) + fwd * 0.38, yr) + + +func _utility_pole(base: Vector3) -> Vector3: + var h := 8.4 + _post(base + Vector3(0, h * 0.5, 0), 0.13, h, M["concrete_dark"], "concrete", 6) + # Two crossarms and a transformer can. The can is what makes the pole read + # as a utility pole rather than as a flagpole. + for i in range(2): + var dy := h - 0.7 - float(i) * 0.9 + _deco(_box_mesh(Vector3(0.09, 0.09, 2.2)), M["wood_dark"], base + Vector3(0, dy, 0)) + for s in [-0.9, 0.9]: + _deco(_cyl_mesh(0.07, 0.07, 0.2, 5), M["glass"], + base + Vector3(0, dy + 0.15, s)) + _deco(_cyl_mesh(0.28, 0.28, 0.8, 6), M["metal_dark"], base + Vector3(0.3, h - 2.4, 0)) + # Cable drops down the pole, the banded ID plate every pole carries, and a + # wrap-around advert — all of which turn a bare cylinder into a utility pole. + for s in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(0.045, h - 3.2, 0.045)), M["ink"], + base + Vector3(s * 0.15, (h - 3.2) * 0.5 + 0.4, 0.14)) + _deco(_box_mesh(Vector3(0.30, 0.44, 0.02)), M["white_board"], + base + Vector3(0, 2.5, 0.145)) + _deco(_box_mesh(Vector3(0.34, 0.9, 0.02)), LevelMaterials.unlit(SakuraPalette.RED), + base + Vector3(0, 4.1, 0.145)) + SakuraSignage.vertical_label(_decor, "電柱", 0.20, SakuraPalette.WALL_WHITE, + base + Vector3(0, 4.42, 0.16)) + return base + Vector3(0, h - 0.7, 0) + + +## A slack wire between two pole tops, drawn as two straight segments through a +## sag point. Two is enough: at this thickness the eye reads the droop, not the +## tessellation. +func _wire(a: Vector3, b: Vector3) -> void: + var pts := [a, (a + b) * 0.5 - Vector3(0, 0.9, 0), b] + for i in range(pts.size() - 1): + var p0: Vector3 = pts[i] + var p1: Vector3 = pts[i + 1] + var mid := (p0 + p1) * 0.5 + var mi := _deco(_box_mesh(Vector3(0.05, 0.05, (p1 - p0).length())), M["ink"], mid) + mi.look_at_from_position(mid, p1, Vector3.UP) + + +func _traffic_mirror(base: Vector3) -> void: + _post(base + Vector3(0, 1.5, 0), 0.07, 3.0, M["metal"], "metal", 6) + _deco(_cyl_mesh(0.62, 0.62, 0.12, 10), M["mirror_back"], + base + Vector3(0, 3.0, 0), Vector3(72, 0, 0)) + _deco(_cyl_mesh(0.54, 0.54, 0.06, 10), M["glass"], + base + Vector3(0, 3.02, -0.09), Vector3(72, 0, 0)) + + +func _bicycle(base: Vector3) -> void: + # The two wheels are separated along Z, so this bicycle's frame runs along + # Z and its axles run along X. (90, 0, 0) put them along Z — in line with + # the frame, which is a unicycle at best. + for s in [-0.5, 0.5]: + _deco(_cyl_mesh(0.33, 0.33, 0.04, 10), M["ink"], + base + Vector3(0, 0.33, s), Vector3(0, 0, 90)) + _deco(_box_mesh(Vector3(0.05, 0.05, 0.95)), M["teal"], base + Vector3(0, 0.55, 0)) + _deco(_box_mesh(Vector3(0.05, 0.42, 0.05)), M["teal"], base + Vector3(0, 0.72, -0.4)) + _deco(_box_mesh(Vector3(0.06, 0.06, 0.44)), M["ink"], + base + Vector3(0, 0.93, -0.4), Vector3(0, 90, 0)) + _deco(_box_mesh(Vector3(0.22, 0.07, 0.3)), M["ink"], base + Vector3(0, 0.86, 0.28)) + + +# ── Boundary ───────────────────────────────────────────────────────────────── + +## A municipal road closure at a boundary opening. The striped rail is an +## explicit visual full stop, but low enough to vault or grapple over so the +## movement system and five-second recovery rule still work. +func _boundary_road_gate(pos: Vector3, yaw: float, width: float = 11.5) -> void: + var a := deg_to_rad(yaw) + var across := Vector3(cos(a), 0, -sin(a)) + var forward := Vector3(sin(a), 0, cos(a)) + var yr := Vector3(0, yaw, 0) + for s in [-1.0, 1.0]: + var foot: Vector3 = pos + across * (float(s) * width * 0.5) + _post(foot + Vector3(0, 0.82, 0), 0.16, 1.64, + M["black"], "metal", 8) + _deco(_box_mesh(Vector3(0.48, 0.12, 0.48)), M["yellow"], + foot + Vector3(0, 1.68, 0)) + _deco(_cyl_mesh(0.12, 0.12, 0.12, 8), M["signal_red"], + foot + Vector3(0, 1.86, 0)) + + var stripes := 10 + var seg := width / float(stripes) + for i in range(stripes): + var mat: Material = M["yellow"] if i % 2 == 0 else M["black"] + var beam := _solid(pos + across * (-width * 0.5 + seg * (float(i) + 0.5)) + + Vector3(0, 1.08, 0), Vector3(seg + 0.03, 0.20, 0.20), + mat, "metal", yr, "BoundaryGate") + beam.set_meta("boundary_gate", true) + + var panel_pos := pos + Vector3(0, 1.56, 0) + _deco(_box_mesh(Vector3(3.7, 0.70, 0.12)), M["white_board"], panel_pos, yr) + for face in [-1.0, 1.0]: + SakuraSignage.label(_decor, "この先 立入禁止", 0.27, SakuraPalette.RED_DEEP, + panel_pos + forward * (face * 0.075), + Vector3(0, yaw + (180.0 if face < 0.0 else 0.0), 0)) + + +func _build_boundary() -> void: + # A real edge, not an invisible wall: a concrete flood wall, a cedar + # treeline behind it, and hills behind that — so the boundary reads as where + # the town stops rather than as where the level does. + var h := 5.0 + # ── Where the railway leaves ───────────────────────────────────────────── + # + # The wall has to open for it, at both ends. Straight out through the west + # wall at z = 0, and — since the line now turns — out through the EAST wall + # at whatever z the curve has reached by the time it gets there. That is + # computed from the path rather than written down, so moving the curve + # cannot leave the track clipping through solid concrete. + # ── The opening has to be as wide as the CROSSING, not as the corridor ─── + # + # The line leaves the west wall square on, so there a half-gap of + # RAIL_HALF + 1.5 is right. It leaves the EAST wall at 55 degrees, and a + # fourteen-metre corridor crossing a wall at that angle needs an opening of + # 14 / cos(55°) ≈ 24 m in the wall's own direction, not 14. Cutting the + # straight-on figure is why the wall clipped into the railway. + var west_gap := Vector2(-RAIL_HALF - 1.5, RAIL_HALF + 1.5) + var east_gap := west_gap + for entry in _rail_path(): + var p: Vector2 = entry[0] + if p.x >= HALF_X - 0.6 and p.x <= HALF_X + 0.6: + var t: Vector2 = entry[1] + var half: float = (RAIL_HALF + 1.5) / maxf(absf(t.x), 0.2) + east_gap = Vector2(p.y - half, p.y + half) + break + + # The roads leave too. A wall that seals every street at the map edge is the + # clearest possible statement that there is nothing beyond it; opening it + # where a carriageway meets it — and running the carriageway on out into the + # district beyond — is most of what makes the boundary read as the edge of a + # NEIGHBOURHOOD rather than the edge of the world. + var road_gap := ROAD_HALF + PAVE_W + 0.6 + var ew_gaps: Array = [ + Vector2(SHOP_ST_Z - SHOP_ST_HALF - PAVE_W - 0.6, + SHOP_ST_Z + SHOP_ST_HALF + PAVE_W + 0.6), + Vector2(SOUTH_ST_Z - SOUTH_ST_HALF - PAVE_W - 0.6, + SOUTH_ST_Z + SOUTH_ST_HALF + PAVE_W + 0.6), + ] + + for s in [-1.0, 1.0]: + var side_gaps: Array = [east_gap if s > 0.0 else west_gap] + # The south street runs out both sides; the shopping street only west, + # since east of SHOP_ST_X1 it does not exist. + side_gaps.append(ew_gaps[1]) + if s < 0.0: + side_gaps.append(ew_gaps[0]) + for seg in _segments(-HALF_Z - 1.0, HALF_Z + 1.0, side_gaps): + _solid(Vector3(s * HALF_X, h * 0.5, (seg.x + seg.y) * 0.5), + Vector3(1.0, h, seg.y - seg.x), M["concrete"], "concrete") + _deco(_box_mesh(Vector3(1.3, 0.24, seg.y - seg.x)), M["concrete_dark"], + Vector3(s * HALF_X, h + 0.1, (seg.x + seg.y) * 0.5)) + # The north wall opens twice: for the main street, and for the park + # road, which leaves on the far side of the park at PARK_RD_EXIT_X. + var ns_gaps: Array = [Vector2(-road_gap, road_gap)] + if s < 0.0: + ns_gaps.append(Vector2(PARK_RD_EXIT_X - road_gap, + PARK_RD_EXIT_X + road_gap)) + for seg in _segments(-HALF_X - 1.0, HALF_X + 1.0, ns_gaps): + _solid(Vector3((seg.x + seg.y) * 0.5, h * 0.5, s * HALF_Z), + Vector3(seg.y - seg.x, h, 1.0), M["concrete"], "concrete") + _deco(_box_mesh(Vector3(seg.y - seg.x, 0.24, 1.3)), M["concrete_dark"], + Vector3((seg.x + seg.y) * 0.5, h + 0.1, s * HALF_Z)) + + # Every road aperture gets a closure on the wall centreline. The railway + # apertures stay open because the rails, fence and overhead wire already say + # "infrastructure", while a clean road otherwise promises more playable map. + _boundary_road_gate(Vector3(-HALF_X, 0, SHOP_ST_Z), 90.0) + _boundary_road_gate(Vector3(-HALF_X, 0, SOUTH_ST_Z), 90.0) + _boundary_road_gate(Vector3(HALF_X, 0, SOUTH_ST_Z), 90.0) + _boundary_road_gate(Vector3(0, 0, -HALF_Z), 0.0) + _boundary_road_gate(Vector3(0, 0, HALF_Z), 0.0) + _boundary_road_gate(Vector3(PARK_RD_EXIT_X, 0, -HALF_Z), 0.0) + + _build_beyond() + + # Treeline: a dense band of dark cedar just behind the wall, which is what + # actually hides the seam. + for i in range(72): + var p: Vector3 + match i % 4: + 0: p = Vector3(_rng.randf_range(-HALF_X, HALF_X), 0, + -HALF_Z - _rng.randf_range(3.0, 13.0)) + 1: p = Vector3(_rng.randf_range(-HALF_X, HALF_X), 0, + HALF_Z + _rng.randf_range(3.0, 13.0)) + 2: p = Vector3(-HALF_X - _rng.randf_range(3.0, 13.0), 0, + _rng.randf_range(-HALF_Z, HALF_Z)) + _: p = Vector3(HALF_X + _rng.randf_range(3.0, 13.0), 0, + _rng.randf_range(-HALF_Z, HALF_Z)) + # This loop is where the trees standing in the middle of the road came + # from: it predates the roads leaving the map and never knew about them. + # The same is true of the railway, which no longer runs along z = 0. + if _near_road_for_tree(Vector2(p.x, p.z)) or _near_railway(Vector2(p.x, p.z)): + continue + _cedar(p, _rng.randf_range(0.8, 1.5)) + + _build_outfield() + + # Distant hills, in two rings at different values. That value step between + # near ridge and far ridge is the whole of aerial perspective in a cel + # image, and it costs sixteen meshes. + # + # They are squashed hard on Y and stretched on X, and sunk until only the + # top of each shows. A sphere left round reads as a boulder sitting on the + # horizon — what makes a RIDGE is that it is much wider than it is tall and + # that its base is never visible. + # `preview_backdrop` tells map_preview_capture these are painted scenery and + # not level, so they stay out of the bounds it frames the level-select card + # from. Without it the merged AABB is a kilometre across and the card comes + # out as a diorama floating in an empty sky. + for i in range(9): + var a := TAU * float(i) / 9.0 + var mi := _deco(_sphere_mesh(_rng.randf_range(44.0, 68.0), 9, 4), M["hill"], + Vector3(cos(a) * 230.0, -34.0, sin(a) * 205.0)) + mi.scale = Vector3(2.1, 0.62, 1.4) + mi.set_meta("preview_backdrop", true) + for i in range(7): + var a := TAU * (float(i) + 0.5) / 7.0 + var mi := _deco(_sphere_mesh(_rng.randf_range(70.0, 105.0), 9, 4), M["hill_far"], + Vector3(cos(a) * 380.0, -66.0, sin(a) * 345.0)) + mi.scale = Vector3(2.4, 0.58, 1.5) + mi.set_meta("preview_backdrop", true) + + +## The country outside the walls: real, walkable, and hilly. +## +## Everything here is SOLID, unlike the painted ridges on the horizon. That is +## the whole point — the combat volume already tells a player they have left, +## and the timer already kills them for staying out, but neither of those is any +## use if the moment they clear the wall they drop through the bottom of the +## world. Now they land on a hillside. +## +## It is deliberately empty: rolling ground, scattered cedar, no cover and no +## reason to be there. It should read as somewhere the map stops, not as more +## map. +func _build_outfield() -> void: + var inner_x := HALF_X + 6.0 + var inner_z := HALF_Z + 6.0 + + # ── No separate turf layer out here ────────────────────────────────────── + # + # There used to be four grass bands laid over the margin at y = 0.04, from + # when the base plate was a pale `dirt`. The plate is grass now, so those + # bands were both redundant AND four centimetres proud of it — which is + # exactly the hard line visible where the extended ground met the old + # ground. One continuous surface, one elevation, no seam. + + # ── Hills, and only beyond the suburb ──────────────────────────────────── + # + # Every mound is pushed out until its NEAR EDGE clears HILL_START, so no + # hillside can reach back into the built-up ring. Checking the centre is not + # enough for a disc forty metres across — that was the bug that put + # hillsides through the outfield's houses and across its roads. + for i in range(84): + var a := TAU * float(i) / 84.0 + _rng.randf_range(-0.04, 0.04) + var dir := Vector2(cos(a), sin(a)) + var r := _rng.randf_range(20.0, 44.0) + var p := dir * (HILL_START + r) + # Walk out along the ray until the whole disc is past HILL_START. + while _dist_to_town(p) - r < HILL_START and p.length() < 900.0: + p += dir * 6.0 + p += dir * _rng.randf_range(0.0, 90.0) + if _near_railway(p, r + RAIL_HALF + 4.0): + continue + _mound(p, r, _rng.randf_range(6.0, 18.0)) + # ── Cedar, in a band hugging the wall ──────────────────────────────────── + # + # Only the first fifteen metres. Scattered across the whole margin they + # landed in the middle of the roads leaving the map and inside the buildings + # beyond it — a tree standing in a carriageway is the most conspicuous + # possible way to say "this scenery does not know what it is standing on". + # The distance is carried by the mounds and the city instead, and this is + # just the treeline outside the boundary. + for i in range(140): + var a := TAU * float(i) / 140.0 + _rng.randf_range(-0.03, 0.03) + var dir := Vector2(cos(a), sin(a)) + # An absolute band four to twelve metres past the wall — between the + # boundary and where the suburb's first buildings start. + var p := dir * Vector2(inner_x, inner_z) + while _dist_to_town(p) < 4.0 and p.length() < 900.0: + p += dir * 2.0 + p += dir * _rng.randf_range(0.0, 8.0) + if _near_road_for_tree(p) or _near_railway(p): + continue + _cedar(Vector3(p.x, 0.0, p.y), _rng.randf_range(0.9, 1.7)) + + +## ── The city beyond the wall ───────────────────────────────────────────────── +## +## Everything here is outside the play volume and none of it is meant to be +## reached. It exists so that standing at the crossing and looking down any of +## the four roads shows a town CONTINUING rather than stopping. +## +## ── It is a grid, aligned to the roads that actually leave ─────────────────── +## +## The first version placed rows of blocks flanking imaginary streets that had +## nothing to do with the real ones — so the main street ran out through its gap +## in the wall and straight into the side of a building. The layout is now a +## proper grid whose lines ARE the continuing roads: x = 0 is the main street, +## z = SOUTH_ST_Z is the south street, and the rest of the grid is stepped off +## those at a fixed pitch. Blocks only ever fill the cells between, so no road +## can run into anything. +## +## ── And it is built out of the same architecture ───────────────────────────── +## +## Plain boxes with window bands read as a different game from the shophouses +## and pitched-roof houses inside the wall. The buildings out here use cut-down +## versions of the same two forms: parapet-and-awning shophouses on the street +## frontages, pitched-roof houses with eaves behind them. One collider each, +## everything else drawn — they are scenery, not level. +const BEYOND_PITCH := 46.0 # block + street +const BEYOND_STREET := 11.0 +const BEYOND_RINGS := 4 # how far the grid extends past the wall + + +func _build_beyond() -> void: + # Stops inside the suburb ring, so no carriageway ever runs into a hillside. + var road_out := CITY_REACH + 20.0 + + # ── The roads, continuing ──────────────────────────────────────────────── + # Main street north and south, the south street east and west, and the + # shopping street west. Same width, same paint, AND the same kerb and + # pavement — without those the carriageway simply changed into a strip of + # grey at the wall, which is why the roads read as not connecting. + for s in [-1.0, 1.0]: + _slab(Vector2(0, s * (HALF_Z + road_out * 0.5)), + Vector2(ROAD_HALF * 2, road_out), Y_ROAD_NS, M["road"], "concrete") + _street_edges(false, 0.0, ROAD_HALF, HALF_Z - 1.0, HALF_Z + road_out, []) \ + if s > 0.0 else \ + _street_edges(false, 0.0, ROAD_HALF, -HALF_Z - road_out, -HALF_Z + 1.0, []) + _slab(Vector2(s * (HALF_X + road_out * 0.5), SOUTH_ST_Z), + Vector2(road_out, SOUTH_ST_HALF * 2), Y_ROAD_EW, M["road"], "concrete") + _street_edges(true, SOUTH_ST_Z, SOUTH_ST_HALF, HALF_X - 1.0, + HALF_X + road_out, []) if s > 0.0 else \ + _street_edges(true, SOUTH_ST_Z, SOUTH_ST_HALF, -HALF_X - road_out, + -HALF_X + 1.0, []) + var z := HALF_Z + 4.0 + while z < HALF_Z + road_out - 4.0: + _paint(Vector2(0, s * z), Vector2(0.16, 3.0), Y_ROAD_NS, M["line_white"]) + z += 6.0 + var x := HALF_X + 4.0 + while x < HALF_X + road_out - 4.0: + _paint(Vector2(s * x, SOUTH_ST_Z), Vector2(3.0, 0.16), Y_ROAD_EW, + M["line_white"]) + x += 6.0 + _slab(Vector2(-(HALF_X + road_out * 0.5), SHOP_ST_Z), + Vector2(road_out, SHOP_ST_HALF * 2), Y_ROAD_EW, M["road"], "concrete") + _street_edges(true, SHOP_ST_Z, SHOP_ST_HALF, -HALF_X - road_out, + -HALF_X + 1.0, []) + + _beyond_grid() + + # ── The skyline ────────────────────────────────────────────────────────── + # Taller slabs well back, so the horizon has a city on it. Unlit and pale, + # like the ridges: at this distance they are atmosphere. + # Well beyond the hills, so nothing stands in one. + for i in range(18): + var a := TAU * float(i) / 18.0 + 0.19 + var hh := _rng.randf_range(30.0, 70.0) + var d := _rng.randf_range(1.35, 1.75) + _deco(_box_mesh(Vector3(_rng.randf_range(16.0, 34.0), hh, + _rng.randf_range(16.0, 34.0))), M["hill"], + Vector3(cos(a) * (HALF_X + OUTFIELD) * d, hh * 0.5, + sin(a) * (HALF_Z + OUTFIELD) * d)) + + +## The grid itself: streets stepped off the two roads that leave the map, and a +## block in every cell that is clear of the town, the railway and the roads. +func _beyond_grid() -> void: + var lim_x := HALF_X + OUTFIELD + 40.0 + var lim_z := HALF_Z + OUTFIELD + 40.0 + var path := _rail_path() + + # Street centrelines. Anchored on the real roads so the grid lines up with + # what comes out of the wall. + var xs: Array[float] = [] + var k := -BEYOND_RINGS + while k <= BEYOND_RINGS: + xs.append(0.0 + float(k) * BEYOND_PITCH) + k += 1 + var zs: Array[float] = [] + k = -BEYOND_RINGS + while k <= BEYOND_RINGS: + zs.append(SOUTH_ST_Z + float(k) * BEYOND_PITCH) + k += 1 + + # Draw the beyond-only streets, outside the town only. The two real ones are + # already laid above, at their own width. + for gx in xs: + if absf(gx) < 1.0: + continue + for seg in _segments(-lim_z, lim_z, [Vector2(-HALF_Z - 8.0, HALF_Z + 8.0)]): + _slab(Vector2(gx, (seg.x + seg.y) * 0.5), + Vector2(BEYOND_STREET, seg.y - seg.x), Y_ROAD_EW, + M["road"], "concrete") + for gz in zs: + if absf(gz - SOUTH_ST_Z) < 1.0: + continue + for seg in _segments(-lim_x, lim_x, [Vector2(-HALF_X - 8.0, HALF_X + 8.0)]): + _slab(Vector2((seg.x + seg.y) * 0.5, gz), + Vector2(seg.y - seg.x, BEYOND_STREET), Y_ROAD_EW, + M["road"], "concrete") + + # A building block in every cell of that grid. + for i in range(xs.size() - 1): + for j in range(zs.size() - 1): + var x0: float = xs[i] + BEYOND_STREET * 0.5 + var x1: float = xs[i + 1] - BEYOND_STREET * 0.5 + var z0: float = zs[j] + BEYOND_STREET * 0.5 + var z1: float = zs[j + 1] - BEYOND_STREET * 0.5 + _beyond_cell(x0, x1, z0, z1, path) + + +## One city block: a frontage of shophouses on the two long sides, houses in the +## middle. Skipped wherever it would sit on the town, the railway or a road. +func _beyond_cell(x0: float, x1: float, z0: float, z1: float, path: Array) -> void: + var cx := (x0 + x1) * 0.5 + var cz := (z0 + z1) * 0.5 + # Clear of the walled town — with a margin, so nothing crowds the wall. + if absf(cx) < HALF_X + 14.0 and absf(cz) < HALF_Z + 14.0: + return + # And inside the suburb ring. Past CITY_REACH the ground is hills, and a + # building out there would be standing in one. + if _dist_to_town(Vector2(cx, cz)) > CITY_REACH: + return + # Clear of the railway, wherever it happens to run out here. + var rad := maxf(x1 - x0, z1 - z0) * 0.5 + 14.0 + for e in path: + var p: Vector2 = e[0] + if absf(p.x - cx) < rad and absf(p.y - cz) < rad: + return + + # ── Shophouses along the two street frontages ──────────────────────────── + # + # Positioned by their FRONT, not their centre. Putting the centre on the + # cell edge left half of every building — four and a half metres of it — + # standing in the carriageway, which is what a road running into a house + # actually looked like. + var shop_d := 9.0 + for side in [-1.0, 1.0]: + var fz: float = cz + side * ((z1 - z0) * 0.5) + var x := x0 + 2.0 + var idx := int(absf(cx) + absf(cz)) % 8 + while x < x1 - 8.0: + var w := _rng.randf_range(7.0, 12.0) + _beyond_shop(Vector3(x + w * 0.5, 0, fz - side * shop_d * 0.5), + w - 0.4, shop_d, side, idx) + x += w + idx += 1 + + # One row of houses down the middle of the block, clear of both frontages. + var hx := x0 + 6.0 + while hx < x1 - 10.0: + var w := _rng.randf_range(8.0, 11.0) + _beyond_house(Vector3(hx + w * 0.5, 0, cz), w - 0.6, 8.0) + hx += w + 2.5 + + +## A shophouse, cut down for distance: body, parapet, fascia band, awning and a +## sign. The same silhouette as the ones inside the wall, at a tenth the cost. +func _beyond_shop(centre: Vector3, w: float, d: float, facing: float, + idx: int) -> void: + var storeys := 2 if (idx % 3) != 0 else 3 + var h := float(storeys) * FLOOR_H + var front := centre.z + facing * d * 0.5 + _solid(centre + Vector3(0, h * 0.5, 0), Vector3(w, h, d), _wall_mat(idx), + "concrete") + # Parapet — the flat-roofed shophouse's defining edge. + _deco(_box_mesh(Vector3(w + 0.4, 0.7, d + 0.4)), _roof_mat(idx), + centre + Vector3(0, h + 0.35, 0)) + # Ground-floor glazing under an awning, and a white fascia over it. + _deco(_box_mesh(Vector3(w - 1.0, 2.4, 0.1)), M["glass"], + Vector3(centre.x, 1.4, front + facing * 0.06)) + var awn: Color = SakuraPalette.AWNINGS[(idx * 3 + 1) % SakuraPalette.AWNINGS.size()] + _deco(_box_mesh(Vector3(w - 0.6, 0.36, 1.5)), + LevelMaterials.cel(awn, LevelMaterials.RAMP_2, SakuraPalette.TINT_WARM), + Vector3(centre.x, 3.0, front + facing * 0.8)) + _deco(_box_mesh(Vector3(w - 0.6, 0.6, 0.1)), M["white_board"], + Vector3(centre.x, 3.6, front + facing * 0.1)) + # A vertical sign on the corner, in one of the drink colours the shopfronts + # inside the wall use. + _deco(_box_mesh(Vector3(0.7, h - 4.6, 0.12)), + LevelMaterials.unlit(SakuraPalette.DRINKS[(idx * 5) % SakuraPalette.DRINKS.size()]), + Vector3(centre.x + w * 0.5 - 0.6, h * 0.5 + 1.7, front + facing * 0.18)) + # Upper window bands. + for s_i in range(1, storeys): + _deco(_box_mesh(Vector3(w - 1.4, 1.5, 0.08)), M["glass_dark"], + Vector3(centre.x, float(s_i) * FLOOR_H + 1.9, front + facing * 0.05)) + + +## A detached house, cut down for distance: body, pitched roof, eaves, ridge and +## a garden wall. Same form as the ones on the south side. +func _beyond_house(centre: Vector3, w: float, d: float) -> void: + var h := 6.2 + var idx := int(absf(centre.x) + absf(centre.z)) % 8 + _solid(centre + Vector3(0, h * 0.5, 0), Vector3(w, h, d), _wall_mat(idx + 3), + "concrete") + var roof := _roof_mat(idx + 1) + _deco(_prism_mesh(Vector3(w + 1.2, 2.0, d + 1.2)), roof, + centre + Vector3(0, h + 1.0, 0)) + _deco(_box_mesh(Vector3(w + 1.4, 0.18, d + 1.4)), roof, + centre + Vector3(0, h + 0.05, 0)) + _deco(_box_mesh(Vector3(0.34, 0.22, d + 1.5)), M["concrete_dark"], + centre + Vector3(0, h + 2.02, 0)) + _deco(_box_mesh(Vector3(w * 0.5, 1.6, 0.08)), M["glass_dark"], + centre + Vector3(0, 4.2, -d * 0.5 - 0.05)) + _deco(_box_mesh(Vector3(w * 0.45, 1.9, 0.08)), M["glass"], + centre + Vector3(0, 1.35, -d * 0.5 - 0.05)) + _deco(_box_mesh(Vector3(w + 1.0, 1.3, 0.24)), M["concrete"], + centre + Vector3(0, 0.65, -d * 0.5 - 2.6)) + + +## Is this point within `margin` of the running line, anywhere along it? +## +## Roads are describable as a handful of rectangles; the railway is not, because +## it turns. Scenery has to be tested against the actual path or it ends up +## between the rails — which is precisely where the boundary treeline planted +## itself once the line stopped being a straight run along z = 0. +func _near_railway(p: Vector2, margin: float = RAIL_HALF + 2.0) -> bool: + for e in _rail_path(): + var q: Vector2 = e[0] + # Cheap rejection first: the path has several hundred samples and this + # runs for every tree in the map. + if absf(q.x - p.x) > margin or absf(q.y - p.y) > margin: + continue + if q.distance_to(p) < margin: + return true + return false + + +## Is this point on any carriageway — inside the town, leaving it, or part of +## the grid beyond? Used to keep scenery out of the roads. +func _on_any_road(p: Vector2) -> bool: + if absf(p.x) < ROAD_HALF + PAVE_W + 2.5: + return true + if absf(p.y - SOUTH_ST_Z) < SOUTH_ST_HALF + PAVE_W + 2.5: + return true + if p.x < -HALF_X and absf(p.y - SHOP_ST_Z) < SHOP_ST_HALF + PAVE_W + 2.5: + return true + if _near_park_road(p, SHOP_ST_HALF + 2.5): + return true + # The beyond grid: streets every BEYOND_PITCH off the two real roads. + var gx: float = absf(p.x - roundf(p.x / BEYOND_PITCH) * BEYOND_PITCH) + var gz: float = absf((p.y - SOUTH_ST_Z) + - roundf((p.y - SOUTH_ST_Z) / BEYOND_PITCH) * BEYOND_PITCH) + return gx < BEYOND_STREET * 0.5 + 2.5 or gz < BEYOND_STREET * 0.5 + 2.5 + + +## Trees need more clearance than walls or props because their canopy is much +## wider than the point used to place them. This keeps a legal trunk position +## from putting five metres of cedar crown over a road beyond a boundary gate. +func _near_road_for_tree(p: Vector2) -> bool: + var crown_margin := 8.0 + # Keep the first view through each wall opening especially clean. A trunk + # can be legally outside the pavement and still put a large, nearby crown + # directly in front of the gate from a player's eye height. + if absf(p.y) > HALF_Z - 8.0: + if absf(p.x) < 24.0 or absf(p.x - PARK_RD_EXIT_X) < 24.0: + return true + if absf(p.x) > HALF_X - 2.0: + if absf(p.y - SOUTH_ST_Z) < 24.0: + return true + if p.x < 0.0 and absf(p.y - SHOP_ST_Z) < 24.0: + return true + if absf(p.x) < ROAD_HALF + PAVE_W + crown_margin: + return true + if absf(p.y - SOUTH_ST_Z) < SOUTH_ST_HALF + PAVE_W + crown_margin: + return true + if p.x < -HALF_X and absf(p.y - SHOP_ST_Z) \ + < SHOP_ST_HALF + PAVE_W + crown_margin: + return true + if _near_park_road(p, SHOP_ST_HALF + crown_margin): + return true + var gx: float = absf(p.x - roundf(p.x / BEYOND_PITCH) * BEYOND_PITCH) + var gz: float = absf((p.y - SOUTH_ST_Z) + - roundf((p.y - SOUTH_ST_Z) / BEYOND_PITCH) * BEYOND_PITCH) + return gx < BEYOND_STREET * 0.5 + crown_margin \ + or gz < BEYOND_STREET * 0.5 + crown_margin + + +## How far a point lies outside the walled town, in metres. Zero inside it. +func _dist_to_town(p: Vector2) -> float: + var q := Vector2(clampf(p.x, -HALF_X, HALF_X), clampf(p.y, -HALF_Z, HALF_Z)) + return p.distance_to(q) + + +## ── Cover ──────────────────────────────────────────────────────────────────── +## +## The map's three lanes are meant to be crossed under fire, and the long ones +## had almost nothing standing in them: a player entering the railway corridor +## or the south street could be seen from one end to the other. +## +## ── Hedges alone do not do this ────────────────────────────────────────────── +## +## The first pass at it was hedges and kei vans, and both are too SHORT. A +## 1.15 m hedge and a 1.7 m van are cover — you can get behind them — but a +## sightline is broken by something you cannot see OVER, and a standing player's +## eye is about 1.7 m. So the hedges went up to 2.1 m, and the real work is now +## done by things with mass: concrete block walls, containers, hoardings and box +## trucks. The soft stuff is still there, but it is the garnish rather than the +## meal. +## +## The rule for placing them is the same either way: break sight ALONG a lane +## without closing it. Staggered down alternate sides, never in a line across, +## and never over a doorway or a crossing approach. + +## ブロック塀 — the pierced concrete block wall that bounds half the plots in +## Japan. Two metres of hard, opaque, entirely characteristic sightline break, +## and the single most useful object in this list. +func _block_wall(a: Vector2, b: Vector2, height: float = 2.0) -> void: + # A plot boundary never crosses a carriageway. Checked here rather than at + # each call site: this is the third separate hand-placed wall to land in a + # road, so the invariant belongs with the thing that has to hold it. + if _on_any_road(a) or _on_any_road(b) or _on_any_road((a + b) * 0.5): + return + var mid := (a + b) * 0.5 + var d := b - a + var yaw := _yaw_of(d.normalized()) + var body := _solid(Vector3(mid.x, height * 0.5, mid.y), + Vector3(d.length(), height, 0.3), M["concrete"], "concrete", + Vector3(0, yaw, 0)) + body.name = "BlockWall" + # Capping, and a pier every few metres: without them it is a blank slab. + _deco(_box_mesh(Vector3(d.length() + 0.1, 0.14, 0.44)), M["concrete_dark"], + Vector3(mid.x, height + 0.05, mid.y), Vector3(0, yaw, 0)) + var n := maxi(1, int(d.length() / 3.4)) + for i in range(n + 1): + var p: Vector2 = a.lerp(b, float(i) / float(n)) + _deco(_box_mesh(Vector3(0.42, height, 0.44)), M["concrete_mid"], + Vector3(p.x, height * 0.5, p.y), Vector3(0, yaw, 0)) + + +## A lineside hut — the little rendered-block permanent-way store that stands +## beside every railway in Japan. Replaces the shipping containers this used to +## have: those were the one prop in the map borrowed from a different kind of +## place altogether, and they read as an industrial estate rather than a +## suburb. Same job — 2.6 m of opaque mass against the fence — in a form that +## belongs. +func _lineside_hut(pos: Vector3, yaw: float, idx: int) -> void: + var yr := Vector3(0, yaw, 0) + var body := _solid(pos + Vector3(0, 1.3, 0), Vector3(4.4, 2.6, 2.0), + _wall_mat(idx + 2), "concrete", yr) + body.name = "LinesideHut" + # A shallow pitched roof with eaves, the same form the houses use. + _deco(_prism_mesh(Vector3(4.9, 0.9, 2.4)), M["roof_slate"], + pos + Vector3(0, 3.05, 0), yr) + _deco(_box_mesh(Vector3(5.0, 0.14, 2.5)), M["roof_slate"], + pos + Vector3(0, 2.64, 0), yr) + var fwd := Vector3(sin(deg_to_rad(yaw)), 0, cos(deg_to_rad(yaw))) + _deco(_box_mesh(Vector3(1.0, 1.9, 0.1)), M["wood_dark"], + pos + Vector3(0, 0.95, 0) + fwd * 1.02, yr) + _deco(_box_mesh(Vector3(1.3, 0.9, 0.08)), M["glass_dark"], + pos + Vector3(1.4, 1.6, 0) + fwd * 1.02, yr) + _deco(_cyl_mesh(0.05, 0.05, 2.4, 6), M["metal"], + pos + Vector3(-2.05, 1.2, 0) + fwd * 1.0, yr) + + +## A hoarding — an advertising board on legs over a vacant lot. Tall, thin, and +## the cheapest possible way to cut a long view without adding a building. +func _billboard(pos: Vector3, yaw: float, idx: int) -> void: + var yr := Vector3(0, yaw, 0) + for s in [-1.0, 1.0]: + _post(pos + Vector3(cos(deg_to_rad(yaw)), 0, -sin(deg_to_rad(yaw))) + * (s * 2.6) + Vector3(0, 1.6, 0), 0.14, 3.2, M["metal_dark"], "metal", 6) + var body := _solid(pos + Vector3(0, 4.3, 0), Vector3(6.6, 3.0, 0.24), + M["white_board"], "wood", yr) + body.name = "Hoarding" + var col: Color = SakuraPalette.DRINKS[(idx * 3) % SakuraPalette.DRINKS.size()] + _deco(_box_mesh(Vector3(6.0, 2.4, 0.1)), LevelMaterials.unlit(col), + pos + Vector3(0, 4.3, 0) + + Vector3(sin(deg_to_rad(yaw)), 0, cos(deg_to_rad(yaw))) * 0.18, yr) + SakuraSignage.label(_decor, + SakuraSignage.WALL_ADS[idx % SakuraSignage.WALL_ADS.size()], 0.8, + SakuraPalette.WALL_WHITE, + pos + Vector3(0, 4.3, 0) + + Vector3(sin(deg_to_rad(yaw)), 0, cos(deg_to_rad(yaw))) * 0.26, + Vector3(0, yaw, 0)) + + +## ── Vehicles ───────────────────────────────────────────────────────────────── +## +## A vehicle's LENGTH runs along its local +X, and `yaw` turns it. So a car +## parked on an east-west street takes yaw 0 or 180, and one on a north-south +## street takes 90 or 270. Getting that wrong is what made these look "squished +## lengthwise": every one of them was placed at 90 on an east-west street, so +## its three-and-a-half metre length ran ACROSS the carriageway and its 1.5 m +## width ran along it. They were the right size and pointing the wrong way. + + +## 軽バン — the kei van. The dimensions are the real regulatory ones, because +## the whole reason this shape is recognisable is that it is a box built to the +## legal maximum: 3.40 long, 1.48 wide, and tall for its footprint. Cab-over, +## so the windscreen is at the very front and there is no bonnet at all, and +## the wheels are pushed right into the corners. +func _kei_van(pos: Vector3, yaw: float, colour: Color) -> void: + var mat := LevelMaterials.cel(colour, LevelMaterials.RAMP_2, SakuraPalette.TINT_COOL) + var yr := Vector3(0, yaw, 0) + var fwd := Vector3(cos(deg_to_rad(yaw)), 0, -sin(deg_to_rad(yaw))) + var right := Vector3(sin(deg_to_rad(yaw)), 0, cos(deg_to_rad(yaw))) + + # Body: a low sill, a distinct shoulder and a narrower greenhouse. Keeping + # all three the same box was accurate to the legal envelope but read as a + # delivery crate with wheels rather than a Japanese kei van. + var body := _solid(pos + Vector3(0, 0.66, 0), Vector3(3.40, 0.88, 1.48), + mat, "metal", yr) + body.name = "KeiVan" + _solid(pos + Vector3(-0.08, 1.36, 0), Vector3(2.92, 0.58, 1.38), mat, "metal", yr) + _deco(_box_mesh(Vector3(2.98, 0.10, 1.42)), M["metal"], + pos + Vector3(-0.08, 1.70, 0), yr) + _deco(_box_mesh(Vector3(3.34, 0.12, 1.50)), mat, + pos + Vector3(0, 1.02, 0), yr) + + # Glass. The windscreen is nearly upright and sits over the front axle — + # that steep front face is most of the silhouette. + _deco(_box_mesh(Vector3(0.12, 0.58, 1.30)), M["glass_dark"], + pos + Vector3(0, 1.38, 0) + fwd * 1.48, Vector3(0, yaw, -6.0)) + for s in [-1.0, 1.0]: + for xoff in [-0.74, 0.55]: + _deco(_box_mesh(Vector3(1.02, 0.44, 0.07)), M["glass_dark"], + pos + Vector3(0, 1.39, 0) + fwd * xoff + right * (s * 0.705), yr) + # Sliding-door seam and a small horizontal handle. + _deco(_box_mesh(Vector3(0.035, 0.80, 0.035)), M["metal_dark"], + pos - fwd * 0.15 + right * (s * 0.742) + Vector3(0, 0.92, 0), yr) + _deco(_box_mesh(Vector3(0.32, 0.06, 0.04)), M["metal_dark"], + pos - fwd * 0.55 + right * (s * 0.75) + Vector3(0, 1.03, 0), yr) + _deco(_box_mesh(Vector3(0.08, 0.44, 1.28)), M["glass_dark"], + pos + Vector3(0, 1.39, 0) - fwd * 1.52, yr) + + # Wheels in the corners, with arches over them. + for s in [-1.0, 1.0]: + for f in [-1.0, 1.0]: + # ── Wheel orientation ──────────────────────────────────────── + # A CylinderMesh's axis is local +Y, and a wheel's axle has to be + # the vehicle's LATERAL direction. rotation_degrees is YXZ, so the + # vector goes through Rz, then Rx, then Ry: (0, yaw, 90) sends + # +Y to -X and then swings it round to the vehicle's LENGTH, which + # is why every wheel on the map was turned side-on. (90, yaw, 0) + # sends +Y to +Z first, and Ry lands it on the lateral axis. + _deco(_cyl_mesh(0.28, 0.28, 0.18, 8), M["ink"], + pos + fwd * (f * 1.18) + right * (s * 0.70) + Vector3(0, 0.28, 0), + Vector3(90, yaw, 0)) + _deco(_box_mesh(Vector3(0.78, 0.30, 0.1)), mat, + pos + fwd * (f * 1.18) + right * (s * 0.745) + Vector3(0, 0.46, 0), yr) + + # Bumpers, lamps, plate, mirrors. + for f in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(0.14, 0.22, 1.44)), M["metal_dark"], + pos + Vector3(0, 0.38, 0) + fwd * (f * 1.70), yr) + for s in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(0.08, 0.16, 0.3)), M["lantern"], + pos + Vector3(0, 0.86, 0) + fwd * 1.70 + right * (s * 0.5), yr) + _deco(_box_mesh(Vector3(0.08, 0.14, 0.26)), M["red"], + pos + Vector3(0, 0.95, 0) - fwd * 1.70 + right * (s * 0.5), yr) + _deco(_box_mesh(Vector3(0.16, 0.16, 0.1)), M["metal_dark"], + pos + Vector3(0, 1.52, 0) + fwd * 1.30 + right * (s * 0.86), yr) + _deco(_box_mesh(Vector3(0.02, 0.14, 0.3)), M["white_board"], + pos + Vector3(0, 0.52, 0) - fwd * 1.78, yr) + # Narrow grille, centre badge and the tall yellow Japanese plate at the front. + _deco(_box_mesh(Vector3(0.04, 0.20, 0.70)), M["black"], + pos + Vector3(0, 0.62, 0) + fwd * 1.78, yr) + _deco(_box_mesh(Vector3(0.045, 0.16, 0.30)), M["yellow"], + pos + Vector3(0, 0.43, 0) + fwd * 1.80, yr) + + +## 軽自動車 — the five-door kei hatch. The van is common, but a street made +## only of vans and box trucks still reads like a service yard. This lower, +## short-bonnet silhouette supplies the ordinary private car Japan actually has +## in abundance while keeping the same tiny 3.4 by 1.48 metre footprint. +func _kei_hatch(pos: Vector3, yaw: float, colour: Color) -> void: + var pivot := Node3D.new() + pivot.name = "KeiCar" + pivot.position = pos + pivot.rotation_degrees = Vector3(0, yaw, 0) + add_child(pivot) + var prev := _group + _group = pivot + var mat := LevelMaterials.cel(colour, LevelMaterials.RAMP_2, SakuraPalette.TINT_COOL) + + _solid(Vector3(0, 0.58, 0), Vector3(3.35, 0.74, 1.48), mat, "metal") + _solid(Vector3(-0.20, 1.18, 0), Vector3(2.35, 0.56, 1.36), mat, "metal") + _deco(_box_mesh(Vector3(2.40, 0.10, 1.40)), M["metal"], Vector3(-0.20, 1.51, 0)) + # The tiny bonnet and sharply raked screens are the silhouette difference + # between this and the cab-over van. + _deco(_box_mesh(Vector3(0.72, 0.18, 1.43)), mat, Vector3(1.30, 0.91, 0), + Vector3(0, 0, -5.0)) + _deco(_box_mesh(Vector3(0.08, 0.55, 1.29)), M["glass_dark"], + Vector3(1.00, 1.23, 0), Vector3(0, 0, -15.0)) + _deco(_box_mesh(Vector3(0.08, 0.48, 1.27)), M["glass_dark"], + Vector3(-1.39, 1.20, 0), Vector3(0, 0, 12.0)) + for side in [-1.0, 1.0]: + for xoff in [-0.72, 0.32]: + _deco(_box_mesh(Vector3(0.78, 0.40, 0.07)), M["glass_dark"], + Vector3(xoff, 1.22, side * 0.70)) + _deco(_box_mesh(Vector3(0.30, 0.06, 0.04)), M["metal_dark"], + Vector3(-0.32, 0.92, side * 0.75)) + _deco(_box_mesh(Vector3(0.18, 0.13, 0.12)), M["metal_dark"], + Vector3(0.86, 1.23, side * 0.84)) + for axle_x in [-1.08, 1.08]: + _deco(_cyl_mesh(0.29, 0.29, 0.18, 10), M["ink"], + Vector3(axle_x, 0.29, side * 0.70), Vector3(90, 0, 0)) + _deco(_cyl_mesh(0.13, 0.13, 0.185, 8), M["metal"], + Vector3(axle_x, 0.29, side * 0.705), Vector3(90, 0, 0)) + for side in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(0.08, 0.16, 0.28)), M["lantern"], + Vector3(1.69, 0.76, side * 0.50)) + _deco(_box_mesh(Vector3(0.08, 0.20, 0.24)), M["red"], + Vector3(-1.69, 0.82, side * 0.50)) + _deco(_box_mesh(Vector3(0.08, 0.18, 0.62)), M["black"], Vector3(1.71, 0.48, 0)) + _deco(_box_mesh(Vector3(0.04, 0.15, 0.30)), M["yellow"], Vector3(1.76, 0.40, 0)) + _deco(_box_mesh(Vector3(0.04, 0.14, 0.30)), M["white_board"], Vector3(-1.76, 0.45, 0)) + _group = prev + + +## 小型トラック — the light cab-over truck that does every delivery in Japan. +## +## The proportions ARE the identity: a flat-faced cab sitting directly over the +## front axle with no bonnet, and a box body behind it that is both taller and +## slightly wider than the cab, so the profile steps UP from front to back. A +## generic "truck" with a nose and a body the same height as its cab reads as +## American and looks wrong parked on this street. +func _box_truck(pos: Vector3, yaw: float) -> void: + var yr := Vector3(0, yaw, 0) + var fwd := Vector3(cos(deg_to_rad(yaw)), 0, -sin(deg_to_rad(yaw))) + var right := Vector3(sin(deg_to_rad(yaw)), 0, cos(deg_to_rad(yaw))) + var cab: Material = M["vend_white"] + + # Chassis, then the cab over the front axle, then the taller box behind. + _deco(_box_mesh(Vector3(5.5, 0.22, 1.5)), M["metal_dark"], + pos + Vector3(0, 0.70, 0), yr) + var cab_body := _solid(pos + Vector3(0, 1.66, 0) + fwd * 1.85, + Vector3(1.90, 1.72, 1.90), cab, "metal", yr) + cab_body.name = "Truck" + _solid(pos + Vector3(0, 2.02, 0) - fwd * 0.95, + Vector3(3.70, 2.14, 1.98), cab, "metal", yr) + + # The box's frame lines and a coloured lower band — every one of these + # carries a livery stripe along the bottom of the body. + for f in range(5): + _deco(_box_mesh(Vector3(0.1, 2.1, 2.02)), M["metal"], + pos + Vector3(0, 2.02, 0) - fwd * (0.95 + (float(f) - 2.0) * 0.85), yr) + _deco(_box_mesh(Vector3(3.74, 0.34, 2.02)), LevelMaterials.cel( + SakuraPalette.TRAIN_STRIPE, LevelMaterials.RAMP_2, SakuraPalette.TINT_COOL), + pos + Vector3(0, 1.12, 0) - fwd * 0.95, yr) + # Roof cap, and the deflector every one of them has over the cab. + _deco(_box_mesh(Vector3(3.8, 0.12, 2.06)), M["metal"], + pos + Vector3(0, 3.12, 0) - fwd * 0.95, yr) + _deco(_box_mesh(Vector3(0.7, 0.5, 1.8)), cab, + pos + Vector3(0, 2.70, 0) + fwd * 1.7, Vector3(0, yaw, -18)) + + # The flat face: a big upright windscreen low down, and the grille below it. + _deco(_box_mesh(Vector3(0.12, 0.78, 1.76)), M["glass_dark"], + pos + Vector3(0, 2.08, 0) + fwd * 2.76, yr) + for s in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(1.7, 0.6, 0.08)), M["glass_dark"], + pos + Vector3(0, 1.94, 0) + fwd * 1.85 + right * (s * 0.96), yr) + _deco(_box_mesh(Vector3(0.1, 0.3, 0.42)), M["lantern"], + pos + Vector3(0, 1.06, 0) + fwd * 2.76 + right * (s * 0.66), yr) + # Mirrors on stalks, which stand well proud of a cab-over face. + _deco(_box_mesh(Vector3(0.1, 0.42, 0.1)), M["metal_dark"], + pos + Vector3(0, 2.34, 0) + fwd * 2.7 + right * (s * 1.14), yr) + _deco(_box_mesh(Vector3(0.1, 0.44, 1.7)), M["metal_dark"], + pos + Vector3(0, 1.30, 0) + fwd * 2.78, yr) + _deco(_box_mesh(Vector3(0.14, 0.34, 1.9)), M["metal_dark"], + pos + Vector3(0, 0.80, 0) + fwd * 2.80, yr) + + # Rear roller doors and road hardware. The old blank white end was the + # largest surface on the vehicle whenever it faced away from the camera. + var rear := pos - fwd * 2.83 + _deco(_box_mesh(Vector3(0.10, 1.92, 1.82)), M["vend_white"], + rear + Vector3(0, 2.02, 0), yr) + for side in [-1.0, 0.0, 1.0]: + _deco(_box_mesh(Vector3(0.13, 1.94, 0.07)), M["metal"], + rear + Vector3(0, 2.02, 0) + right * (float(side) * 0.86), yr) + for y in [1.25, 1.75, 2.25, 2.75]: + _deco(_box_mesh(Vector3(0.12, 0.055, 1.76)), M["metal"], + rear + Vector3(0, y, 0), yr) + for side in [-1.0, 1.0]: + _deco(_box_mesh(Vector3(0.13, 0.24, 0.28)), M["red"], + rear + Vector3(0, 0.84, 0) + right * (float(side) * 0.62), yr) + _deco(_box_mesh(Vector3(0.13, 0.18, 0.38)), M["white_board"], + rear + Vector3(0, 0.72, 0), yr) + _deco(_box_mesh(Vector3(0.18, 0.16, 1.92)), M["metal_dark"], + rear + Vector3(0, 0.47, 0), yr) + + # Wheels: one pair under the cab, one under the back of the box. + for s in [-1.0, 1.0]: + for f in [1.85, -1.85]: + _deco(_cyl_mesh(0.44, 0.44, 0.26, 10), M["ink"], + pos + fwd * f + right * (s * 0.88) + Vector3(0, 0.44, 0), + Vector3(90, yaw, 0)) + + +## A clipped hedge. Now above standing eye height, so it actually breaks a view +## rather than merely giving somebody something to crouch behind. +func _hedge(a: Vector2, b: Vector2, height: float = 2.1) -> void: + var mid := (a + b) * 0.5 + var d := b - a + var body := _solid(Vector3(mid.x, height * 0.5, mid.y), + Vector3(d.length(), height, 1.0), M["leaf_deep"], "wood", + Vector3(0, _yaw_of(d.normalized()), 0)) + body.name = "Hedge" + _deco(_box_mesh(Vector3(d.length() - 0.2, 0.26, 1.12)), M["leaf"], + Vector3(mid.x, height + 0.03, mid.y), + Vector3(0, _yaw_of(d.normalized()), 0)) + + +## A lineside relay cabinet or a roadside transformer box. +func _cabinet(pos: Vector3, yaw: float) -> void: + _solid(pos + Vector3(0, 0.7, 0), Vector3(1.15, 1.4, 0.7), M["cabinet"], + "metal", Vector3(0, yaw, 0)) + _deco(_box_mesh(Vector3(1.26, 0.1, 0.82)), M["cabinet_top"], + pos + Vector3(0, 1.45, 0), Vector3(0, yaw, 0)) + _deco(_box_mesh(Vector3(0.5, 0.28, 0.04)), M["yellow"], + pos + Vector3(0, 1.05, 0) + Vector3(sin(deg_to_rad(yaw)), 0, + cos(deg_to_rad(yaw))) * 0.37, Vector3(0, yaw, 0)) + + +## A stack of delivery crates against a wall. Vaultable, and the only cover in +## the map a player can reliably get on top of. +func _crates(pos: Vector3, yaw: float) -> void: + var cols := [SakuraPalette.CRATE, SakuraPalette.CRATE_ALT, SakuraPalette.BASKET] + for i in range(3): + var c: Color = cols[(i + int(pos.x)) % cols.size()] + var s := 0.62 - float(i) * 0.05 + _solid(pos + Vector3(0, 0.31 + float(i) * 0.62, 0), + Vector3(s * 1.5, 0.62, s * 1.2), + LevelMaterials.cel(c, LevelMaterials.RAMP_2, SakuraPalette.TINT_COOL), + "wood", Vector3(0, yaw + float(i) * 6.0, 0)) + + +## A bus shelter: a roof on posts with one glazed side. +func _bus_shelter(pos: Vector3, yaw: float) -> void: + var yr := Vector3(0, yaw, 0) + var right := Vector3(cos(deg_to_rad(yaw)), 0, -sin(deg_to_rad(yaw))) + var back := Vector3(sin(deg_to_rad(yaw)), 0, cos(deg_to_rad(yaw))) + _solid(pos + Vector3(0, 1.2, 0) - back * 0.7, Vector3(4.2, 2.4, 0.12), + M["glass"], "glass", yr) + for s in [-1.0, 1.0]: + _post(pos + right * (s * 2.0) + Vector3(0, 1.3, 0), 0.07, 2.6, + M["metal_dark"], "metal", 6) + _solid(pos + Vector3(0, 2.68, 0), Vector3(4.6, 0.16, 1.8), M["roof_slate"], + "metal", yr) + _solid(pos + Vector3(0, 0.5, 0) - back * 0.45, Vector3(3.2, 0.12, 0.45), + M["wood"], "wood", yr) + SakuraSignage.label(_decor, "バス", 0.22, SakuraPalette.INK, + pos + Vector3(0, 2.35, 0) + back * 0.9, Vector3(0, yaw + 180.0, 0)) + + +## Everything above, placed where the lanes are too open. +func _build_cover() -> void: + var van_cols := [SakuraPalette.WALL_WHITE, SakuraPalette.TRAIN_BODY, + SakuraPalette.WALL_BLUE, SakuraPalette.WALL_SAGE, SakuraPalette.RED_SOFT] + + # ── The railway corridor ───────────────────────────────────────────────── + # A permanent-way store: containers and a hoarding, hard against the fence + # where they interrupt the two-hundred-metre view down the line without + # fouling the running lines. + var rx := -HALF_X + 26.0 + var ci := 0 + while rx < CURVE_START_X - 10.0: + if absf(rx) > ROAD_HALF + 12.0 and absf(rx - BRIDGE_X) > 9.0 \ + and absf(rx - STATION_X) > 20.0: + var s: float = 1.0 if ci % 2 == 0 else -1.0 + # OUTSIDE the fence (6.6), not merely clear of the sleepers. At + # z = 5.25 the hut still sat on the ballast, which reads as a shed + # on the tracks however the clearance maths works out. + _lineside_hut(Vector3(rx, 0.0, s * 8.9), + 0.0 if s > 0.0 else 180.0, ci) + ci += 1 + rx += _rng.randf_range(26.0, 38.0) + + # ── The south street ───────────────────────────────────────────────────── + # Block walls between the plots — which is what is actually there — plus a + # truck or a van on alternate sides. + var i := 0 + var x := -HALF_X + 20.0 + while x < HALF_X - 20.0: + if absf(x) > ROAD_HALF + 9.0 and absf(x - EAST_ST_X) > 10.0 \ + and absf(x - STATION_X) > 14.0: + var side: float = 1.0 if i % 2 == 0 else -1.0 + if i % 4 == 2: + # East-west street: length along X. Facing with the traffic on + # its own side, so the two kerbs point opposite ways. + _box_truck(Vector3(x, Y_ROAD_EW, + SOUTH_ST_Z + side * (SOUTH_ST_HALF - 1.6)), + 0.0 if side > 0.0 else 180.0) + elif i % 2 == 0: + _kei_hatch(Vector3(x, Y_ROAD_EW, + SOUTH_ST_Z + side * (SOUTH_ST_HALF - 1.2)), + 0.0 if side > 0.0 else 180.0, van_cols[i % van_cols.size()]) + else: + _kei_van(Vector3(x, Y_ROAD_EW, + SOUTH_ST_Z + side * (SOUTH_ST_HALF - 1.2)), + 0.0 if side > 0.0 else 180.0, van_cols[i % van_cols.size()]) + # A block wall running back off the pavement, so the lane is broken + # across as well as along. + _block_wall(Vector2(x + 6.0, SOUTH_ST_Z + side * (SOUTH_ST_HALF + PAVE_W + 0.6)), + Vector2(x + 6.0, SOUTH_ST_Z + side * (SOUTH_ST_HALF + PAVE_W + 9.0)), 2.0) + i += 1 + x += _rng.randf_range(16.0, 24.0) + + # ── The verge, between the railway fence and the south street ──────────── + var hx := -HALF_X + 16.0 + while hx < CURVE_START_X - 6.0: + var run := _rng.randf_range(8.0, 15.0) + if absf(hx) > ROAD_HALF + 14.0 and absf(hx + run) > ROAD_HALF + 14.0 \ + and absf(hx - BRIDGE_X) > 9.0 and absf(hx - STATION_X) > 20.0: + if int(hx) % 2 == 0: + _hedge(Vector2(hx, 11.4), Vector2(hx + run, 11.4)) + else: + _block_wall(Vector2(hx, 11.4), Vector2(hx + run, 11.4), 2.1) + hx += run + _rng.randf_range(6.0, 12.0) + + # ── The shopping street ────────────────────────────────────────────────── + var sx := -HALF_X + 22.0 + while sx < SHOP_ST_X1 - 14.0: + if absf(sx) > ROAD_HALF + 9.0 and absf(sx - BRIDGE_X) > 10.0: + if i % 5 == 0: + _box_truck(Vector3(sx, Y_ROAD_EW, SHOP_ST_Z + SHOP_ST_HALF - 1.6), 0.0) + elif i % 2 == 0: + _kei_hatch(Vector3(sx, Y_ROAD_EW, SHOP_ST_Z + SHOP_ST_HALF - 1.2), + 0.0, van_cols[(i + 2) % van_cols.size()]) + else: + _kei_van(Vector3(sx, Y_ROAD_EW, SHOP_ST_Z + SHOP_ST_HALF - 1.2), + 0.0, van_cols[(i + 2) % van_cols.size()]) + i += 1 + sx += _rng.randf_range(19.0, 27.0) + + # ── The service alley ──────────────────────────────────────────────────── + var ax := -HALF_X + 16.0 + while ax < SHOP_ST_X1 - 10.0: + if absf(ax) > ROAD_HALF + 5.0 and absf(ax - BRIDGE_X) > 9.0: + _crates(Vector3(ax, Y_GRASS, ALLEY_Z - 1.0), _rng.randf_range(0, 40)) + if int(ax) % 3 == 0: + _crates(Vector3(ax + 5.0, Y_GRASS, ALLEY_Z - 0.6), + _rng.randf_range(0.0, 40.0)) + ci += 1 + ax += _rng.randf_range(15.0, 24.0) + + # ── The crossing, the station and the east district ────────────────────── + # On the PAVEMENT, both of them. The first used to sit at z = 19, which is + # inside the south street's carriageway (15.5 to 24.5) — a bus shelter in + # the middle of the road. + _bus_shelter(Vector3(ROAD_HALF + 4.4, Y_PAVE, + SOUTH_ST_Z - SOUTH_ST_HALF - PAVE_W * 0.5 - 0.2), 0.0) + _bus_shelter(Vector3(STATION_X - 6.0, Y_PAVE, + SOUTH_ST_Z - SOUTH_ST_HALF - PAVE_W * 0.5 - 0.2), 0.0) + for s in [-1.0, 1.0]: + _cabinet(Vector3(s * (ROAD_HALF + 2.2), Y_PAVE, -RAIL_HALF - 9.0), 90.0) + # Hoardings on the vacant ground either side of the main street, which is + # the longest uninterrupted view in the map. + # Clear of every carriageway — the first pair used to stand at z = -29, + # which is inside the shopping street. Guarded rather than trusted, because + # a hand-written coordinate is exactly what put them there. + for s in [-1.0, 1.0]: + for spot in [Vector2(s * (ROAD_HALF + 10.0), -RAIL_HALF - 15.0)]: + if _on_any_road(spot): + continue + _billboard(Vector3(spot.x, 0.0, spot.y), 90.0 - 90.0 * s, int(spot.y)) + + var ex := EAST_DIST_X0 + 14.0 + while ex < EAST_DIST_X1 - 14.0: + if absf(ex - EAST_ST_X) > 10.0: + if i % 2 == 0: + _kei_hatch(Vector3(ex, Y_ROAD_EW, EAST_ST_Z + EAST_ST_HALF - 1.2), + 0.0, van_cols[(i + 1) % van_cols.size()]) + else: + _kei_van(Vector3(ex, Y_ROAD_EW, EAST_ST_Z + EAST_ST_HALF - 1.2), + 0.0, van_cols[(i + 1) % van_cols.size()]) + i += 1 + # Plot boundary walls, which is what separates these houses in life. + var north_wall_x := ex - 6.0 + if absf(north_wall_x - EAST_ST_X) > EAST_ST_HALF + 0.5: + _block_wall(Vector2(north_wall_x, + EAST_ST_Z + EAST_ST_HALF + PAVE_W + 0.8), + Vector2(north_wall_x, + EAST_ST_Z + EAST_ST_HALF + PAVE_W + 9.0), 2.0) + var south_wall_x := ex - 14.0 + if absf(south_wall_x - EAST_ST_X) > EAST_ST_HALF + 0.5: + _block_wall(Vector2(south_wall_x, + EAST_ST_Z - EAST_ST_HALF - PAVE_W - 0.8), + Vector2(south_wall_x, + EAST_ST_Z - EAST_ST_HALF - PAVE_W - 9.0), 2.0) + ex += _rng.randf_range(17.0, 24.0) + + # ── The park ───────────────────────────────────────────────────────────── + for s in [-1.0, 1.0]: + _hedge(Vector2(SHRINE_X - 24.0, PARK_Z + s * 8.0), + Vector2(SHRINE_X - 8.0, PARK_Z + s * 8.0)) + _hedge(Vector2(-6.0, PARK_Z - 8.0), Vector2(-6.0, PARK_Z + 8.0)) + _block_wall(Vector2(SHRINE_X - 30.0, PARK_Z - 9.0), + Vector2(SHRINE_X - 30.0, PARK_Z + 9.0), 2.0) + + # ── The north park ─────────────────────────────────────────────────────── + # Its own hedges are laid in _build_north_park; this is the run of walls + # along its southern edge, facing the shopping street. + var fx := -HALF_X + 20.0 + while fx < HALF_X - 20.0: + if absf(fx) > ROAD_HALF + 9.0 and not _near_park_road(Vector2(fx, -42.0)): + _block_wall(Vector2(fx, -42.0), Vector2(fx + 12.0, -42.0), 2.0) + fx += _rng.randf_range(22.0, 32.0) + + +## A hill, as three concentric terraces. +## +## Not a scaled cylinder, which is what this was first: `_post` at a negative Y +## with a squashed scale put every crown BELOW ground level — the arithmetic +## came out as `-0.30r + r·s/2`, which is negative for every scale in the range +## it was given, so seventy-two hills were buried and the margin rendered flat. +## +## Terraces rather than a dome because the collision has to be honest. A cone +## would need a cylinder collider the size of its base, so a player would stand +## on an invisible flat top; a squashed sphere needs non-uniform scale on a +## SphereShape3D, which Godot warns about. Three stacked cylinders collide +## exactly as they look — and a terraced hillside is hardly out of place here. +func _mound(centre: Vector2, radius: float, height: float) -> void: + var tiers := 3 + for i in range(tiers): + var f := float(i) / float(tiers) + var r: float = radius * (1.0 - f * 0.62) + var top: float = height * (float(i + 1) / float(tiers)) + # Each tier runs from below ground to its own top, so they nest and + # there is never a gap to fall into between two of them. + var h: float = top + 6.0 + var body := _post(Vector3(centre.x, top - h * 0.5, centre.y), r, h, + M["grass"], "concrete", 7) + body.name = "Mound" + + +func _cedar(base: Vector3, scale: float) -> void: + var h := 9.0 * scale + var trunk := _deco(_cyl_mesh(0.28 * scale, 0.16 * scale, h * 0.4, 5), + M["trunk_dark"], base + Vector3(0, h * 0.2, 0)) + trunk.name = "Cedar" + # Three tiers, narrowing. A conifer in this style is a stack of cones, and + # the ink pass draws the boundary between each pair of tiers — which is what + # gives it its layered look for three meshes. + for i in range(3): + var r := (2.3 - float(i) * 0.6) * scale + var th := (4.2 - float(i) * 0.7) * scale + _deco(_cyl_mesh(r, r * 0.16, th, 6), M["cedar"] if i < 2 else M["leaf_deep"], + base + Vector3(0, h * 0.32 + float(i) * 2.3 * scale, 0)) + + +# ── Petals ─────────────────────────────────────────────────────────────────── + +func _build_petals() -> void: + # ── Where the petals were ──────────────────────────────────────────────── + # + # They were there, and they were invisible, and the reason is worth writing + # down because it is not obvious: the emitter was a THIN box at 24 m, so + # every petal in the map spent its life falling from a ceiling. At eye level + # — the only height a first-person player ever looks at — the air was empty + # almost all of the time, and the handful of petals that had fallen far + # enough were three hundred metres of perspective away and a pixel wide. + # + # The fix is to emit through the whole VOLUME instead. A petal is spawned at + # any height from the pavement to above the rooflines, so the air is full at + # every height from the first frame, and the count went up with it. + _petal_field("PetalsAir", Vector3(0, 13.0, 0), + Vector3(HALF_X, 12.0, HALF_Z), 2400, Vector2(0.115, 0.075), 1.0) + + # Two dense local fields over the groves, because blossom does not fall + # evenly over a town — it falls hardest under the trees dropping it, and + # that difference is most of what sells the effect as blossom rather than + # as weather. + _petal_field("PetalsPark", Vector3(0, 7.0, PARK_Z), + Vector3(HALF_X * 0.55, 6.5, 11.0), 1100, Vector2(0.13, 0.085), 0.85) + _petal_field("PetalsVerge", Vector3(0, 6.0, 11.4), + Vector3(HALF_X * 0.9, 5.5, 5.0), 900, Vector2(0.13, 0.085), 0.85) + + +## One field of falling blossom. +func _petal_field(node_name: String, centre: Vector3, extents: Vector3, + amount: int, size: Vector2, speed: float) -> void: + var particles := GPUParticles3D.new() + particles.name = node_name + particles.amount = amount + particles.lifetime = 16.0 + # Preprocess a full lifetime so the field is already settled on frame one — + # without it a match opens on an empty sky that slowly fills. + particles.preprocess = 16.0 + particles.visibility_aabb = AABB( + centre - extents - Vector3(20, 24, 20), + (extents + Vector3(20, 24, 20)) * 2.0) + + var pm := ParticleProcessMaterial.new() + pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX + pm.emission_box_extents = extents + pm.direction = Vector3(0.35, -1, 0.1) + pm.spread = 26.0 + pm.initial_velocity_min = 0.6 * speed + pm.initial_velocity_max = 1.5 * speed + pm.gravity = Vector3(0.45, -0.8, 0.15) * speed + # A petal does not fall, it tumbles. The angular velocity and the turbulence + # are what make it read as blossom rather than as snow. + pm.angular_velocity_min = -200.0 + pm.angular_velocity_max = 200.0 + pm.turbulence_enabled = true + pm.turbulence_noise_strength = 0.38 + pm.turbulence_noise_scale = 1.5 + pm.scale_min = 0.75 + pm.scale_max = 1.15 + particles.process_material = pm + + var quad := QuadMesh.new() + quad.size = size + particles.draw_pass_1 = quad + + # ── Why the petals are TRANSPARENT ─────────────────────────────────────── + # + # The first version made them opaque, and every petal in the frame came out + # as a black speck: a 15 cm quad at any distance is one or two pixels, and + # an isolated one- or two-pixel depth spike is the largest second difference + # in the picture. The ink pass did exactly what it was asked to and inked + # each petal solid — the sky filled with what looked like flies. + # + # Making the material transparent puts it in the transparent pass, which + # runs after the ink quad and does not write depth. So the ink pass never + # sees a petal at all, and they composite over the finished frame as flat + # pink marks. Which is also how they are actually done: blossom is a + # separate cel laid over the background, not something in the scene. + var pmat := StandardMaterial3D.new() + pmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + pmat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + pmat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED + # PETAL_DEEP rather than PETAL. The pale tone is right for blossom seen as a + # MASS on a branch, where it sits against the sky; a single petal in the air + # is read against the road, the buildings and the shadow side of everything, + # and at 0.98 luminance it came out as white confetti. The deep tone is the + # only one that still says "pink" at eleven centimetres across. + pmat.albedo_color = Color(SakuraPalette.PETAL_DEEP, 0.95) + pmat.cull_mode = BaseMaterial3D.CULL_DISABLED + pmat.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES + particles.material_override = pmat + particles.position = centre + add_child(particles) + + +# ── Player ─────────────────────────────────────────────────────────────────── + +func _spawn_player(pid: int) -> CharacterBody3D: + var player := super._spawn_player(pid) + var s: Vector3 = _spawn_points[randi() % _spawn_points.size()] + player.position = s + Vector3(randf_range(-1.2, 1.2), 0, randf_range(-1.2, 1.2)) + return player diff --git a/scenes/maps/sakura_crossing/sakura_crossing_builder.gd.uid b/scenes/maps/sakura_crossing/sakura_crossing_builder.gd.uid new file mode 100644 index 0000000..1406c8d --- /dev/null +++ b/scenes/maps/sakura_crossing/sakura_crossing_builder.gd.uid @@ -0,0 +1 @@ +uid://revyva8kljmd diff --git a/scenes/maps/sakura_palette.gd b/scenes/maps/sakura_palette.gd new file mode 100644 index 0000000..e742247 --- /dev/null +++ b/scenes/maps/sakura_palette.gd @@ -0,0 +1,228 @@ +extends Object +class_name SakuraPalette + +## The palette, ported from the Sakura Crossing reference (MIT). +## +## Kept as hex strings rather than float triples so every value can be checked +## against the source line by line. `Color(hex)` parses sRGB, which is what the +## reference authors in and what a `source_color` shader uniform expects. +## +## ── The discipline, which matters more than any individual value ───────────── +## +## The palette is deliberately NARROW, and it is narrow in a specific way: +## +## * The masses are pale. Walls, paving, plaster, concrete and sky are all +## high-value and low-saturation. A cel image is read from its dark end, so +## the large areas have to stay out of the way or there is nothing left for +## the ink line and the shadow bands to be read against. +## * The greens lean TEAL, never yellow. A yellow-green mass at this scale +## goes sour and fights the pink. +## * There are exactly four saturated accents — red, yellow, blue, teal — and +## they are reserved for FOCAL objects: the crossing gear, the vending +## machines, a shop awning, the torii. If a saturated colour is spread over +## a large area it becomes the loudest thing in the frame and the eye stops +## going where the level designer wants it to. +## * Shadow is a HUE, not a value. Nothing here has a "dark version"; the cel +## shader swings every shadow band toward a cool violet instead. That is why +## there are so few dark entries in this table — the darks are made by the +## light, not by the paint. +## +## Deviating from those four rules is what makes a cel scene look like a +## low-poly 3D scene with a filter on it. + +# ── Sky & atmosphere ───────────────────────────────────────────────────────── +const SKY_TOP := Color("8fbdea") +const SKY_MID := Color("d4e8fa") +const SKY_HAZE := Color("fbe7e9") +const CLOUD := Color("fdfaf8") +const CLOUD_SHADE := Color("e6e6f2") +const FOG := Color("e6ecf7") +const HILL := Color("c6cfe6") +const HILL_FAR := Color("d8dded") + +# ── Light ──────────────────────────────────────────────────────────────────── +const SUN := Color("fff1d8") +const FILL := Color("a9bdf5") +const HEMI_SKY := Color("dcecff") +const HEMI_GROUND := Color("b6a6c6") + +# ── Ink ────────────────────────────────────────────────────────────────────── +const INK := Color("39324f") +const INK_SOFT := Color("4a4468") + +# ── Ground ─────────────────────────────────────────────────────────────────── +const ROAD := Color("8e8a9c") +const ROAD_WORN := Color("9a95a6") +const ROAD_DARK := Color("7b7689") +const LINE_WHITE := Color("f4f2f6") +const LINE_YELLOW := Color("f0c341") +const TACTILE := Color("f2c53d") +const SIDEWALK := Color("dcd8e2") +const SIDEWALK_ALT := Color("e7e2e6") +const CURB := Color("c7c2d0") +const CONCRETE := Color("d9d5dd") +const CONCRETE_MID := Color("c2bdc8") +const CONCRETE_DARK := Color("a7a2b0") +const GUTTER := Color("bdb8c4") +const DRAIN := Color("6d687a") +const DIRT := Color("c9bfae") +const GRAVEL := Color("a9a3ab") +const BALLAST := Color("7d7686") + +# ── Buildings ──────────────────────────────────────────────────────────────── +const WALL_WHITE := Color("faf6ef") +const WALL_CREAM := Color("f2e7d3") +const WALL_BLUE := Color("d6e3ee") +const WALL_BEIGE := Color("e7dbc4") +const WALL_GRAY := Color("dedee6") +const WALL_PINK := Color("f0dcda") +const WALL_TEA := Color("dccdb6") +const WALL_SAGE := Color("dde2d6") +const ROOF_SLATE := Color("59617a") +const ROOF_BLUE := Color("4d5c78") +const ROOF_BROWN := Color("6b585c") +const ROOF_TEAL := Color("4f6b70") +const TRIM := Color("8b8496") +const GLASS := Color("9dc0d4") +const GLASS_DARK := Color("53627a") +const SHUTTER := Color("6e6a7a") +const SHUTTER_LIGHT := Color("847f92") + +## The wall tones, as a list, for generators that want variety by index. +const WALLS: Array[Color] = [ + WALL_WHITE, WALL_CREAM, WALL_BLUE, WALL_BEIGE, + WALL_GRAY, WALL_PINK, WALL_TEA, WALL_SAGE, +] +const ROOFS: Array[Color] = [ROOF_SLATE, ROOF_BLUE, ROOF_BROWN, ROOF_TEAL] + +# ── Accents ────────────────────────────────────────────────────────────────── +# The four saturated colours, plus their deep variants. Focal objects only. +const RED := Color("e0453f") +const RED_DEEP := Color("b5322f") +const RED_SOFT := Color("ef6a60") +const YELLOW := Color("f4c033") +const YELLOW_DEEP := Color("d39c1f") +const BLACK := Color("322e3b") +const BLACK_SOFT := Color("453f4f") +const TEAL := Color("2f9c9a") +const TEAL_DEEP := Color("22736f") +const BLUE := Color("3d6ec4") +const BLUE_DEEP := Color("2a4f97") +const ORANGE := Color("ef8a3c") +const PURPLE := Color("8f6fb5") + +# ── Vegetation ─────────────────────────────────────────────────────────────── +const LEAF := Color("5aa578") +const LEAF_DEEP := Color("3f7f60") +const LEAF_PALE := Color("84bd97") +const GRASS := Color("86ab84") +const TRUNK := Color("9a8082") +const TRUNK_DARK := Color("765f62") +const MOSS := Color("7d9c74") +const BAMBOO := Color("94b06b") +const CEDAR := Color("3f6b52") + +# ── Cherry blossom ─────────────────────────────────────────────────────────── +const BLOSSOM := Color("fbc6d8") +const BLOSSOM_LIGHT := Color("fff0f4") +const BLOSSOM_WARM := Color("fedde2") +const BLOSSOM_DEEP := Color("f0a3c0") +const PETAL := Color("fcd9e4") +const PETAL_DEEP := Color("f6bccf") + +# ── Railway ────────────────────────────────────────────────────────────────── +const RAIL_METAL := Color("6b6472") +const RAIL_HEAD := Color("c2bcc4") +const SLEEPER := Color("6d6576") +const SLEEPER_LIGHT := Color("847b8c") +const GATE_YELLOW := Color("f4c033") +const GATE_BLACK := Color("322e3b") +const SIGNAL_RED := Color("f2453c") +const SIGNAL_OFF := Color("6a3b44") +const CABINET := Color("d8d5da") +const CABINET_TOP := Color("b6b2bc") + +# ── Train ──────────────────────────────────────────────────────────────────── +const TRAIN_BODY := Color("f7f2e6") +const TRAIN_BODY_SHADE := Color("e6dfd0") +const TRAIN_STRIPE := Color("2f7fd0") +const TRAIN_STRIPE2 := Color("3fae9a") +const TRAIN_WINDOW := Color("3a4258") +const TRAIN_SKIRT := Color("9aa0ad") +const TRAIN_ROOF := Color("bdb8bd") +const TRAIN_DOOR := Color("eae4d8") + +# ── Metal & props ──────────────────────────────────────────────────────────── +const METAL := Color("b8bcc6") +const METAL_DARK := Color("878b96") +const METAL_WARM := Color("c9c0b4") +const MIRROR_BACK := Color("e4a83c") +const MIRROR_FACE := Color("c8d8e4") +const VEND_WHITE := Color("f8f5f0") +const VEND_RED := Color("db4038") +const VEND_TEAL := Color("2e9a98") +const CRATE := Color("3f7fbf") +const CRATE_ALT := Color("e25a4a") +const BASKET := Color("db5a4a") +const BIN := Color("5d8fb8") + +## Bright can/bottle colours for vending machine shelves. +const DRINKS: Array[Color] = [ + Color("e0453f"), Color("f4c033"), Color("3d6ec4"), Color("2f9c9a"), + Color("ef8a3c"), Color("8f6fb5"), Color("5aa578"), Color("f4f2f6"), + Color("e86f9c"), Color("44b4d8"), Color("c94f7a"), Color("9dbb3c"), +] + +# ── Shopping street ────────────────────────────────────────────────────────── +const AWNING_GREEN := Color("4f8f6a") +const AWNING_ORANGE := Color("e08a3c") +const AWNING_BLUE := Color("4a7fae") +const AWNING_CREAM := Color("efe0c2") +const LANTERN := Color("f6e2c0") +const LANTERN_LIT := Color("ffd9a0") +const NOREN := Color("2f4a72") +const NOREN_RED := Color("b5322f") +const NOREN_CREAM := Color("f2e8d6") +const AWNINGS: Array[Color] = [ + AWNING_GREEN, AWNING_ORANGE, AWNING_BLUE, AWNING_CREAM, + NOREN, NOREN_RED, +] + +# ── Shrine ─────────────────────────────────────────────────────────────────── +const TORII := Color("d8412f") +const TORII_DEEP := Color("a72f23") +const SHRINE_WOOD := Color("a9744f") +const SHRINE_WOOD_DARK := Color("8a604a") +const SHRINE_ROOF := Color("69707e") +const SHRINE_STONE := Color("cfcad2") +const ROPE := Color("f0e5ca") +const EMA := Color("e9d9b6") + +# ── Stone & water ──────────────────────────────────────────────────────────── +const STONE := Color("c6c0cb") +const STONE_DARK := Color("a39daf") +const STONE_WARM := Color("cfc6bc") +const WATER := Color("93b8ce") +const WATER_DEEP := Color("6d90ad") +const WATER_SKY := Color("cadff0") +const WATER_PETAL := Color("f3cada") + + +## ── Shadow tints ───────────────────────────────────────────────────────────── +## +## The cool violet each material's shadow bands swing toward. The reference's +## default is 0x6c5f8c and most things take it; the departures are all the same +## kind of decision, which is that a shadow keeps a memory of what the surface is +## MADE of: +## +## TINT_WARM timber, blossom, clay, lantern paper. Pushing these the full +## way to violet turns cedar grey and turns a cherry canopy into a +## storm cloud, which is the one thing it may never do. +## TINT_COOL metal, glass, water, rail. These genuinely are cooler in shade +## than anything else in the frame, and letting them go further +## than the default is most of what makes metal read as metal +## without a specular highlight. +const TINT := Color("6c5f8c") +const TINT_WARM := Color("8a7286") +const TINT_COOL := Color("5a5a8c") +const TINT_FOLIAGE := Color("6a7a8c") diff --git a/scenes/maps/sakura_palette.gd.uid b/scenes/maps/sakura_palette.gd.uid new file mode 100644 index 0000000..e7891ed --- /dev/null +++ b/scenes/maps/sakura_palette.gd.uid @@ -0,0 +1 @@ +uid://dwnx8r8t7pjyn diff --git a/scenes/maps/sakura_signage.gd b/scenes/maps/sakura_signage.gd new file mode 100644 index 0000000..22ff2d9 --- /dev/null +++ b/scenes/maps/sakura_signage.gd @@ -0,0 +1,220 @@ +extends Object +class_name SakuraSignage + +## Runtime type, for signage, road markings and every other piece of lettering +## in the world. +## +## ── Why this exists ────────────────────────────────────────────────────────── +## +## The reference is emphatic about it: "Every sign, fascia, lantern and price +## strip above is drawn at runtime with Canvas2D — there is not one image asset +## in `src/`." That is not a technical footnote, it is most of why the place +## reads as a place. A Japanese street is *made* of lettering — fascia boards, +## projecting 袖看板, noren, price strips, road paint, the station name board — +## and a street built without any of it reads as an architectural massing study, +## which is exactly what the first pass of this map was. +## +## ── How it is done here ────────────────────────────────────────────────────── +## +## Godot's answer to the reference's Canvas2D is `TextMesh`: real geometry +## generated from a font, which suits this renderer better than a drawn texture +## would. The whole map is untextured flat colour, so a texture-mapped sign would +## be the only bitmap in it — and, more to the point, geometry gets INKED. Every +## glyph picks up the screen-space outline exactly as the buildings do, which is +## what makes the lettering sit in the picture rather than on top of it. +## +## Two measurements decided the settings: +## +## depth = 0 Extruded text costs 4 500 vertices for four kanji; flat text +## costs 2 100. The side walls of the extrusion are most of the +## mesh and none of them are ever seen on a sign fixed to a wall. +## Flat glyphs sit a centimetre proud of their board instead. +## curve_step 8 Tessellation. 0.5 (the default) is 43 000 vertices for three +## signs; 8 is 13 700 and is visually identical at any distance a +## sign is read from — and slightly chunkier glyph curves are, if +## anything, more correct for a cel image. +## +## The font is a SYSTEM font stack, and deliberately the same one the reference +## uses: 'Yu Gothic', 'Meiryo', 'Hiragino Kaku Gothic ProN', 'MS Gothic'. That +## ships no font file and gets a real Japanese face on any machine likely to run +## this. If none of them resolve, TextMesh yields an empty mesh — signs go blank, +## nothing crashes. + +const FONT_STACK := ["Yu Gothic", "Meiryo", "Hiragino Kaku Gothic ProN", "MS Gothic"] + +## TextMesh generates at this nominal size and is scaled by `pixel_size`, so one +## font size serves every sign in the map and the cache keys stay small. +const NOMINAL_SIZE := 64 +## Measured: a TextMesh's AABB height is about 1.229x (font_size * pixel_size) +## for CJK glyphs. Used to turn a requested cap height in metres into a +## pixel_size, so callers can ask for "0.4 m tall" and get it. +const HEIGHT_RATIO := 1.229 + +static var _font: Font = null +static var _mesh_cache: Dictionary = {} + + +static func font() -> Font: + if _font != null: + return _font + var f := SystemFont.new() + f.font_names = PackedStringArray(FONT_STACK) + # Signage is nearly always heavy. A regular weight at these sizes reads as + # body copy, which no shop fascia in the world is set in. + f.font_weight = 700 + f.subpixel_positioning = TextServer.SUBPIXEL_POSITIONING_DISABLED + _font = f + return _font + + +## A flat text mesh whose glyphs are `height` metres tall, centred on its origin. +## Cached by (text, height) — a row of shops reusing a name shares one mesh. +static func text_mesh(text: String, height: float) -> TextMesh: + var key := "%s|%.3f" % [text, height] + if _mesh_cache.has(key): + return _mesh_cache[key] + var tm := TextMesh.new() + tm.font = font() + tm.text = text + tm.font_size = NOMINAL_SIZE + tm.pixel_size = height / (float(NOMINAL_SIZE) * HEIGHT_RATIO) + tm.depth = 0.0 + tm.curve_step = 8.0 + tm.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + tm.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _mesh_cache[key] = tm + return tm + + +## How wide that mesh comes out, for sizing the board behind it. +static func text_width(text: String, height: float) -> float: + return text_mesh(text, height).get_aabb().size.x + + +## Place a line of text. `rot` is in degrees; the mesh faces +Z unrotated, so a +## sign on a wall facing -Z wants rot.y = 180 and a marking painted on the road +## wants rot.x = -90. +## `inked` decides whether the screen-space ink pass may draw around the glyphs. +## +## Raised lettering on a sign board WANTS the outline — it is an object standing +## off a wall and the ink is what seats it there. **Paint on the ground does +## not.** Road markings were inked at first and the result was hollow letters: +## a stroke of 止まれ read at a grazing angle is only a few pixels wide, the ink +## fired on both of its edges, and the two outlines met in the middle and ate the +## letter. Which is also the correct art call — no cel painter inks around each +## stroke of road paint, because paint on tarmac has no silhouette. Passing +## `false` puts the glyphs in the transparent pass, where the ink cannot see +## them and they composite as flat marks on the road. +static func label(parent: Node, text: String, height: float, colour: Color, + pos: Vector3, rot: Vector3 = Vector3.ZERO, + inked: bool = true) -> MeshInstance3D: + var mi := MeshInstance3D.new() + mi.mesh = text_mesh(text, height) + mi.material_override = _unlit(colour, inked) + mi.position = pos + if rot != Vector3.ZERO: + mi.rotation_degrees = rot + parent.add_child(mi) + return mi + + +## Vertical writing (縦書き), one glyph per line, running downward. +## +## There is no vertical layout mode on TextMesh, and it does not need one: a +## 袖看板 sets one character per line anyway, so stacking single-character meshes +## IS the correct typography here rather than a workaround for a missing feature. +static func vertical_label(parent: Node, text: String, height: float, + colour: Color, top: Vector3, rot: Vector3 = Vector3.ZERO) -> void: + var pitch := height * 1.34 + var chars := text.length() + for i in range(chars): + var p := top - Vector3(0, float(i) * pitch, 0) + if rot != Vector3.ZERO: + # Stack along the sign's own down axis, which is world down for any + # rotation this map uses (signs are never rolled). + p = top - Vector3(0, float(i) * pitch, 0) + label(parent, text[i], height, colour, p, rot) + + +static var _unlit_cache: Dictionary = {} + +static func _unlit(colour: Color, inked: bool = true) -> StandardMaterial3D: + var key := "%s|%s" % [colour.to_html(), inked] + if _unlit_cache.has(key): + return _unlit_cache[key] + var m := StandardMaterial3D.new() + m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + m.albedo_color = colour + if not inked: + # The transparent pass runs after the ink quad and writes no depth, so + # these glyphs are never seen by the edge detector. See label(). + m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + m.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED + # Lettering is read from both sides on projecting signs and hanging banners, + # and a one-sided glyph that vanishes as you walk past it is worse than no + # glyph at all. + m.cull_mode = BaseMaterial3D.CULL_DISABLED + _unlit_cache[key] = m + return m + + +# ── The copy ───────────────────────────────────────────────────────────────── +# +# Every string in the map, gathered here so the writing can be read as writing +# rather than hunted for through the geometry. It is all ordinary suburban +# signage — shops, warnings, road paint — because that is what the reference's +# world is made of and what makes it feel like somewhere rather than like a set. + +## Shop names for the 商店街. One per unit, cycled. +const SHOP_NAMES := [ + "さくら書店", "ラーメン", "花や", "米店", "パン工房", "喫茶ふみきり", + "電器", "時計店", "八百屋", "酒", "薬局", "たばこ", + "クリーニング", "精肉店", "魚屋", "そば", "文具", "写真館", +] + +## The short word that goes on a projecting side sign, keyed to the same index. +const SHOP_VERTICAL := [ + "本", "麺", "花", "米", "パン", "珈琲", + "電器", "時計", "青果", "酒", "薬", "煙草", + "洗濯", "肉", "魚", "蕎麦", "文具", "写真", +] + +## Noren carry one or two characters, never a whole name. +const NOREN_TEXT := ["営業中", "ゆ", "そば", "酒", "めし", "喫茶"] + +## Painted on the road, in the order they are met approaching the crossing. +const ROAD_STOP := "止まれ" +const ROAD_SLOW := "徐行" + +## The crossing itself. +const CROSSING_WARN := "踏切注意" +const CROSSING_STOP := "とまれ" + +## The station. A 駅名標 gives the name in kana, then the neighbours either side. +const STATION_KANA := "さくらふみきり" +const STATION_KANJI := "桜踏切" +const STATION_ROMAJI := "Sakura-Fumikiri" +const STATION_PREV := "みなみおか" +const STATION_NEXT := "きたの" + +## Street name plates. +const STREET_NAMES := ["桜通り", "駅前通り", "踏切通り", "本町"] + +## Vending machine price strips. +const PRICES := ["¥130", "¥150", "¥160", "¥110"] + +## Large painted signs for the exposed FLANK walls of a terrace. +## +## Wherever a run of shops is cut — at the crossing, at the map edge — the end +## unit turns a blank two- or three-storey gable to the street, and the render +## showed several of them as the largest empty surfaces in the map. In a real +## shopping street that wall is the most valuable advertising space on the block +## and is never blank; here it is also the one place a piece of type can be big +## enough to read from the far side of the tracks. +const WALL_ADS := [ + "大売出し", "さくら電器", "駐車場", "うどん", "スーパー", + "歯科", "不動産", "美容室", +] + +## Painted on the platform where the doors will be. +const PLATFORM_MARK := "乗車口" diff --git a/scenes/maps/sakura_signage.gd.uid b/scenes/maps/sakura_signage.gd.uid new file mode 100644 index 0000000..99fe8ee --- /dev/null +++ b/scenes/maps/sakura_signage.gd.uid @@ -0,0 +1 @@ +uid://k2moxja60sn0 diff --git a/scenes/maps/test_level/test_level.tscn b/scenes/maps/test_level/test_level.tscn index 10afb6d..2ae9947 100644 --- a/scenes/maps/test_level/test_level.tscn +++ b/scenes/maps/test_level/test_level.tscn @@ -1,6 +1,6 @@ [gd_scene format=3 uid="uid://c5k51gbegvxss"] -[ext_resource type="Script" uid="uid://c7ltcn37gfd71" path="res://debug/test_level_builder.gd" id="1"] +[ext_resource type="Script" uid="uid://btkp2l168jj45" path="res://debug/test_level_builder.gd" id="1"] [node name="TestLevel" type="Node3D" unique_id=130984349] script = ExtResource("1") diff --git a/tools/audio_audit.py b/tools/audio_audit.py new file mode 100644 index 0000000..ee02af3 --- /dev/null +++ b/tools/audio_audit.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python +"""Release gate for runtime audio assets. + +Checks format, headroom, clipping, DC offset, loop continuity and event-family +coverage. It intentionally uses only the Python standard library plus +ffprobe, matching the rest of the repository's tooling. +""" + +from __future__ import annotations + +import argparse +import json +import math +import os +import shutil +import statistics +import struct +import subprocess +import sys +import tempfile +import wave +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SOUNDS = ROOT / "assets" / "sounds" +FFPROBE = shutil.which("ffprobe") +FFMPEG = shutil.which("ffmpeg") + +FAMILIES = { + "weapon_ak47": 3, + "weapon_m4": 3, + "weapon_mp7": 3, + "weapon_dmr": 2, + "weapon_awp": 2, + "weapon_shotgun": 2, + "weapon_nailgun": 2, + "weapon_plasma": 2, + "weapon_rocket": 1, + "weapon_swarm": 2, + "weapon_mortar": 1, + "weapon_knife": 3, + "weapon_reload": 1, + "explosion_deep": 5, + "impact_bullet": 3, + "footstep_concrete": 4, + "footstep_metal": 4, + "footstep_wood": 4, + "footstep_glass": 4, + "ui_hover_premium": 2, + "ui_click_premium": 2, + "ui_confirm_premium": 1, + "ui_kill_premium": 1, + "ui_error_premium": 1, + "ui_equip_premium": 1, + "movement_grapple_launch": 1, + "movement_grapple_latch": 1, +} + +LOOPS = { + "movement_wind.wav": 2, + "movement_slide_concrete.wav": 2, + "movement_slide_metal.wav": 2, + "movement_slide_wood.wav": 2, + "movement_slide_glass.wav": 2, + "movement_wallrun.wav": 2, + "movement_grapple_reel.wav": 2, + "projectile_rocket_loop.wav": 1, + "projectile_swarm_loop.wav": 1, +} + +ONESHOTS = ( + "movement_dash_00.wav", + "movement_jump_00.wav", + "movement_double_jump_00.wav", + "movement_vault_00.wav", + "movement_land_00.wav", + "movement_grapple_launch_00.wav", + "movement_grapple_latch_00.wav", +) + +def db(value: float) -> float: + return 20.0 * math.log10(max(value, 1.0e-12)) + + +def wav_metrics(path: Path) -> dict[str, float | int]: + with wave.open(str(path), "rb") as source: + channels = source.getnchannels() + rate = source.getframerate() + width = source.getsampwidth() + count = source.getnframes() + if width != 2: + raise ValueError(f"{path.name}: expected 16-bit PCM, got {width * 8}-bit") + raw = source.readframes(count) + samples = struct.unpack("<" + "h" * (len(raw) // 2), raw) + mono: list[float] = [] + for frame in range(count): + start = frame * channels + mono.append(sum(samples[start:start + channels]) / (32768.0 * channels)) + peak = max((abs(v) for v in mono), default=0.0) + mean = sum(mono) / max(1, count) + rms = math.sqrt(sum(v * v for v in mono) / max(1, count)) + diffs = [mono[i] - mono[i - 1] for i in range(1, count)] + diff_rms = math.sqrt(sum(v * v for v in diffs) / max(1, len(diffs))) + sorted_diff = sorted(abs(v) for v in diffs) + typical_diff = sorted_diff[len(sorted_diff) // 2] if sorted_diff else 0.0 + seam = abs(mono[-1] - mono[0]) if mono else 0.0 + clipped = sum(1 for value in samples if abs(value) >= 32767) + return { + "channels": channels, + "rate": rate, + "duration": count / rate, + "peak_db": db(peak), + "rms_db": db(rms), + "crest_db": db(peak / max(rms, 1.0e-12)), + "dc": abs(mean), + "clip_samples": clipped, + "roughness": diff_rms / max(rms, 1.0e-12), + "seam": seam, + "seam_ratio": seam / max(typical_diff, 1.0e-6), + } + + +def inspect_music(path: Path) -> dict: + if not FFPROBE: + raise RuntimeError("ffprobe is required to inspect Ogg music") + result = subprocess.run([ + FFPROBE, "-v", "error", "-show_entries", + "format=duration:stream=sample_rate,channels", "-of", "json", str(path), + ], check=True, capture_output=True, text=True) + return json.loads(result.stdout) + + +def spectral_metrics(path: Path) -> dict[str, float]: + """Active-frame spectral distribution from FFmpeg's spectrum analyzer.""" + if not FFMPEG: + raise RuntimeError("ffmpeg is required for spectral audio gates") + result = subprocess.run([ + FFMPEG, "-hide_banner", "-nostats", "-v", "error", "-i", str(path), + "-af", "aspectralstats=measure=centroid+flatness,ametadata=print:file=-", + "-f", "null", os.devnull, + ], check=True, capture_output=True, text=True) + centroids: list[float] = [] + flatness: list[float] = [] + for line in (result.stdout + result.stderr).splitlines(): + if "centroid=" in line: + value = float(line.rsplit("=", 1)[1]) + if value > 20.0: + centroids.append(value) + elif "flatness=" in line: + value = float(line.rsplit("=", 1)[1]) + if value > 0.0: + flatness.append(value) + if not centroids or not flatness: + raise RuntimeError(f"{path.name}: no active spectral frames") + centroids.sort() + flatness.sort() + + def percentile(values: list[float], amount: float) -> float: + index = min(len(values) - 1, round((len(values) - 1) * amount)) + return values[index] + + return { + "centroid": statistics.median(centroids), + "centroid_p90": percentile(centroids, 0.90), + "flatness": statistics.median(flatness), + "flatness_p90": percentile(flatness, 0.90), + } + + +def music_metrics(path: Path) -> dict[str, float | int]: + if not FFMPEG: + raise RuntimeError("ffmpeg is required to inspect the music loop seam") + with tempfile.TemporaryDirectory(prefix="papaya-music-audit-") as folder: + decoded = Path(folder) / "music.wav" + subprocess.run([ + FFMPEG, "-hide_banner", "-loglevel", "error", "-y", + "-i", str(path), "-ar", "48000", "-ac", "2", + "-c:a", "pcm_s16le", str(decoded), + ], check=True) + return wav_metrics(decoded) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--verbose", action="store_true") + args = parser.parse_args() + failures: list[str] = [] + checked: set[Path] = set() + metrics_by_name: dict[str, dict[str, float | int]] = {} + + for family, minimum in FAMILIES.items(): + matches = sorted(SOUNDS.glob(f"{family}_[0-9][0-9].wav")) + if len(matches) < minimum: + failures.append( + f"{family}: needs {minimum} variations, found {len(matches)}") + checked.update(matches) + + for filename, channels in LOOPS.items(): + path = SOUNDS / filename + if not path.exists(): + failures.append(f"{filename}: missing loop") + continue + checked.add(path) + metrics = wav_metrics(path) + if metrics["channels"] != channels: + failures.append( + f"{filename}: expected {channels} channels, got {metrics['channels']}") + if metrics["duration"] < 3.5: + failures.append(f"{filename}: loop shorter than 3.5 seconds") + if filename == "movement_wind.wav" and metrics["duration"] < 40.0: + failures.append( + f"{filename}: speed-wind cycle is still perceptibly short " + f"({metrics['duration']:.1f}s)") + if metrics["seam_ratio"] > 6.0: + failures.append( + f"{filename}: audible loop seam ratio {metrics['seam_ratio']:.2f}") + + for filename in ONESHOTS: + path = SOUNDS / filename + if not path.exists(): + failures.append(f"{filename}: missing movement one-shot") + else: + checked.add(path) + + for path in sorted(checked): + try: + metrics = wav_metrics(path) + except (wave.Error, ValueError) as exc: + failures.append(str(exc)) + continue + metrics_by_name[path.name] = metrics + if metrics["rate"] != 48_000: + failures.append(f"{path.name}: expected 48 kHz, got {metrics['rate']}") + if metrics["peak_db"] > -1.0: + failures.append(f"{path.name}: unsafe peak {metrics['peak_db']:.2f} dBFS") + if metrics["clip_samples"]: + failures.append( + f"{path.name}: {metrics['clip_samples']} clipped PCM samples") + if metrics["dc"] > 0.012: + failures.append(f"{path.name}: DC offset {metrics['dc']:.4f}") + # High first-difference energy catches the old raw/screaming noise beds. + if path.name in LOOPS and metrics["roughness"] > 1.45: + failures.append( + f"{path.name}: harsh loop roughness {metrics['roughness']:.2f}") + if args.verbose: + print( + f"{path.name:34} peak {metrics['peak_db']:6.1f} " + f"rms {metrics['rms_db']:6.1f} crest {metrics['crest_db']:5.1f} " + f"rough {metrics['roughness']:4.2f} seam {metrics['seam_ratio']:4.2f}") + + # Constant friction/motor loops must remain materially structured. Wind is + # exempt from flatness because genuine turbulent air is broadband by nature. + for filename in LOOPS: + if filename == "movement_wind.wav": + continue + try: + spectrum = spectral_metrics(SOUNDS / filename) + if spectrum["flatness"] > 0.48: + failures.append( + f"{filename}: noise-like spectral flatness " + f"{spectrum['flatness']:.2f}") + if spectrum["centroid_p90"] > 3_200.0: + failures.append( + f"{filename}: grating upper-spectrum centroid " + f"{spectrum['centroid_p90']:.0f} Hz") + if args.verbose: + print( + f"{filename:34} centroid {spectrum['centroid']:7.0f} Hz " + f"p90 {spectrum['centroid_p90']:7.0f} Hz " + f"flatness {spectrum['flatness']:.3f}/" + f"{spectrum['flatness_p90']:.3f}") + except (RuntimeError, ValueError, subprocess.CalledProcessError) as exc: + failures.append(str(exc)) + + # The criticized high/childish families stay physically low, while the + # explosion family must sit clearly above firearms before bus processing. + low_tone_files = [ + *sorted(SOUNDS.glob("ui_*_premium_[0-9][0-9].wav")), + SOUNDS / "movement_dash_00.wav", + SOUNDS / "movement_jump_00.wav", + SOUNDS / "movement_double_jump_00.wav", + SOUNDS / "movement_grapple_launch_00.wav", + SOUNDS / "movement_grapple_latch_00.wav", + ] + for path in low_tone_files: + try: + spectrum = spectral_metrics(path) + if spectrum["centroid"] > 4_200.0: + failures.append( + f"{path.name}: high-pitched centroid " + f"{spectrum['centroid']:.0f} Hz") + if spectrum["centroid_p90"] > 2_800.0: + failures.append( + f"{path.name}: harsh transient centroid " + f"{spectrum['centroid_p90']:.0f} Hz") + roughness = float(metrics_by_name[path.name]["roughness"]) + if roughness > 0.18: + failures.append( + f"{path.name}: grating transient roughness {roughness:.2f}") + if args.verbose: + print( + f"{path.name:34} tone {spectrum['centroid']:7.0f} Hz " + f"p90 {spectrum['centroid_p90']:7.0f} Hz") + except (RuntimeError, ValueError, subprocess.CalledProcessError) as exc: + failures.append(str(exc)) + + explosion_rms = [ + float(values["rms_db"]) for name, values in metrics_by_name.items() + if name.startswith("explosion_deep_") + ] + firearm_rms = [ + float(values["rms_db"]) for name, values in metrics_by_name.items() + if name.startswith("weapon_") and "reload" not in name + ] + if explosion_rms and firearm_rms: + if statistics.median(explosion_rms) < statistics.median(firearm_rms) + 4.0: + failures.append( + "explosions lack the required 4 dB median body advantage") + if max(explosion_rms) < -15.0: + failures.append("explosion family is still too quiet") + + music = SOUNDS / "music_menu_premium.ogg" + if not music.exists(): + failures.append("music_menu_premium.ogg: missing") + else: + try: + info = inspect_music(music) + stream = info["streams"][0] + duration = float(info["format"]["duration"]) + if int(stream["sample_rate"]) != 48_000 or int(stream["channels"]) != 2: + failures.append("music_menu_premium.ogg: expected 48 kHz stereo") + if duration < 25.0: + failures.append( + f"music_menu_premium.ogg: too short ({duration:.1f}s)") + metrics = music_metrics(music) + if metrics["seam_ratio"] > 6.0: + failures.append( + "music_menu_premium.ogg: audible loop seam " + f"ratio {metrics['seam_ratio']:.2f}") + if metrics["clip_samples"] or metrics["peak_db"] > -1.0: + failures.append("music_menu_premium.ogg: unsafe decoded peak") + except (RuntimeError, KeyError, ValueError, subprocess.CalledProcessError) as exc: + failures.append(f"music_menu_premium.ogg: {exc}") + + print( + f"AUDIO_AUDIT files={len(checked) + int(music.exists())} " + f"families={len(FAMILIES)} loops={len(LOOPS)} failures={len(failures)}") + for failure in failures: + print("FAIL:", failure) + if failures: + return 1 + print("AUDIO_AUDIT PASSED") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/audit_animation_speeds.py b/tools/audit_animation_speeds.py new file mode 100644 index 0000000..9189baa --- /dev/null +++ b/tools/audit_animation_speeds.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Measure authored ground speed from root-motion animation clips. + +Usage: + blender --background --python tools/audit_animation_speeds.py -- \ + [ ...] + +The reported metres/second is the ground-plane displacement of the resolved +hips bone over the clip's own duration. Use the root-motion edition of an +animation library; an in-place edition correctly reports approximately zero. +""" +import os +import sys + +import bpy +from mathutils import Vector + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from rig_map import RigRoles + + +argv = sys.argv +argv = argv[argv.index("--") + 1:] if "--" in argv else [] +if len(argv) < 2: + print(__doc__) + sys.exit(1) + +LIBRARY = argv[0] +CLIPS = argv[1:] + + +def assign_action(armature, action): + if not armature.animation_data: + armature.animation_data_create() + armature.animation_data.action = action + try: + if len(action.slots): + armature.animation_data.action_slot = action.slots[0] + except (AttributeError, TypeError): + pass + + +def hips_world_position(armature, hips_name, frame): + bpy.context.scene.frame_set(int(round(frame))) + depsgraph = bpy.context.evaluated_depsgraph_get() + evaluated = armature.evaluated_get(depsgraph) + return ( + evaluated.matrix_world @ evaluated.pose.bones[hips_name].matrix + ).translation + + +def main(): + bpy.ops.object.select_all(action="SELECT") + bpy.ops.object.delete() + bpy.ops.import_scene.gltf(filepath=LIBRARY) + + armatures = [obj for obj in bpy.data.objects if obj.type == "ARMATURE"] + if len(armatures) != 1: + print(f"ERROR: expected one armature, found {len(armatures)}") + sys.exit(1) + armature = armatures[0] + roles = RigRoles(armature) + if not roles.hips: + print("ERROR: could not resolve hips bone") + sys.exit(1) + + fps = ( + bpy.context.scene.render.fps + / bpy.context.scene.render.fps_base + ) + if CLIPS == ["--list"]: + for action in sorted(bpy.data.actions, key=lambda item: item.name): + print(action.name) + return + + failed = False + print(f"AUTHORED_SPEED_AUDIT fps={fps:.3f} hips={roles.hips}") + for clip in CLIPS: + action = bpy.data.actions.get(clip) + if action is None: + print(f"ERROR: missing clip {clip}") + failed = True + continue + assign_action(armature, action) + first, last = (float(value) for value in action.frame_range) + start = hips_world_position(armature, roles.hips, first) + end = hips_world_position(armature, roles.hips, last) + displacement = end - start + displacement.z = 0.0 + duration = (last - first) / fps + speed = displacement.length / duration if duration > 1e-6 else 0.0 + print( + f"AUTHORED_SPEED {clip} speed={speed:.5f}m/s " + f"distance={displacement.length:.5f}m " + f"duration={duration:.5f}s " + f"delta=({displacement.x:.5f},{displacement.y:.5f})" + ) + + sys.exit(1 if failed else 0) + + +main() diff --git a/tools/build_audio_assets.py b/tools/build_audio_assets.py new file mode 100644 index 0000000..bc4c0e9 --- /dev/null +++ b/tools/build_audio_assets.py @@ -0,0 +1,655 @@ +#!/usr/bin/env python +"""Build Papaya Shooter's mastered audio set. + +The checked-in source recordings remain untouched. This script creates the +runtime masters with deterministic filtering/layering so their peaks, tone and +loop seams cannot drift between hand edits. + +Requirements: + Python 3.10+ + ffmpeg on PATH +""" + +from __future__ import annotations + +import math +import random +import shutil +import struct +import subprocess +import tempfile +import urllib.request +import wave +import hashlib +import zipfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SOUNDS = ROOT / "assets" / "sounds" +RATE = 48_000 +FFMPEG = shutil.which("ffmpeg") +FFPROBE = shutil.which("ffprobe") +MENU_SOURCE = "https://opengameart.org/sites/default/files/pynchon.mp3" +MENU_SOURCE_SHA256 = "295bc30016587d909a8b1d494cc7fb13550f5c74807221e9eb0eb9f2769d4e3b" +SOURCE_FILES = { + # Field-recorded wind, already authored as a loop. CC-BY 3.0: + # Jonathan Shaw (InspectorJ), loop edit by AntumDeluge. + "wind": ( + "https://opengameart.org/sites/default/files/wind-01_0.flac", + "4027477c493e26db8e2ea53793a30e376c4665de67c0333d7480777cd85e1639", + "wind-01.flac", + ), + # All sources below are CC0. See assets/sounds/SOURCES.md. + "generator": ( + "https://opengameart.org/sites/default/files/generator_loop.wav", + "64b174cf8ef71e7f71aa03cf29864ab489f7be1f624456cab916043da38911bd", + "generator_loop.wav", + ), + "stomp": ( + "https://opengameart.org/sites/default/files/stomp.flac", + "c9114c31ab07a371db12bfb22190d3a8e08424813319b67b6040df1f23fa9bba", + "stomp.flac", + ), + "scrapes": ( + "https://opengameart.org/sites/default/files/scrapes.zip", + "9598070ddd77ed99e922df1d4472a249624adcd945794d1b6eba88ac77f6a014", + "scrapes.zip", + ), + "lightclunk1": ( + "https://opengameart.org/sites/default/files/lightclunk1.wav", + "d5a2aa8764c02924f6ed6fbaad0c650ab2d57ec57a626cee9e6645f941dbc8fa", + "lightclunk1.wav", + ), + "lightclunk2": ( + "https://opengameart.org/sites/default/files/lightclunk2.wav", + "987be837a5941ded9a81572c319b9bf8f57d9f89741a5781fc7104adc87d60f5", + "lightclunk2.wav", + ), + "mechanical2": ( + "https://opengameart.org/sites/default/files/mechanical2.wav", + "27b4b6c3517c90d635295fca48fccd747950f869d680c871662f26038c3665ef", + "mechanical2.wav", + ), + "mechanical_explosion": ( + "https://opengameart.org/sites/default/files/mechanical_explosion.wav", + "13e9f713bd0d5921b223b01d29d68bb7625a1a1fbf1894af35ebfd5ef72c8632", + "mechanical_explosion.wav", + ), + "air_whoosh": ( + "https://opengameart.org/sites/default/files/whoosh2_0.wav", + "d3fbff5bb78b289af16b2dfa29083f07ef83b0cbaff4514c48dfda16d082b468", + "whoosh2.wav", + ), + "rocket_engine": ( + "https://opengameart.org/sites/default/files/rocket_engine.001.wav", + "dc82ef86d6af2278d6592d20372737c32620f1c1957290c79f6643af5381f277", + "rocket_engine.wav", + ), +} + + +def run(args: list[str]) -> None: + subprocess.run(args, cwd=ROOT, check=True) + + +def ffmpeg(*args: str) -> None: + if not FFMPEG: + raise SystemExit("ffmpeg is required to build audio assets") + run([FFMPEG, "-hide_banner", "-loglevel", "error", "-y", *args]) + + +def media_duration(path: Path) -> float: + if not FFPROBE: + raise SystemExit("ffprobe is required to build audio assets") + result = subprocess.run([ + FFPROBE, "-v", "error", "-show_entries", "format=duration", + "-of", "default=nw=1:nk=1", str(path), + ], check=True, capture_output=True, text=True) + return float(result.stdout.strip()) + + +def write_wav(path: Path, channels: list[list[float]]) -> None: + count = min(map(len, channels)) + path.parent.mkdir(parents=True, exist_ok=True) + with wave.open(str(path), "wb") as out: + out.setnchannels(len(channels)) + out.setsampwidth(2) + out.setframerate(RATE) + frames = bytearray() + for i in range(count): + for channel in channels: + value = max(-0.999, min(0.999, channel[i])) + frames.extend(struct.pack(" list[float]: + current = max((abs(v) for v in values), default=1.0) + gain = peak / max(current, 1.0e-9) + return [v * gain for v in values] + + +def download_sources(temp: Path) -> dict[str, Path]: + """Fetch pinned, licensed recordings and fail if any upstream file drifts.""" + result: dict[str, Path] = {} + for key, (url, expected_sha, filename) in SOURCE_FILES.items(): + path = temp / filename + urllib.request.urlretrieve(url, path) + digest = hashlib.sha256(path.read_bytes()).hexdigest() + if digest != expected_sha: + raise RuntimeError( + f"{key} source checksum changed ({digest}); verify {url}") + result[key] = path + scrape_dir = temp / "scrapes" + with zipfile.ZipFile(result["scrapes"]) as archive: + archive.extractall(scrape_dir) + for index in range(1, 9): + result[f"scrape{index}"] = scrape_dir / f"scrape-{index}.wav" + return result + + +def make_loop(source: Path, output: Path, fade: float = 0.18) -> None: + """Rotate a processed recording through a tail/head crossfade.""" + duration = media_duration(source) + if duration < 3.5 + fade: + raise RuntimeError(f"{source.name}: source is too short to loop") + graph = ( + f"[0:a]atrim=start={fade}:end={duration - fade}," + "asetpts=PTS-STARTPTS[body];" + f"[0:a]atrim=start={duration - fade}:end={duration}," + "asetpts=PTS-STARTPTS[tail];" + f"[0:a]atrim=start=0:end={fade},asetpts=PTS-STARTPTS[head];" + f"[tail][head]acrossfade=d={fade}:c1=tri:c2=tri[seam];" + "[body][seam]concat=n=2:v=0:a=1[out]" + ) + ffmpeg("-i", str(source), "-filter_complex", graph, "-map", "[out]", + "-ar", str(RATE), "-c:a", "pcm_s16le", str(output)) + + +def make_recording_sequence( + inputs: list[Path], output: Path, filters: str, + channels: int = 2) -> None: + """Concatenate physical gestures, process them, then close the loop seam.""" + base = output.with_name(f"{output.stem}_base.wav") + args: list[str] = [] + labels: list[str] = [] + graph_parts: list[str] = [] + for index, source in enumerate(inputs): + args.extend(["-i", str(source)]) + label = f"s{index}" + labels.append(f"[{label}]") + graph_parts.append( + f"[{index}:a]aresample={RATE}," + "aformat=sample_fmts=fltp:channel_layouts=mono," + f"afade=t=in:st=0:d=0.015,afade=t=out:st=0.95:d=0.12[{label}]") + graph_parts.append( + "".join(labels) + f"concat=n={len(inputs)}:v=0:a=1," + f"{filters}[processed]") + ffmpeg(*args, "-filter_complex", ";".join(graph_parts), "-map", "[processed]", + "-ar", str(RATE), "-ac", str(channels), "-c:a", "pcm_s16le", + str(base)) + make_loop(base, output) + + +def make_source_loop( + source: Path, output: Path, filters: str, seconds: float, + channels: int) -> None: + base = source.parent / f"{output.stem}_base.wav" + ffmpeg("-i", str(source), "-af", f"{filters},atrim=duration={seconds}", + "-ar", str(RATE), "-ac", str(channels), "-c:a", "pcm_s16le", + str(base)) + make_loop(base, output, 0.30) + + +def build_movement_loops(temp: Path, sources: dict[str, Path]) -> None: + # Use almost the complete source-authored wind loop. The previous 8-second + # edit exposed its cadence during sustained movement; forty-eight seconds + # makes repetition disappear in normal play. A narrower stereo image and + # less low ambience make it read as speed over the body, not weather around + # the whole map. + make_source_loop( + sources["wind"], SOUNDS / "movement_wind.wav", + "atrim=start=0," + "highpass=f=150,lowpass=f=5200:p=2," + "equalizer=f=420:t=q:w=0.8:g=2.0," + "equalizer=f=2600:t=q:w=1.0:g=-4.5," + "stereotools=mlev=0.82:slev=0.40," + "acompressor=threshold=-25dB:ratio=1.8:attack=28:release=220:makeup=1dB," + "alimiter=limit=0.20:attack=7:release=140:level=0", + 48.8, 2) + + scrape_sets = { + "movement_slide_concrete.wav": ( + [sources["scrape2"], sources["scrape6"], sources["scrape4"]], + "highpass=f=55,lowpass=f=2200:p=2," + "equalizer=f=420:t=q:w=0.9:g=1," + "equalizer=f=1650:t=q:w=1.0:g=-3," + "acompressor=threshold=-28dB:ratio=2.0:attack=18:release=150:makeup=0dB," + "alimiter=limit=0.22:attack=7:release=100:level=0", + ), + "movement_slide_metal.wav": ( + [sources["scrape3"], sources["scrape8"], sources["scrape1"]], + "highpass=f=75,lowpass=f=2400:p=2," + "equalizer=f=680:t=q:w=1.1:g=1.5," + "equalizer=f=1750:t=q:w=1.1:g=-4," + "acompressor=threshold=-28dB:ratio=2.0:attack=20:release=150:makeup=0dB," + "alimiter=limit=0.21:attack=7:release=105:level=0", + ), + "movement_slide_wood.wav": ( + [sources["scrape5"], sources["scrape2"], sources["scrape7"]], + "highpass=f=45,lowpass=f=1800:p=2," + "equalizer=f=180:t=q:w=0.9:g=2," + "equalizer=f=1350:t=q:w=1.0:g=-2.5," + "acompressor=threshold=-29dB:ratio=2.0:attack=20:release=165:makeup=0dB," + "alimiter=limit=0.22:attack=7:release=110:level=0", + ), + "movement_slide_glass.wav": ( + [sources["scrape1"], sources["scrape4"], sources["scrape8"]], + "highpass=f=95,lowpass=f=2300:p=2," + "equalizer=f=1000:t=q:w=1.0:g=1," + "equalizer=f=1850:t=q:w=1.1:g=-4," + "acompressor=threshold=-29dB:ratio=2.0:attack=20:release=145:makeup=0dB," + "alimiter=limit=0.19:attack=7:release=100:level=0", + ), + "movement_wallrun.wav": ( + [sources["scrape7"], sources["scrape5"], sources["scrape2"]], + "highpass=f=55,lowpass=f=2000:p=2," + "equalizer=f=240:t=q:w=0.9:g=1.5," + "equalizer=f=1500:t=q:w=1.0:g=-3," + "acompressor=threshold=-29dB:ratio=2.0:attack=20:release=160:makeup=0dB," + "alimiter=limit=0.19:attack=7:release=110:level=0", + ), + } + for filename, (recordings, filters) in scrape_sets.items(): + make_recording_sequence(recordings, temp / filename, filters) + shutil.copyfile(temp / filename, SOUNDS / filename) + + # Cable reel: discrete ratchet/clutch recordings alternating with short + # friction pulls. A continuous boiler/generator bed read as another jet, + # which is semantically wrong even after aggressive filtering. + make_recording_sequence( + [sources["mechanical2"], sources["scrape3"], + sources["lightclunk2"], sources["scrape8"]], + SOUNDS / "movement_grapple_reel.wav", + "highpass=f=80,lowpass=f=2200:p=2," + "equalizer=f=260:t=q:w=0.8:g=1.5," + "equalizer=f=1700:t=q:w=1.1:g=-3," + "volume=6dB," + "acompressor=threshold=-28dB:ratio=1.8:attack=18:release=135:makeup=0dB," + "alimiter=limit=0.18:attack=7:release=105:level=0") + make_source_loop( + sources["generator"], SOUNDS / "projectile_rocket_loop.wav", + "atrim=start=7,highpass=f=38,lowpass=f=1700:p=2," + "equalizer=f=95:t=q:w=0.8:g=3," + "acompressor=threshold=-26dB:ratio=2.0:attack=20:release=160:makeup=0dB," + "alimiter=limit=0.25:attack=7:release=110:level=0", + 5.0, 1) + make_source_loop( + sources["generator"], SOUNDS / "projectile_swarm_loop.wav", + "atrim=start=12,highpass=f=75,lowpass=f=2300:p=2," + "equalizer=f=380:t=q:w=0.9:g=1.5," + "acompressor=threshold=-27dB:ratio=2.0:attack=18:release=145:makeup=0dB," + "alimiter=limit=0.20:attack=7:release=100:level=0", + 5.0, 1) + + +def synth_foot_layer(kind: str, variant: int, path: Path) -> None: + rng = random.Random(3000 + variant * 101 + sum(map(ord, kind))) + duration = 0.24 + count = round(duration * RATE) + values: list[float] = [] + low_state = 0.0 + configs = { + "concrete": (92.0, 0.065, 0.10, 0.14), + "metal": (128.0, 0.050, 0.07, 0.11), + "wood": (78.0, 0.085, 0.045, 0.18), + "glass": (112.0, 0.055, 0.055, 0.11), + } + body_hz, body_decay, grit, body_gain = configs[kind] + body_hz *= 1.0 + (variant - 1.5) * 0.018 + for i in range(count): + t = i / RATE + noise = rng.uniform(-1.0, 1.0) + low_state += 0.075 * (noise - low_state) + heel = math.exp(-t / body_decay) + toe_t = max(0.0, t - (0.052 + variant * 0.003)) + toe = math.exp(-toe_t / 0.030) if t > 0.050 else 0.0 + body = math.sin(math.tau * body_hz * t) * heel * body_gain + texture = low_state * (heel * 0.7 + toe * 0.3) * grit + ring = 0.0 + if kind == "metal": + ring = (math.sin(math.tau * 510.0 * t) + + 0.55 * math.sin(math.tau * 735.0 * t)) \ + * math.exp(-t / 0.055) * 0.022 + elif kind == "wood": + ring = math.sin(math.tau * 142.0 * t) \ + * math.exp(-t / 0.070) * 0.035 + elif kind == "glass": + ring = (math.sin(math.tau * 740.0 * t) + + 0.35 * math.sin(math.tau * 1120.0 * t)) \ + * math.exp(-t / 0.038) * 0.012 + values.append(body + texture + ring) + write_wav(path, [normalize(values, 0.28)]) + + +def process_footsteps(temp: Path) -> None: + sources = sorted(SOUNDS.glob("footstep*.wav")) + # Do not recursively remaster outputs from a previous run. + sources = [p for p in sources if not any( + token in p.stem for token in ("concrete", "metal", "wood", "glass"))] + if len(sources) < 4: + raise SystemExit("Expected at least four source footstep recordings") + + filters = { + "concrete": "highpass=f=55,lowpass=f=6800,equalizer=f=180:t=q:w=0.8:g=2", + "metal": "highpass=f=65,lowpass=f=8200,equalizer=f=2400:t=q:w=1.2:g=-1", + "wood": "highpass=f=45,lowpass=f=4700,equalizer=f=150:t=q:w=0.8:g=3", + "glass": "highpass=f=65,lowpass=f=7600,equalizer=f=3200:t=q:w=1.0:g=-2", + } + for kind, source_filter in filters.items(): + for variant in range(4): + layer = temp / f"{kind}_{variant}.wav" + synth_foot_layer(kind, variant, layer) + out = SOUNDS / f"footstep_{kind}_{variant:02d}.wav" + graph = ( + f"[0:a]{source_filter},volume=0.72[src];" + "[1:a]volume=0.55[layer];" + "[src][layer]amix=inputs=2:duration=longest:normalize=0," + "afade=t=out:st=0.19:d=0.05," + "alimiter=limit=0.50:attack=2:release=35:level=0[out]" + ) + ffmpeg("-i", str(sources[variant % len(sources)]), "-i", str(layer), + "-filter_complex", graph, "-map", "[out]", + "-ar", str(RATE), "-ac", "1", "-c:a", "pcm_s16le", str(out)) + + +def process_layered(source: Path, output: Path, profile: str) -> None: + profiles = { + "auto": (12_500, 250, 0.30, "36|72", "0.14|0.07", 0.68), + "heavy": (11_500, 220, 0.40, "58|116|210", "0.18|0.10|0.055", 0.70), + "shotgun": (10_500, 190, 0.48, "65|135|245", "0.20|0.11|0.06", 0.68), + "energy": (10_000, 310, 0.24, "45|95", "0.12|0.06", 0.62), + "launcher": (9_000, 175, 0.50, "70|145|260", "0.19|0.10|0.05", 0.68), + "melee": (9_500, 350, 0.12, "30|65", "0.08|0.04", 0.58), + } + lowpass, body_lp, body_gain, delays, decays, limit = profiles[profile] + graph = ( + "[0:a]aformat=sample_fmts=fltp:channel_layouts=mono,asplit=3[d][b][t];" + f"[d]highpass=f=38,lowpass=f={lowpass}," + "equalizer=f=3600:t=q:w=1.1:g=-2.2,volume=0.74[dry];" + f"[b]highpass=f=32,lowpass=f={body_lp},volume={body_gain}[body];" + f"[t]aecho=0.8:0.55:{delays}:{decays}," + f"lowpass=f={min(6200, lowpass)},volume=0.24[tail];" + "[dry][body][tail]amix=inputs=3:duration=longest:normalize=0," + f"alimiter=limit={limit}:attack=3:release=80:level=0[out]" + ) + ffmpeg("-i", str(source), "-filter_complex", graph, "-map", "[out]", + "-ar", str(RATE), "-ac", "1", "-c:a", "pcm_s16le", str(output)) + + +def process_weapons() -> None: + groups = { + "weapon_ak47": ("ak47_fire", "auto"), + "weapon_m4": ("m4_fire", "auto"), + "weapon_mp7": ("mp7_fire", "auto"), + "weapon_dmr": ("dmr_fire", "heavy"), + "weapon_awp": ("awp_fire", "heavy"), + "weapon_shotgun": ("shotgun_fire", "shotgun"), + "weapon_nailgun": ("nailgun_fire", "energy"), + "weapon_plasma": ("plasma_fire", "energy"), + "weapon_rocket": ("rocket_fire", "launcher"), + "weapon_swarm": ("swarm_fire", "launcher"), + "weapon_mortar": ("mortar_fire", "launcher"), + "weapon_knife": ("knife_swing", "melee"), + } + for output_id, (source_id, profile) in groups.items(): + sources = sorted(SOUNDS.glob(f"{source_id}*.wav")) + if not sources: + raise SystemExit(f"No source files found for {source_id}") + for variant, source in enumerate(sources[:4]): + process_layered( + source, SOUNDS / f"{output_id}_{variant:02d}.wav", profile) + + reload_source = SOUNDS / "shotgun_reload.wav" + reload_output = SOUNDS / "weapon_reload_00.wav" + # This master did not change in the industrial redesign. Avoid rewriting a + # stream an open Godot editor may currently have mapped into a live weapon. + if not reload_output.exists(): + ffmpeg("-i", str(reload_source), + "-af", "highpass=f=55,lowpass=f=9000," + "equalizer=f=3900:t=q:w=1.1:g=-3," + "acompressor=threshold=-16dB:ratio=2.2:attack=800:release=100:" + "makeup=1.5dB," + "alimiter=limit=0.56:attack=2:release=60:level=0", + "-ar", str(RATE), "-ac", "1", "-c:a", "pcm_s16le", + str(reload_output)) + + +def process_explosions(sources: dict[str, Path]) -> None: + candidates = sorted(SOUNDS.glob("explosion*.wav")) + original_explosions = [p for p in candidates if "deep" not in p.stem] + for variant, source in enumerate(original_explosions[:5]): + graph = ( + "[0:a]aformat=sample_fmts=fltp:channel_layouts=mono,asplit=3[d][b][t];" + "[d]highpass=f=26,lowpass=f=6800," + "equalizer=f=3400:t=q:w=1:g=-4.5,volume=0.92[dry];" + "[b]highpass=f=24,lowpass=f=185,volume=0.90[body];" + "[t]aecho=0.8:0.55:95|205|390:0.24|0.13|0.07," + "lowpass=f=4200,volume=0.36[tail];" + "[1:a]aformat=sample_fmts=fltp:channel_layouts=mono," + "highpass=f=30,lowpass=f=3200:p=2," + "equalizer=f=1900:t=q:w=1.0:g=-4,volume=0.26[metal];" + "[dry][body][tail][metal]amix=inputs=4:duration=longest:normalize=0," + "acompressor=threshold=-14dB:ratio=2.0:attack=6:release=260:makeup=3dB," + "alimiter=limit=0.86:attack=4:release=190:level=0[out]" + ) + ffmpeg("-i", str(source), "-i", str(sources["mechanical_explosion"]), + "-filter_complex", graph, "-map", "[out]", + "-ar", str(RATE), "-ac", "1", "-c:a", "pcm_s16le", + str(SOUNDS / f"explosion_deep_{variant:02d}.wav")) + + +def make_layered_oneshot( + output: Path, + layers: list[tuple[Path, float, int, float, float, bool]], + final_filters: str) -> None: + """Build one tactile cue from recorded gestures, never broadband synthesis. + + layer tuple: (source, volume, delay_ms, pitch_ratio, trim_seconds, reverse) + """ + args: list[str] = [] + graph: list[str] = [] + labels: list[str] = [] + for index, (source, volume, delay_ms, pitch, trim, reverse) in enumerate(layers): + args.extend(["-i", str(source)]) + chain = ( + f"[{index}:a]aresample={RATE}," + "aformat=sample_fmts=fltp:channel_layouts=mono," + f"atrim=start=0:end={trim},asetpts=PTS-STARTPTS") + if reverse: + chain += ",areverse" + if abs(pitch - 1.0) > 0.001: + chain += f",asetrate={RATE * pitch:.2f},aresample={RATE}" + effective_duration = trim / pitch + fade_out = min(0.10, effective_duration * 0.28) + fade_start = max(0.01, effective_duration - fade_out) + chain += ( + f",volume={volume}," + "afade=t=in:st=0:d=0.008," + f"afade=t=out:st={fade_start:.3f}:d={fade_out:.3f}," + f"adelay={delay_ms}") + label = f"l{index}" + graph.append(f"{chain}[{label}]") + labels.append(f"[{label}]") + graph.append( + "".join(labels) + + f"amix=inputs={len(layers)}:duration=longest:normalize=0," + + final_filters + "[out]") + ffmpeg(*args, "-filter_complex", ";".join(graph), "-map", "[out]", + "-ar", str(RATE), "-ac", "1", "-c:a", "pcm_s16le", str(output)) + + +def process_movement_oneshots(sources: dict[str, Path]) -> None: + # Ordinary movement stays physical. Only the impossible dash/double jump + # receives a restrained thrust layer: the audio version of the 90/10 rule. + events = { + "movement_dash": [ + (sources["air_whoosh"], 0.56, 0, 0.96, 0.48, False), + (sources["rocket_engine"], 0.54, 5, 0.90, 0.44, False), + (sources["lightclunk2"], 0.10, 28, 0.74, 0.20, False), + ], + "movement_jump": [ + # Two short shoe contacts: heel/body, then toe release. No whoosh, + # reversed scrape, engine, mechanism or other thrust-like source. + (SOUNDS / "footstep_concrete_00.wav", 0.72, 0, 1.14, 0.19, False), + (SOUNDS / "footstep_concrete_02.wav", 0.32, 18, 0.98, 0.15, False), + ], + "movement_double_jump": [ + (sources["air_whoosh"], 0.52, 0, 0.94, 0.46, False), + (sources["rocket_engine"], 0.58, 4, 0.88, 0.46, False), + (sources["lightclunk1"], 0.09, 24, 0.72, 0.20, False), + ], + "movement_vault": [ + (sources["scrape5"], 0.22, 0, 0.84, 0.48, False), + (sources["air_whoosh"], 0.34, 0, 0.92, 0.38, False), + (sources["lightclunk1"], 0.12, 70, 0.76, 0.22, False), + ], + "movement_land": [ + (sources["stomp"], 0.74, 0, 0.84, 0.50, False), + (sources["lightclunk1"], 0.15, 18, 0.72, 0.24, False), + ], + "movement_grapple_launch": [ + # Spring release, cable friction and trigger clutch. + (sources["mechanical2"], 0.32, 0, 0.88, 0.28, False), + (sources["scrape5"], 0.16, 12, 0.86, 0.25, False), + (sources["lightclunk1"], 0.28, 0, 0.90, 0.18, False), + ], + "movement_grapple_latch": [ + # Hook impact followed by the pawl closing on the line. + (sources["lightclunk2"], 0.56, 0, 0.92, 0.24, False), + (sources["stomp"], 0.18, 0, 1.08, 0.18, False), + (sources["mechanical2"], 0.24, 34, 1.04, 0.25, False), + ], + } + for output_id, layers in events.items(): + is_jet = output_id in {"movement_dash", "movement_double_jump"} + is_grapple = output_id.startswith("movement_grapple_") + upper_cut = 2100 if is_grapple else (2200 if is_jet else 2800) + presence_cut = -6.5 if is_grapple else (-5.5 if is_jet else -3.5) + make_layered_oneshot( + SOUNDS / f"{output_id}_00.wav", layers, + f"highpass=f=42,lowpass=f={upper_cut}:p=2," + "equalizer=f=170:t=q:w=0.9:g=2," + f"equalizer=f=1500:t=q:w=1.1:g={presence_cut}," + "acompressor=threshold=-20dB:ratio=1.7:attack=14:release=125:makeup=0dB," + "loudnorm=I=-20:TP=-6:LRA=5," + "alimiter=limit=0.48:attack=6:release=100:level=0") + + impacts = sorted(SOUNDS.glob("bullet_impact*.wav")) + for variant, source in enumerate(impacts[:4]): + ffmpeg("-i", str(source), + "-af", "highpass=f=70,lowpass=f=9500," + "equalizer=f=4100:t=q:w=1.2:g=-3," + "aecho=0.8:0.5:24|52:0.10|0.04," + "alimiter=limit=0.42:attack=2:release=45:level=0", + "-ar", str(RATE), "-ac", "1", "-c:a", "pcm_s16le", + str(SOUNDS / f"impact_bullet_{variant:02d}.wav")) + + +def envelope(t: float, attack: float, release: float, duration: float) -> float: + return min(1.0, t / max(attack, 1.0e-6)) \ + * min(1.0, (duration - t) / max(release, 1.0e-6)) + + +def build_ui(sources: dict[str, Path]) -> None: + # Interface feedback is a family of relay/lever gestures. Pitch stays below + # the old arcade chirp range, and semantic weight comes from mechanism size. + cues = { + "ui_hover_premium_00.wav": [ + (sources["lightclunk2"], 0.34, 0, 0.68, 0.18, False), + ], + "ui_hover_premium_01.wav": [ + (sources["lightclunk1"], 0.30, 0, 0.66, 0.18, False), + ], + "ui_click_premium_00.wav": [ + (sources["lightclunk1"], 0.52, 0, 0.72, 0.26, False), + ], + "ui_click_premium_01.wav": [ + (sources["lightclunk2"], 0.54, 0, 0.70, 0.24, False), + ], + "ui_confirm_premium_00.wav": [ + (sources["lightclunk2"], 0.34, 0, 0.68, 0.20, False), + (sources["lightclunk1"], 0.46, 46, 0.70, 0.26, False), + ], + "ui_kill_premium_00.wav": [ + (sources["stomp"], 0.44, 0, 0.78, 0.42, False), + (sources["lightclunk2"], 0.38, 62, 0.72, 0.24, False), + ], + "ui_error_premium_00.wav": [ + (sources["mechanical2"], 0.28, 0, 0.66, 0.34, False), + (sources["lightclunk1"], 0.38, 42, 0.62, 0.24, False), + ], + "ui_equip_premium_00.wav": [ + (sources["mechanical2"], 0.38, 0, 0.68, 0.42, False), + (sources["lightclunk2"], 0.30, 58, 0.66, 0.22, False), + ], + } + for filename, layers in cues.items(): + make_layered_oneshot( + SOUNDS / filename, layers, + "highpass=f=60,lowpass=f=2400:p=2," + "equalizer=f=210:t=q:w=0.9:g=1.5," + "equalizer=f=1500:t=q:w=1.0:g=-3," + "equalizer=f=2200:t=q:w=1.0:g=-4," + "acompressor=threshold=-22dB:ratio=1.6:attack=12:release=95:makeup=0dB," + "loudnorm=I=-22:TP=-8:LRA=4," + "alimiter=limit=0.40:attack=5:release=85:level=0") + + +def build_menu_music(temp: Path) -> None: + source = temp / "pynchon.mp3" + urllib.request.urlretrieve(MENU_SOURCE, source) + digest = hashlib.sha256(source.read_bytes()).hexdigest() + if digest != MENU_SOURCE_SHA256: + raise RuntimeError( + "PYNCHON source checksum changed; verify the CC0 download before rebuilding") + mastered = temp / "pynchon_mastered.wav" + ffmpeg("-i", str(source), + "-af", "highpass=f=28,lowpass=f=15500," + "loudnorm=I=-18:TP=-2:LRA=5", + "-ar", str(RATE), "-ac", "2", "-c:a", "pcm_s16le", str(mastered)) + duration = media_duration(mastered) + fade = 0.25 + # MP3 encoder delay leaves even a source-authored loop with a boundary + # click. Rotate by the crossfade length: output starts at +fade, and its + # final crossfade ends at that same musical point. + graph = ( + f"[0:a]atrim=start={fade}:end={duration - fade},asetpts=PTS-STARTPTS[body];" + f"[0:a]atrim=start={duration - fade}:end={duration},asetpts=PTS-STARTPTS[tail];" + f"[0:a]atrim=start=0:end={fade},asetpts=PTS-STARTPTS[head];" + f"[tail][head]acrossfade=d={fade}:c1=tri:c2=tri[seam];" + "[body][seam]concat=n=2:v=0:a=1[out]" + ) + ffmpeg("-i", str(mastered), "-filter_complex", graph, "-map", "[out]", + "-ar", str(RATE), "-ac", "2", "-c:a", "libvorbis", "-q:a", "5", + str(SOUNDS / "music_menu_premium.ogg")) + + +def main() -> None: + SOUNDS.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="papaya-audio-") as raw_temp: + temp = Path(raw_temp) + sources = download_sources(temp) + build_movement_loops(temp, sources) + process_footsteps(temp) + process_weapons() + process_explosions(sources) + process_movement_oneshots(sources) + build_ui(sources) + build_menu_music(temp) + print("Built premium audio assets in", SOUNDS) + + +if __name__ == "__main__": + main() diff --git a/tools/build_cmu_locomotion_library.py b/tools/build_cmu_locomotion_library.py new file mode 100644 index 0000000..849887d --- /dev/null +++ b/tools/build_cmu_locomotion_library.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Build the compact CMU backward-locomotion animation library. + +Usage: + blender --background --python tools/build_cmu_locomotion_library.py -- \ + <76_11.bvh> + +Subject 76 trial 11 is CMU's optical capture described as "quick large steps +backwards." The script finds a naturally matching gait-cycle boundary, keeps +the captured bone motion intact, removes gameplay-owned horizontal root travel, +and exports only that cycle. +""" + +import os +import sys + +import bpy + + +argv = sys.argv +argv = argv[argv.index("--") + 1:] if "--" in argv else [] +if len(argv) != 2: + print(__doc__) + sys.exit(1) + +SOURCE, OUTPUT = argv +SEARCH_START = range(170, 281) +SEARCH_END = range(300, 411) +MIN_CYCLE_FRAMES = 95 +MAX_CYCLE_FRAMES = 175 +SIGNATURE_BONES = ( + "Hips", "LowerBack", "Spine", "Spine1", "Neck", "Head", + "LeftArm", "LeftForeArm", "RightArm", "RightForeArm", + "LeftUpLeg", "LeftLeg", "LeftFoot", + "RightUpLeg", "RightLeg", "RightFoot", +) + + +def action_fcurves(action): + """Blender 5.x channelbag f-curves for an imported BVH action.""" + curves = [] + for layer in action.layers: + for strip in layer.strips: + for slot in action.slots: + channelbag = strip.channelbag(slot) + if channelbag is not None: + curves.extend(channelbag.fcurves) + return curves + + +def pose_signature(armature, frame): + bpy.context.scene.frame_set(frame) + signature = [] + for name in SIGNATURE_BONES: + bone = armature.pose.bones.get(name) + if bone is None: + continue + signature.append(bone.matrix.to_quaternion().normalized()) + hips = armature.pose.bones["Hips"].matrix.translation + return signature, hips.copy() + + +def find_cycle(armature): + samples = { + frame: pose_signature(armature, frame) + for frame in range(min(SEARCH_START), max(SEARCH_END) + 1) + } + best = None + for start in SEARCH_START: + start_rotations, start_hips = samples[start] + for end in SEARCH_END: + duration = end - start + if duration < MIN_CYCLE_FRAMES or duration > MAX_CYCLE_FRAMES: + continue + end_rotations, end_hips = samples[end] + rotation_error = sum( + rotation.rotation_difference(other).angle ** 2 + for rotation, other in zip(start_rotations, end_rotations) + ) / max(len(start_rotations), 1) + # Match the captured vertical phase but intentionally ignore + # horizontal displacement: it proves the performer travelled. + vertical_error = (end_hips.z - start_hips.z) ** 2 * 0.02 + score = rotation_error + vertical_error + if best is None or score < best[0]: + best = (score, start, end) + if best is None: + raise RuntimeError("No plausible gait cycle found in the capture") + return best + + +def crop_action(action, start, end): + shift = start - 1 + curves = action_fcurves(action) + for curve in curves: + points = curve.keyframe_points + for index in range(len(points) - 1, -1, -1): + if points[index].co.x < start or points[index].co.x > end: + points.remove(points[index]) + for point in points: + point.co.x -= shift + point.handle_left.x -= shift + point.handle_right.x -= shift + + # The gameplay body owns horizontal travel. Preserve every captured + # rotation and the vertical compression/lift, but keep the cycle in place. + hips_path = 'pose.bones["Hips"].location' + for curve in curves: + if curve.data_path != hips_path or curve.array_index not in (0, 1): + continue + baseline = curve.evaluate(1.0) + for point in curve.keyframe_points: + point.co.y -= baseline + point.handle_left.y -= baseline + point.handle_right.y -= baseline + + +def main(): + bpy.ops.wm.read_factory_settings(use_empty=True) + bpy.context.scene.render.fps = 120 + bpy.context.scene.render.fps_base = 1.0 + bpy.ops.import_anim.bvh( + filepath=SOURCE, + axis_forward="-Z", + axis_up="Y", + ) + armature = next( + (obj for obj in bpy.data.objects if obj.type == "ARMATURE"), None + ) + if armature is None or armature.animation_data is None: + raise RuntimeError(f"{SOURCE}: no animated armature") + action = armature.animation_data.action + if action is None: + raise RuntimeError(f"{SOURCE}: no action") + + score, start, end = find_cycle(armature) + crop_action(action, start, end) + action.name = "RunBackward" + action.use_fake_user = True + bpy.context.scene.frame_start = 1 + bpy.context.scene.frame_end = end - start + 1 + + track = armature.animation_data.nla_tracks.new() + track.name = "RunBackward" + strip = track.strips.new("RunBackward", 0, action) + strip.name = "RunBackward" + track.mute = True + armature.animation_data.action = None + + os.makedirs(os.path.dirname(os.path.abspath(OUTPUT)), exist_ok=True) + bpy.ops.export_scene.gltf( + filepath=OUTPUT, + export_format="GLB", + export_yup=True, + export_skins=True, + export_animations=True, + export_animation_mode="NLA_TRACKS", + export_bake_animation=False, + export_optimize_animation_size=True, + export_optimize_animation_keep_anim_armature=False, + ) + print( + f"CMU 76_11 frames {start}..{end} -> RunBackward " + f"({(end - start) / 120.0:.3f}s, phase score {score:.6f})" + ) + print(f"Wrote {OUTPUT}") + + +main() diff --git a/tools/build_wallrun_library.py b/tools/build_wallrun_library.py new file mode 100644 index 0000000..28c1a4b --- /dev/null +++ b/tools/build_wallrun_library.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Build the compact, animation-only wall-run library used by retarget.py. + +The source clips are Uisco's authored UE4 Mannequin wall-run pair: +https://uisco.itch.io/wall-running-animations + +Usage: + blender --background --python tools/build_wallrun_library.py -- \ + + +The two FBX files use the same UE4 mannequin skeleton. This script keeps one +copy of that skeleton, names the actions explicitly, and exports no mesh. +""" +import os +import sys + +import bpy + + +argv = sys.argv +argv = argv[argv.index("--") + 1:] if "--" in argv else [] +if len(argv) != 3: + print(__doc__) + sys.exit(1) + +LEFT, RIGHT, OUTPUT = argv + + +def import_clip(path, name): + before_objects = set(bpy.data.objects) + before_actions = set(bpy.data.actions) + bpy.ops.import_scene.fbx(filepath=path) + objects = [obj for obj in bpy.data.objects if obj not in before_objects] + arms = [obj for obj in objects if obj.type == "ARMATURE"] + actions = [action for action in bpy.data.actions if action not in before_actions] + if len(arms) != 1 or len(actions) != 1: + raise RuntimeError( + f"{path}: expected one armature/action, got " + f"{len(arms)} armatures and {len(actions)} actions" + ) + actions[0].name = name + actions[0].use_fake_user = True + return arms[0], actions[0], objects + + +def main(): + bpy.ops.wm.read_factory_settings(use_empty=True) + left_arm, left_action, left_objects = import_clip(LEFT, "WallRunLeft") + right_arm, right_action, right_objects = import_clip(RIGHT, "WallRunRight") + + left_bones = [bone.name for bone in left_arm.data.bones] + right_bones = [bone.name for bone in right_arm.data.bones] + if left_bones != right_bones: + raise RuntimeError("left/right source skeletons do not match") + + for obj in left_objects + right_objects: + if obj not in (left_arm, right_arm): + bpy.data.objects.remove(obj, do_unlink=True) + bpy.data.objects.remove(right_arm, do_unlink=True) + + if left_arm.animation_data is None: + left_arm.animation_data_create() + left_arm.animation_data.action = None + for track in list(left_arm.animation_data.nla_tracks): + left_arm.animation_data.nla_tracks.remove(track) + for action in (left_action, right_action): + track = left_arm.animation_data.nla_tracks.new() + track.name = action.name + strip = track.strips.new(action.name, 0, action) + strip.name = action.name + track.mute = True + + for action in list(bpy.data.actions): + if action not in (left_action, right_action): + bpy.data.actions.remove(action) + + os.makedirs(os.path.dirname(os.path.abspath(OUTPUT)), exist_ok=True) + bpy.ops.export_scene.gltf( + filepath=OUTPUT, + export_format="GLB", + export_yup=True, + export_animations=True, + export_animation_mode="NLA_TRACKS", + export_skins=True, + export_bake_animation=False, + export_optimize_animation_size=True, + ) + print( + f"Exported WallRunLeft/WallRunRight on " + f"{len(left_arm.data.bones)} bones to {OUTPUT}" + ) + + +main() diff --git a/tools/extract_animation_subset.py b/tools/extract_animation_subset.py new file mode 100644 index 0000000..acaab08 --- /dev/null +++ b/tools/extract_animation_subset.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +Create a compact animation-only GLB from selected actions in a source GLB. + +Usage: + blender --background --python tools/extract_animation_subset.py -- \ + [ ...] + +The source skeleton is retained, all meshes and unrelated actions are removed, +and each requested action is exported as its own NLA track. This keeps external +authored animation libraries small enough to ship as retargeting sources. +""" +import os +import sys + +import bpy + + +argv = sys.argv +argv = argv[argv.index("--") + 1:] if "--" in argv else [] +if len(argv) < 3: + print( + "Usage: blender --background --python " + "tools/extract_animation_subset.py -- " + " [ ...]" + ) + sys.exit(1) + +SOURCE = argv[0] +OUTPUT = argv[1] +REQUESTED = argv[2:] + + +def main(): + bpy.ops.object.select_all(action="SELECT") + bpy.ops.object.delete() + bpy.ops.import_scene.gltf(filepath=SOURCE) + + armatures = [obj for obj in bpy.data.objects if obj.type == "ARMATURE"] + if len(armatures) != 1: + print(f"ERROR: expected one armature, found {len(armatures)}") + sys.exit(1) + armature = armatures[0] + + missing = [name for name in REQUESTED if bpy.data.actions.get(name) is None] + if missing: + print(f"ERROR: requested actions not found: {missing}") + sys.exit(1) + + for obj in list(bpy.data.objects): + if obj != armature: + bpy.data.objects.remove(obj, do_unlink=True) + + if not armature.animation_data: + armature.animation_data_create() + for track in list(armature.animation_data.nla_tracks): + armature.animation_data.nla_tracks.remove(track) + armature.animation_data.action = None + + keep = set() + for name in REQUESTED: + action = bpy.data.actions[name] + action.use_fake_user = True + track = armature.animation_data.nla_tracks.new() + track.name = name + strip = track.strips.new(name, 0, action) + strip.name = name + track.mute = True + keep.add(action) + + for action in list(bpy.data.actions): + if action not in keep: + bpy.data.actions.remove(action) + + os.makedirs(os.path.dirname(os.path.abspath(OUTPUT)), exist_ok=True) + bpy.ops.export_scene.gltf( + filepath=OUTPUT, + export_format="GLB", + export_yup=True, + export_animations=True, + export_animation_mode="NLA_TRACKS", + export_skins=True, + export_bake_animation=True, + ) + print(f"Exported {len(keep)} clips to {OUTPUT}") + + +main() diff --git a/tools/inject_wallrun.py b/tools/inject_wallrun.py new file mode 100644 index 0000000..481cde6 --- /dev/null +++ b/tools/inject_wallrun.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +""" +Retarget the authored left/right wall-run clips into an existing character GLB. + +Unlike retarget.py, this is deliberately animation-only: it does not rebuild +the hierarchy, touch skin weights, subdivide cloth, scale meshes, or rewrite +the rig sidecar. It is safe to use on a character that has already passed the +full one-time character build. + +Usage: + blender --background --python tools/inject_wallrun.py -- \ + <_wallrun.glb> +""" +import math +import os +import sys + +import bpy +from mathutils import Matrix, Quaternion, Vector + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from rig_map import RigRoles, build_map +from retarget_pose import authored_world_rotation, build_segment_pairs + + +argv = sys.argv +argv = argv[argv.index("--") + 1:] if "--" in argv else [] +if len(argv) != 3: + print(__doc__) + sys.exit(1) + +CHARACTER, LIBRARY, OUTPUT = argv +UP = Vector((0.0, 0.0, 1.0)) +CLIPS = ("WallRunLeft", "WallRunRight") + + +def find_armature(objects): + return next((obj for obj in objects if obj.type == "ARMATURE"), None) + + +def assign_action(obj, action): + if obj.animation_data is None: + obj.animation_data_create() + obj.animation_data.action = action + if action is None: + return + try: + if len(action.slots): + obj.animation_data.action_slot = action.slots[0] + except Exception: + pass + + +def world_rest(arm): + return { + bone.name: arm.matrix_world @ bone.matrix_local + for bone in arm.data.bones + } + + +def bone_order(arm): + out = [] + + def walk(bone): + out.append(bone.name) + for child in bone.children: + walk(child) + + for bone in arm.data.bones: + if bone.parent is None: + walk(bone) + return out + + +def rig_forward(arm, roles): + acc = Vector((0.0, 0.0, 0.0)) + for side in ("L", "R"): + for role in ("toe", "foot"): + name = roles.limb.get((role, side)) + if not name: + continue + bone = arm.data.bones[name] + direction = ( + (arm.matrix_world @ bone.tail_local) + - (arm.matrix_world @ bone.matrix_local.translation) + ) + direction.z = 0.0 + if direction.length > 1e-5: + acc += direction.normalized() + break + return acc.normalized() if acc.length > 1e-5 else None + + +def facing_correction(src_arm, src_roles, target_arm, target_roles): + source = rig_forward(src_arm, src_roles) + target = rig_forward(target_arm, target_roles) + if source is None or target is None: + return Quaternion() + angle = Vector((source.x, source.y)).angle_signed( + Vector((target.x, target.y)), 0.0 + ) + return Quaternion(UP, angle) if abs(angle) >= 1e-4 else Quaternion() + + +def solve_pose(arm, order, rest_world, desired_rot, hips, hips_head): + pose_world = {} + basis = {} + bones = arm.data.bones + for name in order: + bone = bones[name] + rest = rest_world[name] + if bone.parent is not None: + parent = bone.parent.name + matrix = pose_world[parent] @ rest_world[parent].inverted() @ rest + else: + matrix = rest + rotation = Quaternion() + if name in desired_rot: + rotation = matrix.to_quaternion().inverted() @ desired_rot[name] + location = Vector((0.0, 0.0, 0.0)) + if name == hips and hips_head is not None: + location = matrix.inverted() @ hips_head + basis[name] = (location, rotation) + pose_world[name] = ( + matrix + @ Matrix.Translation(location) + @ rotation.to_matrix().to_4x4() + ) + return basis + + +def retarget_action(target_arm, target_roles, src_arm, src_roles, mapping, + src_action, clip_name, yaw, scale): + src_rest = world_rest(src_arm) + target_rest = world_rest(target_arm) + target_order = bone_order(target_arm) + src_rest_rot = { + name: matrix.to_quaternion() for name, matrix in src_rest.items() + } + target_rest_rot = { + name: matrix.to_quaternion() for name, matrix in target_rest.items() + } + yaw_inverse = yaw.inverted() + segment_pairs = build_segment_pairs(mapping, src_roles, target_roles) + src_hips = src_roles.hips + target_hips = target_roles.hips + src_hips_rest = src_rest[src_hips].translation.copy() + target_hips_rest = target_rest[target_hips].translation.copy() + + assign_action(src_arm, src_action) + frame_start, frame_end = ( + int(round(value)) for value in src_action.frame_range + ) + baked = bpy.data.actions.new(clip_name) + assign_action(target_arm, baked) + for pose_bone in target_arm.pose.bones: + pose_bone.rotation_mode = "QUATERNION" + + scene = bpy.context.scene + for frame in range(frame_start, frame_end + 1): + scene.frame_set(frame) + depsgraph = bpy.context.evaluated_depsgraph_get() + src_eval = src_arm.evaluated_get(depsgraph) + source_world = src_eval.matrix_world + desired = {} + for target_name, source_name in mapping.items(): + if ( + source_name not in src_eval.pose.bones + or target_name not in target_rest_rot + ): + continue + desired[target_name] = authored_world_rotation( + target_name, + source_name, + src_eval, + source_world, + src_rest_rot, + target_rest, + target_rest_rot, + yaw, + yaw_inverse, + segment_pairs, + ) + + moved = ( + source_world @ src_eval.pose.bones[src_hips].matrix + ).translation + displacement = yaw @ ((moved - src_hips_rest) * scale) + # Gameplay owns horizontal travel. Preserve vertical compression and + # lift from the performance, but keep the cycle in-place. + displacement.x = 0.0 + displacement.y = 0.0 + hips_head = target_hips_rest + displacement + basis = solve_pose( + target_arm, + target_order, + target_rest, + desired, + target_hips, + hips_head, + ) + for name in mapping: + if name not in basis: + continue + pose_bone = target_arm.pose.bones[name] + pose_bone.rotation_quaternion = basis[name][1] + pose_bone.keyframe_insert("rotation_quaternion", frame=frame) + pose_bone = target_arm.pose.bones[target_hips] + pose_bone.location = basis[target_hips][0] + pose_bone.keyframe_insert("location", frame=frame) + + assign_action(target_arm, None) + return baked + + +def add_clip_track(arm, action, name): + action.name = name + action.use_fake_user = True + track = arm.animation_data.nla_tracks.new() + track.name = name + strip = track.strips.new(name, 0, action) + strip.name = name + track.mute = True + + +def main(): + bpy.ops.wm.read_factory_settings(use_empty=True) + bpy.ops.import_scene.gltf(filepath=CHARACTER) + target_arm = find_armature(bpy.data.objects) + if target_arm is None: + raise RuntimeError(f"{CHARACTER}: no armature") + target_roles = RigRoles(target_arm) + missing = target_roles.missing_core() + if missing: + raise RuntimeError(f"{CHARACTER}: missing target roles {missing}") + + # Remove the old relabelled sprint, plus stale versions from a prior run. + for track in list(target_arm.animation_data.nla_tracks): + if track.name in ("WallRun",) + CLIPS: + target_arm.animation_data.nla_tracks.remove(track) + for action in list(bpy.data.actions): + if action.name in ("WallRun",) + CLIPS: + bpy.data.actions.remove(action) + assign_action(target_arm, None) + + before_objects = set(bpy.data.objects) + before_actions = set(bpy.data.actions) + bpy.ops.import_scene.gltf(filepath=LIBRARY) + new_objects = [obj for obj in bpy.data.objects if obj not in before_objects] + new_actions = [ + action for action in bpy.data.actions if action not in before_actions + ] + src_arm = find_armature(new_objects) + if src_arm is None: + raise RuntimeError(f"{LIBRARY}: no source armature") + src_roles = RigRoles(src_arm) + mapping = build_map(src_roles, target_roles, {}) + if len(mapping) < 20: + raise RuntimeError(f"{LIBRARY}: only {len(mapping)} mapped bones") + + yaw = facing_correction(src_arm, src_roles, target_arm, target_roles) + src_h = ( + src_arm.matrix_world + @ src_arm.data.bones[src_roles.hips].matrix_local + ).translation.z + target_h = ( + target_arm.matrix_world + @ target_arm.data.bones[target_roles.hips].matrix_local + ).translation.z + scale = target_h / src_h if src_h > 1e-5 else 1.0 + + by_name = {action.name.split(".")[0]: action for action in new_actions} + for clip in CLIPS: + source = by_name.get(clip) + if source is None: + raise RuntimeError(f"{LIBRARY}: missing {clip}") + baked = retarget_action( + target_arm, + target_roles, + src_arm, + src_roles, + mapping, + source, + clip, + yaw, + scale, + ) + add_clip_track(target_arm, baked, clip) + + for obj in new_objects: + bpy.data.objects.remove(obj, do_unlink=True) + for action in new_actions: + if action.users == 0: + bpy.data.actions.remove(action) + assign_action(target_arm, None) + + os.makedirs(os.path.dirname(os.path.abspath(OUTPUT)), exist_ok=True) + bpy.ops.export_scene.gltf( + filepath=OUTPUT, + export_format="GLB", + export_yup=True, + export_apply=False, + export_skins=True, + export_animations=True, + export_animation_mode="NLA_TRACKS", + export_bake_animation=False, + export_optimize_animation_size=True, + export_optimize_animation_keep_anim_armature=False, + ) + print( + f"Injected {', '.join(CLIPS)} into {OUTPUT}; " + f"{len(bpy.data.actions)} total actions" + ) + + +main() diff --git a/tools/levels.py b/tools/levels.py new file mode 100644 index 0000000..eaa168c --- /dev/null +++ b/tools/levels.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +"""Tone statistics for captured frames. + +"It looks flat" is not actionable. "42% of the frame is one shade and the +adjacent-pixel detail is 1.2/255" is — you can watch that number move when you +add a panel-line law, and you can tell the difference between a change that +helped and a change you merely believe helped. + +Run after any capture tool that writes PNGs: + + python tools/levels.py ... [--inset 12] [--gate] + +Columns + mean p1 p50 p99 luma percentiles, 0-255. p99 is the practical white point; + if it sits at 255 the frame is clipping, if it sits at 140 + the frame never reaches white and reads washed out. + clip% pixels >= 250. Blown highlights. Our tonemap history: a + LINEAR tonemap clipped lit whites and the glow pass bloomed + the clip, so every facade glowed like porcelain. This is + the number that catches that happening again. + black% pixels <= 7. Crushed shadows, no detail recoverable. + spread p99 - p1. Total tonal range in use. + dead% share of the frame held by the single most common luma. + A featureless sky filling the top half shows up here. + detail mean |luma difference| between horizontally adjacent + pixels. THE flatness metric: untextured flat-shaded planes + score near zero, surface detail (seams, wear, grain) moves + it up. Cel art should be low but not dead. + sat / satp99 mean and 99th-percentile HSV saturation. Guards the other + direction: we shifted authored hues once by grading + saturation to 1.3 and ginger hair rendered fire-truck red. + +The HUD is bright, saturated and pinned to the frame edges, so it drags every +statistic toward itself. --inset trims that percentage off each edge, which is +what you want whenever you are measuring the 3D render rather than the screen. +""" + +import argparse +import os +import sys + +try: + from PIL import Image, ImageChops +except ImportError: + sys.exit("levels.py needs Pillow: python -m pip install pillow") + + +# --gate thresholds. These are not taste — they are the failure modes this +# project has actually shipped and had to walk back, written down as numbers so +# the next one gets caught by a script instead of by the user's eye. +GATES = { + "clip": (2.0, "highlights blowing out — the glow pass will bloom the clip"), + "black": (35.0, "shadows crushed to pure black, no form left in them"), + # Cel art deliberately carries broad flat fills. These limits are tuned to + # the deterministic probe: the old clear-colour sky hit 28–35% dead and + # ~0.34 detail, while the authored sky + surface laws stay under 40% dead + # and above 0.37 detail. Real city shots carry substantially more detail. + "dead": (40.0, "nearly half the frame is one flat shade"), + "detail": (0.37, "surface detail at the old greybox baseline"), + "satp99": (250.0, "saturation railed; authored hues are being shifted"), +} + + +def _percentile(hist, total, frac): + """Value at `frac` of the population, from a 256-bin histogram.""" + target = total * frac + run = 0 + for value, count in enumerate(hist): + run += count + if run >= target: + return value + return 255 + + +def measure(path, inset_pct=0): + img = Image.open(path).convert("RGB") + if inset_pct: + w, h = img.size + dx, dy = int(w * inset_pct / 100.0), int(h * inset_pct / 100.0) + img = img.crop((dx, dy, w - dx, h - dy)) + + luma = img.convert("L") + hist = luma.histogram() + total = sum(hist) + + # Adjacent-pixel difference: the image against itself shifted one pixel + # left. Done with ImageChops so the per-pixel work stays in C — a Python + # loop over a 1280x720 frame is a second per metric per shot. + w, h = luma.size + shifted = ImageChops.difference(luma.crop((1, 0, w, h)), luma.crop((0, 0, w - 1, h))) + dhist = shifted.histogram() + dtotal = sum(dhist) + detail = sum(v * c for v, c in enumerate(dhist)) / max(dtotal, 1) + + sat = img.convert("HSV").getchannel(1) + shist = sat.histogram() + stotal = sum(shist) + + p1 = _percentile(hist, total, 0.01) + p99 = _percentile(hist, total, 0.99) + + return { + "shot": os.path.basename(path), + "mean": sum(v * c for v, c in enumerate(hist)) / max(total, 1), + "p1": p1, + "p50": _percentile(hist, total, 0.50), + "p99": p99, + "clip": 100.0 * sum(hist[250:]) / max(total, 1), + "black": 100.0 * sum(hist[:8]) / max(total, 1), + "spread": p99 - p1, + "dead": 100.0 * max(hist) / max(total, 1), + "detail": detail, + "sat": sum(v * c for v, c in enumerate(shist)) / max(stotal, 1), + "satp99": _percentile(shist, stotal, 0.99), + } + + +HEADER = "{:<22} {:>6} {:>4} {:>4} {:>4} {:>6} {:>7} {:>7} {:>6} {:>7} {:>6} {:>7}" +ROW = "{shot:<22} {mean:>6.1f} {p1:>4.0f} {p50:>4.0f} {p99:>4.0f} {clip:>6.2f} {black:>7.2f} {spread:>7.0f} {dead:>6.2f} {detail:>7.2f} {sat:>6.1f} {satp99:>7.0f}" +COLS = ("shot", "mean", "p1", "p50", "p99", "clip%", "black%", "spread", "dead%", "detail", "sat", "satp99") + + +def collect(targets): + paths = [] + for t in targets: + if os.path.isdir(t): + paths += [os.path.join(t, f) for f in sorted(os.listdir(t)) + if f.lower().endswith(".png")] + else: + paths.append(t) + return paths + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("targets", nargs="+", help="PNG files or directories of them") + ap.add_argument("--inset", type=float, default=0, + help="trim this %% off each edge before measuring (HUD lives there)") + ap.add_argument("--gate", action="store_true", + help="exit non-zero and name the frames that trip a threshold") + args = ap.parse_args() + + paths = collect(args.targets) + if not paths: + sys.exit("levels.py: no PNGs found in %s" % ", ".join(args.targets)) + + print(HEADER.format(*COLS)) + rows = [measure(p, args.inset) for p in paths] + for r in rows: + print(ROW.format(**r)) + + if len(rows) > 1: + avg = {k: sum(r[k] for r in rows) / len(rows) + for k in rows[0] if k != "shot"} + avg["shot"] = "AVERAGE (%d)" % len(rows) + print("-" * 100) + print(ROW.format(**avg)) + + if not args.gate: + return 0 + + failures = [] + for r in rows: + for key, (limit, why) in GATES.items(): + # detail is a floor, everything else is a ceiling. + bad = r[key] < limit if key == "detail" else r[key] > limit + if bad: + failures.append("%s: %s=%.2f (%s %.2f) — %s" + % (r["shot"], key, r[key], + "below" if key == "detail" else "over", limit, why)) + print() + if failures: + print("GATE FAILED") + for f in failures: + print(" " + f) + return 1 + print("GATE PASSED — %d frames within thresholds" % len(rows)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/merge_animations.py b/tools/merge_animations.py index 8256705..54f5e31 100644 --- a/tools/merge_animations.py +++ b/tools/merge_animations.py @@ -8,11 +8,11 @@ animations use the game's canonical clip names (Idle, Walk, Run, Jump, ...). Animation library — two supported layouts in : - A. `_library.glb` — a single multi-clip library (e.g. Quaternius Universal - Animation Library, CC0, committed at assets/characters/animations/). - Clip names map through LIBRARY_CLIP_MAP below. The character must be - rigged on this library's own skeleton (tools/autorig.py does this - automatically when _library.glb is present). + A. `_library.glb` and optional `_library_v2.glb` — multi-clip libraries + (Quaternius Universal Animation Libraries 1 and 2, CC0, committed at + assets/characters/animations/). Clip names map through the tables below. + The two editions use the same rest skeleton and proportions but different + bone names; V2_BONE_MAP describes that exact rename. B. Loose FBX/GLB files, ONE clip per file, all on the Mixamo skeleton (manual Mixamo downloads). Filenames map to canonical clip names: @@ -28,8 +28,16 @@ Locomotion clips are exported in place (hips horizontal motion stripped) unless --keep-root-motion is given, since the gameplay code moves the body. """ import bpy +import math import sys import os +from mathutils import Matrix, Quaternion, Vector + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from rig_map import RigRoles, build_map +from retarget_pose import ( + authored_world_rotation, build_segment_pairs, heading_inverse +) argv = sys.argv argv = argv[argv.index("--") + 1:] if "--" in argv else [] @@ -44,6 +52,7 @@ OUTPUT = argv[2] STRIP_ROOT_MOTION = "--keep-root-motion" not in argv MIX = "mixamorig:" +UP = Vector((0.0, 0.0, 1.0)) # Quaternius Universal Animation Library clip -> game canonical clip name. # Unmapped clips are skipped to keep character GLBs small. @@ -57,16 +66,122 @@ LIBRARY_CLIP_MAP = { "Jump_Land": "Land", "Crouch_Idle_Loop": "CrouchIdle", "Crouch_Fwd_Loop": "CrouchWalk", - "Roll": "Dash", + "Roll": "Roll", "Death01": "Death", "Hit_Chest": "Hit", "Dance_Loop": "Dance", # Movement-shooter extras: - "Swim_Fwd_Loop": "Grapple", # superman reach reads as a swing pose "Pistol_Idle_Loop": "PistolIdle", # armed idle (weapon actually held up) "Pistol_Shoot": "PistolShoot", "Pistol_Reload": "PistolReload", - "Sword_Attack": "Throw", # overhead swing reads as a grenade throw + "Sword_Attack": "SwordAttack", +} + +# Quaternius Universal Animation Library 2 Standard adds authored traversal +# clips that the first library does not contain. Keep entry/loop/exit clips +# distinct: runtime code can now play the motion the animator authored instead +# of manufacturing a pose by rotating a few bones. +LIBRARY_V2_CLIP_MAP = { + "Slide_Start": "SlideStart", + "Slide_Loop": "Slide", + "Slide_Exit": "SlideExit", + "NinjaJump_Start": "WallRunStart", + "NinjaJump_Land": "WallRunExit", + "Idle_Rail_Loop": "WallCling", + "ClimbUp_1m": "WallClimb", + "Shield_Dash": "Dash", + "Sword_Dash": "SwordDash", + "OverhandThrow": "Throw", + "Hit_Knockback": "Knockback", + "Chest_Open": "EmoteStretch", + "Idle_Rail_Call": "EmoteCall", + "Yes": "EmoteYes", + "Idle_No_Loop": "EmoteNo", +} + +# Four authored combat-strafe cycles from the Godot TPS Demo. These are kept as +# a tiny animation-only GLB; see _directional_LICENSE.md. They give the runtime +# a real backward and lateral stride instead of yawing a forward run's hips. +DIRECTIONAL_CLIP_MAP = { + "strafe_front-cycle": "StrafeWalkForward", + "strafe_back-cycle": "StrafeWalkBackward", +} + +MESH2MOTION_CLIP_MAP = { + "Strafe_left": [("StrafeWalkLeft", 0.0), ("RunLeft", 0.0)], + "Strafe_right": [("StrafeWalkRight", 0.0), ("RunRight", 0.0)], + "Run_Anime": [("RunForward", 0.0)], + # Authored held flight/reach with only subtle foot motion. + "Flying Forward Super": [("Grapple", 0.0)], +} + +WALLRUN_CLIP_MAP = { + "WallRunLeft": "WallRunLeft", + "WallRunRight": "WallRunRight", +} + +CMU_CLIP_MAP = { + "RunBackward": "RunBackward", +} + +# UAL2 renamed the original Quaternius/Rigify bones to Unreal-style names, but +# their rest transforms are byte-for-byte equivalent (verified in Blender). +# The map is target/game bone -> UAL2 source bone. +V2_BONE_MAP = { + "root": "root", + "DEF-hips": "pelvis", + "DEF-spine.001": "spine_01", + "DEF-spine.002": "spine_02", + "DEF-spine.003": "spine_03", + "DEF-neck": "neck_01", + "DEF-head": "Head", + "DEF-shoulder.L": "clavicle_l", + "DEF-upper_arm.L": "upperarm_l", + "DEF-forearm.L": "lowerarm_l", + "DEF-hand.L": "hand_l", + "DEF-shoulder.R": "clavicle_r", + "DEF-upper_arm.R": "upperarm_r", + "DEF-forearm.R": "lowerarm_r", + "DEF-hand.R": "hand_r", + "DEF-thigh.L": "thigh_l", + "DEF-shin.L": "calf_l", + "DEF-foot.L": "foot_l", + "DEF-toe.L": "ball_l", + "DEF-thigh.R": "thigh_r", + "DEF-shin.R": "calf_r", + "DEF-foot.R": "foot_r", + "DEF-toe.R": "ball_r", +} +for _side, _suffix in (("L", "l"), ("R", "r")): + for _game_digit, _source_digit in ( + ("f_index", "index"), + ("f_middle", "middle"), + ("f_ring", "ring"), + ("f_pinky", "pinky"), + ("thumb", "thumb"), + ): + for _joint in range(1, 4): + V2_BONE_MAP[f"DEF-{_game_digit}.{_joint:02d}.{_side}"] = ( + f"{_source_digit}_{_joint:02d}_{_suffix}" + ) + +# The TPS robot's lower-body rig is humanoid and its leg rest axes are within a +# few degrees of the Quaternius rig. Only the authored lower-body motion is +# transferred; the project's weapon layer keeps ownership of the upper body. +DIRECTIONAL_BONE_MAP = { + "root": "root", + "DEF-hips": "hips", + "DEF-spine.001": "spine1", + "DEF-spine.002": "spine2", + "DEF-spine.003": "chest", + "DEF-thigh.L": "thigh.L", + "DEF-shin.L": "shin.L", + "DEF-foot.L": "foot.L", + "DEF-toe.L": "toe.L", + "DEF-thigh.R": "thigh.R", + "DEF-shin.R": "shin.R", + "DEF-foot.R": "foot.R", + "DEF-toe.R": "toe.R", } @@ -144,7 +259,7 @@ def normalize_action_paths(action): def hips_height(arm): for b in arm.data.bones: - if b.name.lower().endswith("hips"): + if b.name.lower().endswith("hips") or b.name.lower() == "pelvis": return (arm.matrix_world @ b.head_local).z return 1.0 @@ -185,14 +300,18 @@ def main(): print("ERROR: character has no armature — run tools/autorig.py first") sys.exit(1) - # Normalize character bone names too. - for bone in target_arm.data.bones: - fixed = normalize_prefix(bone.name) - if fixed != bone.name: - bone.name = fixed + # Preserve the character's authored bone names. The rig sidecar is the + # runtime contract for IK, aim and cloth, so silently renaming Hips/Head + # here would invalidate otherwise-correct metadata on artist rigs. Role + # mapping makes a shared naming convention unnecessary. target_hips = hips_height(target_arm) print(f"Character hips height: {target_hips:.3f}") + target_roles = RigRoles(target_arm) + missing = target_roles.missing_core() + if missing: + print(f"ERROR: could not identify target rig roles: {missing}") + sys.exit(1) if not target_arm.animation_data: target_arm.animation_data_create() @@ -201,11 +320,29 @@ def main(): for track in list(target_arm.animation_data.nla_tracks): target_arm.animation_data.nla_tracks.remove(track) target_arm.animation_data.action = None + for action in list(bpy.data.actions): + if action.users == 0: + bpy.data.actions.remove(action) - library_path = os.path.join(ANIM_DIR, "_library.glb") - if os.path.exists(library_path): - merged = merge_from_library(target_arm, target_hips, library_path) - else: + library_specs = ( + ("_library.glb", LIBRARY_CLIP_MAP), + ("_library_v2.glb", LIBRARY_V2_CLIP_MAP), + ("_directional.glb", DIRECTIONAL_CLIP_MAP), + ("_mesh2motion.glb", MESH2MOTION_CLIP_MAP), + ("_cmu_locomotion.glb", CMU_CLIP_MAP), + ("_wallrun.glb", WALLRUN_CLIP_MAP), + ) + merged = 0 + found_library = False + for filename, clip_map in library_specs: + library_path = os.path.join(ANIM_DIR, filename) + if not os.path.exists(library_path): + continue + found_library = True + merged += merge_from_library( + target_arm, target_roles, library_path, clip_map + ) + if not found_library: merged = merge_from_files(target_arm, target_hips) if merged == 0: @@ -221,7 +358,12 @@ def main(): export_animations=True, export_animation_mode="NLA_TRACKS", export_skins=True, - export_bake_animation=True, + # Only mapped body bones are keyed. Baking here would add frozen + # rest-pose tracks to every hair/skirt bone and steal them from the + # runtime spring solver. + export_bake_animation=False, + export_optimize_animation_size=True, + export_optimize_animation_keep_anim_armature=False, ) print("Done.") @@ -249,53 +391,201 @@ def _assign_action(obj, action): pass -def retarget_action(target_arm, src_arm, src_action, clip_name): - """Retarget one source clip onto the character rig via world-space - constraint baking. +def _world_rest(arm): + return { + bone.name: arm.matrix_world @ bone.matrix_local + for bone in arm.data.bones + } - The character rig's arms were fitted to the mesh's A-pose (autorig.py), so - its rest differs from the library's T-pose. Copying each bone's WORLD - orientation (not its local channel) reproduces the library's actual motion - regardless of that rest difference, then baking captures it as a clean - action on the character rig. + +def _bone_order(arm): + out = [] + + def walk(bone): + out.append(bone.name) + for child in bone.children: + walk(child) + + for bone in arm.data.bones: + if bone.parent is None: + walk(bone) + return out + + +def _rig_forward(arm, roles): + acc = Vector((0.0, 0.0, 0.0)) + for side in ("L", "R"): + for role in ("toe", "foot"): + name = roles.limb.get((role, side)) + if not name: + continue + bone = arm.data.bones[name] + direction = ( + (arm.matrix_world @ bone.tail_local) + - (arm.matrix_world @ bone.matrix_local.translation) + ) + direction.z = 0.0 + if direction.length > 1e-5: + acc += direction.normalized() + break + return acc.normalized() if acc.length > 1e-5 else None + + +def _facing_correction(src_arm, src_roles, target_arm, target_roles): + source = _rig_forward(src_arm, src_roles) + target = _rig_forward(target_arm, target_roles) + if source is None or target is None: + return Quaternion() + angle = Vector((source.x, source.y)).angle_signed( + Vector((target.x, target.y)), 0.0 + ) + return Quaternion(UP, angle) if abs(angle) >= 1e-4 else Quaternion() + + +def _solve_pose(arm, order, rest_world, desired_rot, hips, hips_head): + pose_world = {} + basis = {} + bones = arm.data.bones + for name in order: + bone = bones[name] + rest = rest_world[name] + if bone.parent is not None: + parent = bone.parent.name + matrix = ( + pose_world[parent] + @ rest_world[parent].inverted() + @ rest + ) + else: + matrix = rest + rotation = Quaternion() + if name in desired_rot: + rotation = matrix.to_quaternion().inverted() @ desired_rot[name] + location = Vector((0.0, 0.0, 0.0)) + if name == hips and hips_head is not None: + location = matrix.inverted() @ hips_head + basis[name] = (location, rotation) + pose_world[name] = ( + matrix + @ Matrix.Translation(location) + @ rotation.to_matrix().to_4x4() + ) + return basis + + +def retarget_action(target_arm, target_roles, src_arm, src_roles, mapping, + src_action, clip_name, yaw, scale, + lower_body_yaw_degrees=0.0): + """Bake an authored motion onto any recognized humanoid rig. + + Major joint-to-joint directions come from the source performance while the + target keeps its own bone roll. This prevents a crossed/T-pose modelling + rest from surviving into every clip without twisting artist rigs. """ + src_rest = _world_rest(src_arm) + target_rest = _world_rest(target_arm) + target_order = _bone_order(target_arm) + src_rest_rot = { + name: matrix.to_quaternion() for name, matrix in src_rest.items() + } + target_rest_rot = { + name: matrix.to_quaternion() for name, matrix in target_rest.items() + } + yaw_inverse = yaw.inverted() + segment_pairs = build_segment_pairs(mapping, src_roles, target_roles) + src_hips = src_roles.hips + target_hips = target_roles.hips + src_hips_rest = src_rest[src_hips].translation.copy() + target_hips_rest = target_rest[target_hips].translation.copy() + _assign_action(src_arm, src_action) - fr0 = int(src_action.frame_range[0]) - fr1 = int(src_action.frame_range[1]) + frame_start, frame_end = ( + int(round(value)) for value in src_action.frame_range + ) + source_heading = Quaternion() + if clip_name == "RunBackward": + bpy.context.scene.frame_set(frame_start) + source_eval = src_arm.evaluated_get( + bpy.context.evaluated_depsgraph_get() + ) + source_heading = heading_inverse( + ( + source_eval.matrix_world + @ source_eval.pose.bones[src_hips].matrix + ).to_quaternion(), + src_rest_rot[src_hips], + UP, + ) + baked = bpy.data.actions.new(clip_name) + _assign_action(target_arm, baked) + for pose_bone in target_arm.pose.bones: + pose_bone.rotation_mode = "QUATERNION" - for pb in target_arm.pose.bones: - if pb.name not in src_arm.pose.bones: - continue - cr = pb.constraints.new("COPY_ROTATION") - cr.target = src_arm - cr.subtarget = pb.name - cr.target_space = "WORLD" - cr.owner_space = "WORLD" - if pb.name.lower().endswith("hips"): - cl = pb.constraints.new("COPY_LOCATION") - cl.target = src_arm - cl.subtarget = pb.name - cl.target_space = "WORLD" - cl.owner_space = "WORLD" + scene = bpy.context.scene + for frame in range(frame_start, frame_end + 1): + scene.frame_set(frame) + depsgraph = bpy.context.evaluated_depsgraph_get() + src_eval = src_arm.evaluated_get(depsgraph) + source_world = src_eval.matrix_world + desired = {} + for target_name, source_name in mapping.items(): + if ( + source_name not in src_eval.pose.bones + or target_name not in target_rest_rot + ): + continue + target_rotation = authored_world_rotation( + target_name, + source_name, + src_eval, + source_world, + src_rest_rot, + target_rest, + target_rest_rot, + yaw, + yaw_inverse, + segment_pairs, + source_heading, + ) + desired[target_name] = target_rotation - bpy.ops.object.select_all(action="DESELECT") - target_arm.select_set(True) - bpy.context.view_layer.objects.active = target_arm - if target_arm.animation_data: - target_arm.animation_data.action = None + hips_head = None + if src_hips in src_eval.pose.bones: + moved = ( + source_world @ src_eval.pose.bones[src_hips].matrix + ).translation + displacement = yaw @ (source_heading @ ( + (moved - src_hips_rest) * scale + )) + if STRIP_ROOT_MOTION: + displacement.x = 0.0 + displacement.y = 0.0 + hips_head = target_hips_rest + displacement - bpy.ops.nla.bake(frame_start=fr0, frame_end=fr1, step=1, - only_selected=False, visual_keying=True, - clear_constraints=True, clear_parents=False, - use_current_action=True, bake_types={"POSE"}) + basis = _solve_pose( + target_arm, + target_order, + target_rest, + desired, + target_hips, + hips_head, + ) + for name in mapping: + if name not in basis: + continue + pose_bone = target_arm.pose.bones[name] + pose_bone.rotation_quaternion = basis[name][1] + pose_bone.keyframe_insert("rotation_quaternion", frame=frame) + if hips_head is not None: + pose_bone = target_arm.pose.bones[target_hips] + pose_bone.location = basis[target_hips][0] + pose_bone.keyframe_insert("location", frame=frame) - baked = target_arm.animation_data.action - if STRIP_ROOT_MOTION: - strip_hips_horizontal(baked) + _assign_action(target_arm, None) return baked -def merge_from_library(target_arm, target_hips, library_path): +def merge_from_library(target_arm, target_roles, library_path, clip_map): """Retarget every mapped clip from a single multi-clip library GLB onto the character rig (whose arm rest was fitted to the mesh).""" print(f"Using multi-clip library: {library_path}") @@ -308,31 +598,60 @@ def merge_from_library(target_arm, target_hips, library_path): if not src_arm: print("ERROR: no armature in animation library") sys.exit(1) - - # Match the library rig's height to the character so hips bob / foot planting - # translate correctly under the world-space location copy. - src_hips = hips_height(src_arm) - if src_hips > 1e-4: - s = target_hips / src_hips - src_arm.scale = (s, s, s) - bpy.ops.object.select_all(action="DESELECT") - src_arm.select_set(True) - bpy.context.view_layer.objects.active = src_arm - bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + src_roles = RigRoles(src_arm) + missing = src_roles.missing_core() + if missing: + print(f"ERROR: could not identify source rig roles: {missing}") + sys.exit(1) + mapping = build_map(src_roles, target_roles, {}) + if len(mapping) < 10: + print(f"ERROR: only {len(mapping)} role-mapped bones in {library_path}") + sys.exit(1) + yaw = _facing_correction(src_arm, src_roles, target_arm, target_roles) + src_height = ( + src_arm.matrix_world + @ src_arm.data.bones[src_roles.hips].matrix_local + ).translation.z + target_height = ( + target_arm.matrix_world + @ target_arm.data.bones[target_roles.hips].matrix_local + ).translation.z + scale = target_height / src_height if src_height > 1e-5 else 1.0 + print( + f"Role-mapped {len(mapping)} target bones; " + f"hips scale {scale:.3f}" + ) merged = 0 keep = [] for action in new_actions: base = action.name.split(".")[0] # tolerate .001 suffixes - clip_name = LIBRARY_CLIP_MAP.get(base) - if not clip_name: + mapped = clip_map.get(base) + if not mapped: continue - print(f"--- {base} -> '{clip_name}' (retarget bake)") - baked = retarget_action(target_arm, src_arm, action, clip_name) - target_arm.animation_data.action = None - _add_clip_track(target_arm, baked, clip_name) - keep.append(baked) - merged += 1 + specs = [(mapped, 0.0)] if isinstance(mapped, str) else mapped + for spec in specs: + clip_name, travel_yaw_degrees = spec[:2] + print( + f"--- {base} -> '{clip_name}' (retarget bake, " + f"lower-body heading {travel_yaw_degrees:+.0f} deg)" + ) + baked = retarget_action( + target_arm, + target_roles, + src_arm, + src_roles, + mapping, + action, + clip_name, + yaw, + scale, + travel_yaw_degrees, + ) + target_arm.animation_data.action = None + _add_clip_track(target_arm, baked, clip_name) + keep.append(baked) + merged += 1 for o in new_objects: bpy.data.objects.remove(o, do_unlink=True) diff --git a/tools/retarget.py b/tools/retarget.py index 69e535b..76e3d99 100644 --- a/tools/retarget.py +++ b/tools/retarget.py @@ -57,6 +57,9 @@ import gltf_fix import rig_map import surface_map from rig_map import RigRoles, build_map, is_cosmetic, is_segment_of, tokens +from retarget_pose import ( + authored_world_rotation, build_segment_pairs, heading_inverse +) argv = sys.argv argv = argv[argv.index("--") + 1:] if "--" in argv else [] @@ -86,15 +89,64 @@ LIBRARY_CLIP_MAP = { "Jump_Land": "Land", "Crouch_Idle_Loop": "CrouchIdle", "Crouch_Fwd_Loop": "CrouchWalk", - "Roll": "Dash", + "Roll": "Roll", "Death01": "Death", "Hit_Chest": "Hit", "Dance_Loop": "Dance", - "Swim_Fwd_Loop": "Grapple", "Pistol_Idle_Loop": "PistolIdle", "Pistol_Shoot": "PistolShoot", "Pistol_Reload": "PistolReload", - "Sword_Attack": "Throw", + "Sword_Attack": "SwordAttack", +} + +LIBRARY_V2_CLIP_MAP = { + "Slide_Start": "SlideStart", + "Slide_Loop": "Slide", + "Slide_Exit": "SlideExit", + "NinjaJump_Start": "WallRunStart", + "NinjaJump_Land": "WallRunExit", + "Idle_Rail_Loop": "WallCling", + "ClimbUp_1m": "WallClimb", + "Shield_Dash": "Dash", + "Sword_Dash": "SwordDash", + "OverhandThrow": "Throw", + "Hit_Knockback": "Knockback", + "Chest_Open": "EmoteStretch", + "Idle_Rail_Call": "EmoteCall", + "Yes": "EmoteYes", + "Idle_No_Loop": "EmoteNo", +} + +# The TPS demo set is a combat *walk*, not a run. It used to occupy the +# high-speed Run* slots, which made the runtime accelerate a roughly 1.8 m/s +# step cycle to six times normal playback at the game's 11 m/s ground speed. +# Keep the useful authored front/back steps, but label them honestly. +DIRECTIONAL_CLIP_MAP = { + "strafe_front-cycle": "StrafeWalkForward", + "strafe_back-cycle": "StrafeWalkBackward", +} + +# Mesh2Motion's CC0 human add-on library supplies authored forward and lateral +# actions. Never rotate only the legs to manufacture another direction: the +# captured CMU clip below owns backward travel as one coherent full-body move. +MESH2MOTION_CLIP_MAP = { + "Strafe_left": [("StrafeWalkLeft", 0.0), ("RunLeft", 0.0)], + "Strafe_right": [("StrafeWalkRight", 0.0), ("RunRight", 0.0)], + "Run_Anime": [("RunForward", 0.0)], + # A subtle held superhero-flight motion: the leading hands stay extended + # while the feet move less than two degrees across the runtime audit. + "Flying Forward Super": [("Grapple", 0.0)], +} + +# Uisco's authored UE4 Mannequin wall-run pair. These are real lateral +# wall-running performances, not a forward sprint relabelled as traversal. +WALLRUN_CLIP_MAP = { + "WallRunLeft": "WallRunLeft", + "WallRunRight": "WallRunRight", +} + +CMU_CLIP_MAP = { + "RunBackward": "RunBackward", } UP = Vector((0.0, 0.0, 1.0)) @@ -604,13 +656,14 @@ def solve_pose(arm, order, rest_w, desired_rot, hips, hips_head): def retarget_clip(src_arm, src_roles, tgt_arm, tgt_roles, mapping, action, - clip_name, yaw, scale): + clip_name, yaw, scale, lower_body_yaw_degrees=0.0): src_rest = world_rest(src_arm) tgt_rest = world_rest(tgt_arm) order = bone_order(tgt_arm) src_rest_rot = {n: m.to_quaternion() for n, m in src_rest.items()} tgt_rest_rot = {n: m.to_quaternion() for n, m in tgt_rest.items()} yaw_inv = yaw.inverted() + segment_pairs = build_segment_pairs(mapping, src_roles, tgt_roles) src_hips = src_roles.hips tgt_hips = tgt_roles.hips @@ -619,6 +672,20 @@ def retarget_clip(src_arm, src_roles, tgt_arm, tgt_roles, mapping, action, assign_action(src_arm, action) f0, f1 = (int(round(v)) for v in action.frame_range) + source_heading = Quaternion() + if clip_name == "RunBackward": + bpy.context.scene.frame_set(f0) + source_eval = src_arm.evaluated_get( + bpy.context.evaluated_depsgraph_get() + ) + source_heading = heading_inverse( + ( + source_eval.matrix_world + @ source_eval.pose.bones[src_hips].matrix + ).to_quaternion(), + src_rest_rot[src_hips], + UP, + ) baked = bpy.data.actions.new(clip_name) assign_action(tgt_arm, baked) @@ -636,14 +703,27 @@ def retarget_clip(src_arm, src_roles, tgt_arm, tgt_roles, mapping, action, for tgt_name, src_name in mapping.items(): if src_name not in src_eval.pose.bones or tgt_name not in tgt_rest_rot: continue - pose_rot = (smw @ src_eval.pose.bones[src_name].matrix).to_quaternion() - delta = pose_rot @ src_rest_rot[src_name].inverted() - desired[tgt_name] = (yaw @ delta @ yaw_inv) @ tgt_rest_rot[tgt_name] + target_rotation = authored_world_rotation( + tgt_name, + src_name, + src_eval, + smw, + src_rest_rot, + tgt_rest, + tgt_rest_rot, + yaw, + yaw_inv, + segment_pairs, + source_heading, + ) + desired[tgt_name] = target_rotation hips_head = None if src_hips in src_eval.pose.bones: moved = (smw @ src_eval.pose.bones[src_hips].matrix).translation - d = yaw @ ((moved - src_hips_rest) * scale) + d = yaw @ (source_heading @ ( + (moved - src_hips_rest) * scale + )) if STRIP_ROOT_MOTION: d.x = 0.0 d.y = 0.0 # gameplay code moves the body; keep the vertical bob @@ -1162,31 +1242,7 @@ def main(): fix_unlit_materials(meshes) roles = RigRoles(arm) # rest positions moved; re-read - library = os.path.join(ANIM_DIR, "_library.glb") - if not os.path.exists(library): - print(f"ERROR: animation library not found: {library}") - sys.exit(1) - before = set(bpy.data.objects) - before_actions = set(bpy.data.actions) - bpy.ops.import_scene.gltf(filepath=library) - new_objects = [o for o in bpy.data.objects if o not in before] - src_arm = next((o for o in new_objects if o.type == "ARMATURE"), None) - if not src_arm: - print("ERROR: no armature in the animation library") - sys.exit(1) - src_roles = RigRoles(src_arm) - - mapping = build_map(src_roles, roles, OVERRIDES) - print("\nLibrary rig:\n" + src_roles.describe()) print("\nCharacter rig:\n" + roles.describe()) - print(f"\nDriving {len(mapping)} of {len(arm.data.bones)} bones from the library; " - f"{len(arm.data.bones) - len(mapping)} left free for secondary motion.") - - yaw = facing_correction(src_arm, src_roles, arm, roles) - src_h = (src_arm.matrix_world @ src_arm.data.bones[src_roles.hips].matrix_local).translation.z - tgt_h = (arm.matrix_world @ arm.data.bones[roles.hips].matrix_local).translation.z - scale = tgt_h / src_h if src_h > 1e-5 else 1.0 - print(f"Hips height: library {src_h:.3f} m, character {tgt_h:.3f} m (scale {scale:.3f})") if not arm.animation_data: arm.animation_data_create() @@ -1195,16 +1251,83 @@ def main(): merged = 0 kept_clips = set() - for action in [a for a in bpy.data.actions if a not in before_actions]: - clip = LIBRARY_CLIP_MAP.get(action.name.split(".")[0]) - if not clip: + all_library_objects = [] + primary_mapping = {} + library_specs = ( + ("_library.glb", LIBRARY_CLIP_MAP), + ("_library_v2.glb", LIBRARY_V2_CLIP_MAP), + ("_directional.glb", DIRECTIONAL_CLIP_MAP), + ("_mesh2motion.glb", MESH2MOTION_CLIP_MAP), + ("_cmu_locomotion.glb", CMU_CLIP_MAP), + ("_wallrun.glb", WALLRUN_CLIP_MAP), + ) + found_library = False + for filename, clip_map in library_specs: + library = os.path.join(ANIM_DIR, filename) + if not os.path.exists(library): continue - print(f" {action.name} -> {clip}") - baked = retarget_clip(src_arm, src_roles, arm, roles, mapping, action, - clip, yaw, scale) - add_nla_clip(arm, baked, clip) - kept_clips.add(clip) - merged += 1 + found_library = True + before = set(bpy.data.objects) + before_actions = set(bpy.data.actions) + bpy.ops.import_scene.gltf(filepath=library) + new_objects = [o for o in bpy.data.objects if o not in before] + all_library_objects.extend(new_objects) + src_arm = next((o for o in new_objects if o.type == "ARMATURE"), None) + if not src_arm: + print(f"ERROR: no armature in animation library {filename}") + sys.exit(1) + src_roles = RigRoles(src_arm) + mapping = build_map(src_roles, roles, OVERRIDES) + if not primary_mapping: + primary_mapping = mapping + print(f"\n{filename} rig:\n" + src_roles.describe()) + print(f"Driving {len(mapping)} of {len(arm.data.bones)} bones; " + f"{len(arm.data.bones) - len(mapping)} left free.") + + yaw = facing_correction(src_arm, src_roles, arm, roles) + src_h = ( + src_arm.matrix_world + @ src_arm.data.bones[src_roles.hips].matrix_local + ).translation.z + tgt_h = ( + arm.matrix_world @ arm.data.bones[roles.hips].matrix_local + ).translation.z + scale = tgt_h / src_h if src_h > 1e-5 else 1.0 + print( + f"Hips height: library {src_h:.3f} m, " + f"character {tgt_h:.3f} m (scale {scale:.3f})" + ) + + for action in [a for a in bpy.data.actions if a not in before_actions]: + mapped = clip_map.get(action.name.split(".")[0]) + if not mapped: + continue + specs = [(mapped, 0.0)] if isinstance(mapped, str) else mapped + for spec in specs: + clip, travel_yaw_degrees = spec[:2] + suffix = ( + f" (lower-body heading {travel_yaw_degrees:+.0f} deg)" + if abs(travel_yaw_degrees) > 1e-4 else "" + ) + print(f" {action.name} -> {clip}{suffix}") + baked = retarget_clip( + src_arm, + src_roles, + arm, + roles, + mapping, + action, + clip, + yaw, + scale, + travel_yaw_degrees, + ) + add_nla_clip(arm, baked, clip) + kept_clips.add(clip) + merged += 1 + if not found_library: + print(f"ERROR: no animation libraries found in {ANIM_DIR}") + sys.exit(1) if merged == 0: print("ERROR: no clips retargeted") sys.exit(1) @@ -1241,12 +1364,12 @@ def main(): bpy.data.actions.remove(a) print(f"Kept {len(kept_clips)} clips; dropped everything else") - for o in new_objects: + for o in all_library_objects: bpy.data.objects.remove(o, do_unlink=True) os.makedirs(os.path.dirname(os.path.abspath(OUTPUT)), exist_ok=True) sidecar = os.path.splitext(OUTPUT)[0] + ".rig.json" - info = describe_rig(arm, roles, mapping, meshes) + info = describe_rig(arm, roles, primary_mapping, meshes) with open(sidecar, "w", encoding="utf-8") as f: json.dump(info, f, indent=2) print(f"Wrote {os.path.basename(sidecar)}: {len(info['chains'])} cloth chains, " diff --git a/tools/retarget_pose.py b/tools/retarget_pose.py new file mode 100644 index 0000000..4171a6e --- /dev/null +++ b/tools/retarget_pose.py @@ -0,0 +1,125 @@ +"""Silhouette-preserving helpers shared by the animation retargeters. + +The source animation owns the direction of each major body segment. Copying +only a bone's rest-relative rotation preserves a target rig's modelling pose; +characters authored with crossed or rearward arms then keep those directions +in every idle, dance, and locomotion clip. + +These helpers transfer animated joint-to-joint directions for the torso and +limbs while retaining the target bone's own roll. This is an offline bake of +the source performance, not a runtime procedural animation layer. +""" + +from mathutils import Quaternion, Vector + + +def build_segment_pairs(mapping, source_roles, target_roles): + """Return target bone -> (target child, source bone, source child).""" + chains = [ + [target_roles.hips] + list(target_roles.spine), + ] + for side in ("L", "R"): + chains.append([ + target_roles.limb.get((role, side)) + for role in ("shoulder", "upper_arm", "forearm", "hand") + ]) + chains.append([ + target_roles.limb.get((role, side)) + for role in ("thigh", "shin", "foot", "toe") + ]) + + pairs = {} + for chain in chains: + chain = [name for name in chain if name] + for index, target_name in enumerate(chain[:-1]): + source_name = mapping.get(target_name) + if not source_name: + continue + # Chain resampling can map neighbouring target spine bones to the + # same source joint. Look ahead to the first genuinely new joint. + for target_child in chain[index + 1:]: + source_child = mapping.get(target_child) + if source_child and source_child != source_name: + pairs[target_name] = ( + target_child, source_name, source_child + ) + break + return pairs + + +def _twist_about(rotation, axis): + """Extract the twist component of a world-space quaternion around axis.""" + axis = axis.normalized() + vector = Vector((rotation.x, rotation.y, rotation.z)) + projected = axis * vector.dot(axis) + twist = Quaternion((rotation.w, projected.x, projected.y, projected.z)) + if twist.magnitude < 1e-8: + return Quaternion() + twist.normalize() + return twist + + +def heading_inverse(pose_rotation, rest_rotation, up): + """World-up heading that removes a capture's initial stage orientation.""" + return _twist_about( + pose_rotation @ rest_rotation.inverted(), up + ).inverted() + + +def authored_world_rotation( + target_name, + source_name, + source_eval, + source_world, + source_rest_rot, + target_rest, + target_rest_rot, + yaw, + yaw_inverse, + segment_pairs, + source_heading=Quaternion(), +): + """Retarget one bone, using authored segment direction where available.""" + pose_rotation = source_heading @ ( + source_world @ source_eval.pose.bones[source_name].matrix + ).to_quaternion() + delta = pose_rotation @ source_rest_rot[source_name].inverted() + full_rotation = ( + yaw @ delta @ yaw_inverse + ) @ target_rest_rot[target_name] + + pair = segment_pairs.get(target_name) + if pair is None: + return full_rotation + target_child, pair_source, source_child = pair + if ( + pair_source not in source_eval.pose.bones + or source_child not in source_eval.pose.bones + or target_child not in target_rest + ): + return full_rotation + + source_head = ( + source_world @ source_eval.pose.bones[pair_source].matrix + ).translation + source_child_head = ( + source_world @ source_eval.pose.bones[source_child].matrix + ).translation + desired_direction = yaw @ ( + source_heading @ (source_child_head - source_head) + ) + target_direction = ( + target_rest[target_child].translation + - target_rest[target_name].translation + ) + if desired_direction.length < 1e-7 or target_direction.length < 1e-7: + return full_rotation + desired_direction.normalize() + target_direction.normalize() + + swing = target_direction.rotation_difference(desired_direction) + swung_rotation = swing @ target_rest_rot[target_name] + residual = full_rotation @ swung_rotation.inverted() + return ( + _twist_about(residual, desired_direction) @ swung_rotation + ).normalized() diff --git a/tools/verify_character.py b/tools/verify_character.py index 67e03c9..07672c5 100644 --- a/tools/verify_character.py +++ b/tools/verify_character.py @@ -253,7 +253,20 @@ check(len(orphans) <= 1, "every chain is attached to the body", # ----------------------------------------------------------------------- clips actions = {a.name: a for a in bpy.data.actions} print(f"\n {len(actions)} clips: {', '.join(sorted(actions))}\n") -check(len(actions) >= 10, "the canonical clip set shipped", f"{len(actions)} clips") +required_clips = { + "Idle", "Walk", "Run", "Sprint", "Jump", "Fall", "Land", + "CrouchIdle", "CrouchWalk", "Dash", "Slide", + "RunForward", "RunBackward", "RunLeft", "RunRight", + "StrafeWalkForward", "StrafeWalkBackward", + "StrafeWalkLeft", "StrafeWalkRight", + "WallRunLeft", "WallRunRight", "Grapple", +} +missing_clips = sorted(required_clips - actions.keys()) +check(not missing_clips, "the canonical clip set shipped", + f"missing: {', '.join(missing_clips)}" if missing_clips + else f"{len(actions)} clips") +check("WallRun" not in actions, "the placeholder run-cycle wall run was removed", + "legacy WallRun is still present" if "WallRun" in actions else "") def curves(action): @@ -305,7 +318,8 @@ else: # are upper-body clips with genuinely static legs, so demanding leg motion from # every clip fails on a correct build. LOCOMOTION = {"Idle", "Walk", "Run", "Sprint", "Jump", "Fall", "Land", - "CrouchIdle", "CrouchWalk", "Dash"} + "CrouchIdle", "CrouchWalk", "Dash", + "WallRunLeft", "WallRunRight"} # The thighs and shins by ROLE, so this works on any rig's spelling. leg_bones = {b for s in ROLE_LEGS.values() for b in s} @@ -314,10 +328,13 @@ if not leg_bones: if any(h in b.name.lower() for h in ("thigh", "shin"))} frozen = [] +held_rest = [] legless = [] keyed_cloth = set() +held_poses = {"Grapple"} for name, action in sorted(actions.items()): moved = defaultdict(float) + authored_offset = 0.0 for fc in curves(action): b = bone_of(fc.data_path) if not b or len(fc.keyframe_points) < 2: @@ -326,7 +343,19 @@ for name, action in sorted(actions.items()): keyed_cloth.add(b) vals = [kp.co.y for kp in fc.keyframe_points] moved[b] = max(moved[b], max(vals) - min(vals)) - if max(moved.values(), default=0.0) < 0.005: + # A held pose is intentionally almost constant. Quaternion XYZ and + # translation channels are zero in the rest pose, so a meaningful + # absolute value proves the clip contains an authored silhouette. + if (fc.data_path.endswith("rotation_quaternion") + and fc.array_index in (1, 2, 3)) \ + or fc.data_path.endswith("location"): + authored_offset = max( + authored_offset, max((abs(value) for value in vals), default=0.0) + ) + if name in held_poses: + if authored_offset < 0.03: + held_rest.append(f"{name}({authored_offset:.4f})") + elif max(moved.values(), default=0.0) < 0.005: frozen.append(name) legs = max((v for b, v in moved.items() if b in leg_bones), default=0.0) if name in LOCOMOTION and legs < 0.01: @@ -334,6 +363,8 @@ for name, action in sorted(actions.items()): check(not frozen, "no clip retargeted to a frozen rest pose", f"frozen: {', '.join(frozen)}" if frozen else "") +check(not held_rest, "held clips contain a non-rest authored pose", + f"rest-like: {', '.join(held_rest)}" if held_rest else "") check(not legless, "locomotion clips animate the legs", f"static legs: {', '.join(legless)}" if legless else "") check(not keyed_cloth, "cloth bones carry no animation keys", diff --git a/ui/main_menu/main_menu.gd b/ui/main_menu/main_menu.gd index 4e481e2..1fec818 100644 --- a/ui/main_menu/main_menu.gd +++ b/ui/main_menu/main_menu.gd @@ -645,7 +645,7 @@ func _build_skin_row(parent: Container) -> void: func _play_music() -> void: # Menu music is scene-owned, so it stops the moment a level loads. - var path := "res://assets/sounds/music_menu.ogg" + var path := "res://assets/sounds/music_menu_premium.ogg" if not ResourceLoader.exists(path): return var music := AudioStreamPlayer.new() @@ -653,9 +653,11 @@ func _play_music() -> void: if music.stream is AudioStreamOggVorbis: music.stream.loop = true music.bus = "Music" - music.volume_db = -8.0 - music.autoplay = true + music.volume_db = -36.0 add_child(music) + music.play() + create_tween().tween_property(music, "volume_db", -6.0, 1.4) \ + .set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) # ── Background ─────────────────────────────────────────────────────────────── diff --git a/weapons/base_hitscan_weapon.gd b/weapons/base_hitscan_weapon.gd index 5d9c7ef..e611ff5 100644 --- a/weapons/base_hitscan_weapon.gd +++ b/weapons/base_hitscan_weapon.gd @@ -42,6 +42,11 @@ func _ready() -> void: model_root = Node3D.new() add_child(model_root) _build_model() + if fire_sound: + AudioManager.configure_player(fire_sound, + AudioManager.weapon_sound_id(weapon_name)) + if reload_sound: + AudioManager.configure_player(reload_sound, "shotgun_reload") func _build_model() -> void: # Virtual method to be overridden @@ -115,8 +120,10 @@ func _fire() -> void: get_parent().add_recoil(recoil_amplitude, randf_range(-recoil_amplitude * 0.5, recoil_amplitude * 0.5)) if fire_sound: - # Slight pitch variation - fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05) + # The event family already randomizes its recorded variations. Keep the + # player-level shift tiny so sustained fire stays like one weapon, not + # a row of differently sized cartoon rifles. + fire_sound.pitch_scale = 1.0 + randf_range(-0.015, 0.015) fire_sound.play() if current_ammo == 0: diff --git a/weapons/base_projectile_weapon.gd b/weapons/base_projectile_weapon.gd index d325341..8b1e359 100644 --- a/weapons/base_projectile_weapon.gd +++ b/weapons/base_projectile_weapon.gd @@ -42,6 +42,11 @@ func _ready() -> void: model_root = Node3D.new() add_child(model_root) _build_model() + if fire_sound: + AudioManager.configure_player(fire_sound, + AudioManager.weapon_sound_id(weapon_name)) + if reload_sound: + AudioManager.configure_player(reload_sound, "shotgun_reload") func _build_model() -> void: pass @@ -112,7 +117,7 @@ func _fire() -> void: get_parent().add_recoil(recoil_amplitude, randf_range(-recoil_amplitude * 0.5, recoil_amplitude * 0.5)) if fire_sound: - fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05) + fire_sound.pitch_scale = 1.0 + randf_range(-0.015, 0.015) fire_sound.play() if current_ammo == 0: diff --git a/weapons/double_barrel_shotgun.gd b/weapons/double_barrel_shotgun.gd index 4e9526e..223930d 100644 --- a/weapons/double_barrel_shotgun.gd +++ b/weapons/double_barrel_shotgun.gd @@ -29,6 +29,8 @@ func _ready() -> void: set_process_input(true) set_process(true) _build_model() + AudioManager.configure_player(fire_sound, "shotgun_fire") + AudioManager.configure_player(reload_sound, "shotgun_reload") func _build_model() -> void: if not model_root: diff --git a/weapons/explosion_vfx.gd b/weapons/explosion_vfx.gd index c02ba8b..bfeff87 100644 --- a/weapons/explosion_vfx.gd +++ b/weapons/explosion_vfx.gd @@ -26,7 +26,8 @@ static func _flat_mat(color: Color, emissive: float = 0.0) -> StandardMaterial3D return m -static func spawn(scene_tree: SceneTree, pos: Vector3, radius: float) -> void: +static func spawn(scene_tree: SceneTree, pos: Vector3, radius: float, + pitch_scale: float = 1.0) -> void: var root := scene_tree.current_scene if root == null: return @@ -127,7 +128,8 @@ static func spawn(scene_tree: SceneTree, pos: Vector3, radius: float) -> void: # ── Audio: explosion samples with variation ─────────────────────────── var am = scene_tree.root.get_node_or_null("AudioManager") if am and am.has_sound("explosion"): - am.play_3d("explosion", pos) + var size_db := clampf((radius - 2.5) * 0.42, -2.5, 2.5) + am.play_3d("explosion", pos, size_db, clampf(pitch_scale, 0.72, 1.15)) ## Comic star-flash at a weapon muzzle: 4 flat spikes + hot core, parented to diff --git a/weapons/explosive_projectile.gd b/weapons/explosive_projectile.gd index f031e28..a244372 100644 --- a/weapons/explosive_projectile.gd +++ b/weapons/explosive_projectile.gd @@ -60,18 +60,13 @@ func destroy() -> void: func _setup_effects() -> void: # Audio flight_sound = AudioStreamPlayer3D.new() - flight_sound.bus = "SFX" - flight_sound.stream = load("res://assets/sounds/rocket_flight.wav") + AudioManager.configure_player(flight_sound, "rocket_flight") flight_sound.autoplay = true - if flight_sound.stream is AudioStreamWAV: - flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD - flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames # Localized: audible nearby, falls off with distance instead of droning # in every player's ear regardless of where the projectile is. flight_sound.unit_size = 6.0 flight_sound.max_db = 0.0 flight_sound.max_distance = 45.0 - flight_sound.volume_db = -8.0 flight_sound.add_to_group("acoustic_occluded") # muffles behind walls add_child(flight_sound) flight_sound.play() @@ -216,4 +211,4 @@ func _explode(pos: Vector3) -> void: owner_player.server_play_explosion.rpc_id(1, pos, explosion_radius) # Local visual + audio: shared cel-shaded burst (also used by remote replay) - ExplosionVFX.spawn(get_tree(), pos, explosion_radius) + ExplosionVFX.spawn(get_tree(), pos, explosion_radius, explosion_pitch) diff --git a/weapons/homing_projectile.gd b/weapons/homing_projectile.gd index 26114fe..5e6f495 100644 --- a/weapons/homing_projectile.gd +++ b/weapons/homing_projectile.gd @@ -19,16 +19,11 @@ func _start_homing_effects() -> void: # Audio flight_sound = AudioStreamPlayer3D.new() - flight_sound.bus = "SFX" - flight_sound.stream = load("res://assets/sounds/swarm_flight.wav") + AudioManager.configure_player(flight_sound, "swarm_flight") flight_sound.autoplay = true - if flight_sound.stream is AudioStreamWAV: - flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD - flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames flight_sound.unit_size = 6.0 flight_sound.max_db = 0.0 flight_sound.max_distance = 45.0 - flight_sound.volume_db = -10.0 flight_sound.add_to_group("acoustic_occluded") # muffles behind walls add_child(flight_sound) flight_sound.play() diff --git a/weapons/knife.gd b/weapons/knife.gd index 7ed64f1..5d1ac48 100644 --- a/weapons/knife.gd +++ b/weapons/knife.gd @@ -23,6 +23,7 @@ func _ready() -> void: model_root = Node3D.new() add_child(model_root) _build_model() + AudioManager.configure_player(fire_sound, "knife_swing") position = default_pos func _build_model() -> void: @@ -45,7 +46,7 @@ func _build_model() -> void: fire_sound = AudioStreamPlayer3D.new() fire_sound.bus = "SFX" fire_sound.stream = AudioManager.stream_for("knife_swing", "res://assets/sounds/knife_swing.wav") - fire_sound.pitch_scale = 1.2 + fire_sound.pitch_scale = 1.05 fire_sound.volume_db = 0.0 model_root.add_child(fire_sound) diff --git a/weapons/weapon_manager.gd b/weapons/weapon_manager.gd index 5012267..ab3448d 100644 --- a/weapons/weapon_manager.gd +++ b/weapons/weapon_manager.gd @@ -10,6 +10,7 @@ var weapons: Dictionary = {} # slot_index: Node3D var canvas_layer: CanvasLayer var sub_viewport: SubViewport var vm_camera: Camera3D +var _viewmodel_skin_override := "" func _ready() -> void: set_process_input(true) @@ -21,6 +22,9 @@ func _ready() -> void: var nm = get_node_or_null("/root/NetworkManager") if nm and nm.has_signal("ladder_promoted"): nm.ladder_promoted.connect(_on_ladder_promoted) + var skin_manager = get_node_or_null("/root/SkinManager") + if skin_manager and skin_manager.has_signal("skin_changed"): + skin_manager.skin_changed.connect(_on_skin_changed) # Wait one frame for LoadoutManager to be fully ready if needed await get_tree().process_frame @@ -65,22 +69,42 @@ func _setup_viewmodel_viewport() -> void: vm_camera.near = 0.01 # viewmodel sits close to the lens; avoid clipping sub_viewport.add_child(vm_camera) - # Add a light specifically for the viewmodel in case main lights don't reach - var dir_light = DirectionalLight3D.new() - dir_light.layers = 1 << 19 - dir_light.rotation_degrees = Vector3(-60, 45, 0) - dir_light.light_energy = 0.5 - add_child(dir_light) - + # Camera-relative two-light rig. The previous light lived in world space, so + # merely turning the player could put the receiver on its dark side and + # collapse the whole weapon into a black silhouette. Parenting these to the + # viewmodel camera makes their graphic key/fill relationship stable in every + # direction, like lighting an animation cel rather than a world prop. + var key_light := DirectionalLight3D.new() + key_light.name = "ViewmodelKey" + key_light.light_cull_mask = 1 << 19 + key_light.rotation_degrees = Vector3(-34, -42, 0) + key_light.light_color = Color(1.0, 0.93, 0.84) + # The shader carries the graphic four-tone key. This real light only gives + # the narrow custom specular something to react to; the previous energy of + # 3.0 flooded every material into the same plastic middle grey. + key_light.light_energy = 0.80 + key_light.shadow_enabled = false + vm_camera.add_child(key_light) + + var fill_light := DirectionalLight3D.new() + fill_light.name = "ViewmodelFill" + fill_light.light_cull_mask = 1 << 19 + fill_light.rotation_degrees = Vector3(18, 148, 0) + fill_light.light_color = Color(0.46, 0.62, 1.0) + fill_light.light_energy = 0.045 + fill_light.shadow_enabled = false + vm_camera.add_child(fill_light) + + var _bob_timer: float = 0.0 -var _current_slide_tilt_z: float = 0.0 -var _current_slide_tilt_x: float = 0.0 -var _current_slide_offset_x: float = 0.0 -var _current_slide_offset_y: float = 0.0 var _recoil_pitch: float = 0.0 var _recoil_yaw: float = 0.0 var _target_drift_offset: Vector3 = Vector3.ZERO var _current_drift_offset: Vector3 = Vector3.ZERO +var _movement_pose_position := Vector3.ZERO +var _movement_pose_rotation := Vector3.ZERO +var _movement_pose_state := "idle" +var _movement_pose_time := 0.0 var _swap_pitch: float = 0.0 var _reload_dip: float = 0.0 @@ -102,25 +126,38 @@ func _process(_delta: float) -> void: var over_speed_factor = clampf((hspeed - 11.0) / 19.0, 0.0, 1.0) # maxes out at 30 m/s target_fov = lerpf(72.0, 80.0, over_speed_factor) - # Weapon Viewmodel Bobbing, Drift, and Slide Tilt - var is_sliding = player.has_method("get_node") and player.get_node_or_null("MovementStateMachine") and player.get_node("MovementStateMachine").current_state == "slide" - - if is_sliding: - _bob_timer = 0.0 - # Target tilt when sliding (inwards towards center, pitched slightly down) - # Assuming weapon is on right side (standard FPS), tilting left (positive Z rotation) - _current_slide_tilt_z = lerpf(_current_slide_tilt_z, deg_to_rad(15.0), 12.0 * _delta) - _current_slide_tilt_x = lerpf(_current_slide_tilt_x, 0.0, 12.0 * _delta) - # Move closer to center (negative X) and slightly down (negative Y) - _current_slide_offset_x = lerpf(_current_slide_offset_x, -0.15, 12.0 * _delta) - _current_slide_offset_y = lerpf(_current_slide_offset_y, -0.1, 12.0 * _delta) + # Authored first-person handling. The movement state changes HOW the + # character carries the gun; recoil/reload/ADS still layer afterward. + var machine = player.get_node_or_null("MovementStateMachine") + var state: String = machine.current_state if machine else "idle" + if state != _movement_pose_state: + _movement_pose_state = state + _movement_pose_time = 0.0 else: - _current_slide_tilt_z = lerpf(_current_slide_tilt_z, 0.0, 10.0 * _delta) - _current_slide_tilt_x = lerpf(_current_slide_tilt_x, 0.0, 10.0 * _delta) - _current_slide_offset_x = lerpf(_current_slide_offset_x, 0.0, 10.0 * _delta) - _current_slide_offset_y = lerpf(_current_slide_offset_y, 0.0, 10.0 * _delta) - - if player.is_on_floor() and hspeed > 1.0: + _movement_pose_time += _delta + var local_vel := camera.global_transform.basis.inverse() * player.velocity + var state_pose := _movement_pose_target( + state, machine, local_vel, hspeed) + var target_position: Vector3 = state_pose[0] + var target_rotation: Vector3 = state_pose[1] + var active_weapon = weapons.get(active_slot) + var ads_weight := 1.0 if active_weapon \ + and "is_ads" in active_weapon and active_weapon.is_ads else 0.0 + # Keep sights readable without erasing all physical response. + target_position *= lerpf(1.0, 0.28, ads_weight) + target_rotation *= lerpf(1.0, 0.24, ads_weight) + var pose_rate := _movement_pose_rate(state) + var pose_blend := 1.0 - exp(-pose_rate * _delta) + _movement_pose_position = _movement_pose_position.lerp( + target_position, pose_blend) + _movement_pose_rotation = _lerp_euler( + _movement_pose_rotation, target_rotation, pose_blend) + + if state == "slide": + _bob_timer = 0.0 + else: + if state in ["ground", "idle"] and player.is_on_floor() \ + and hspeed > 1.0: # Scale bobbing frequency by walk speed _bob_timer += _delta * 12.0 * (hspeed / 11.0) var bob_y = sin(_bob_timer) * 0.015 @@ -132,8 +169,8 @@ func _process(_delta: float) -> void: if not player.is_on_floor(): # Airborne Drift # Convert player velocity into camera's local space - var local_vel = camera.global_transform.basis.inverse() * player.velocity - _target_drift_offset = Vector3(local_vel.x * 0.004, local_vel.y * 0.004, 0) + _target_drift_offset = Vector3( + local_vel.x * 0.0025, local_vel.y * 0.0025, 0) # Clamp the drift _target_drift_offset.x = clampf(_target_drift_offset.x, -0.05, 0.05) _target_drift_offset.y = clampf(_target_drift_offset.y, -0.05, 0.05) @@ -141,17 +178,23 @@ func _process(_delta: float) -> void: _target_drift_offset = Vector3.ZERO # Smoothly apply drift so jumping/landing doesn't snap - _current_drift_offset = _current_drift_offset.lerp(_target_drift_offset, 15.0 * _delta) + _current_drift_offset = _current_drift_offset.lerp( + _target_drift_offset, 1.0 - exp(-15.0 * _delta)) vm_camera.translate_object_local(_current_drift_offset) vm_camera.rotate_object_local(Vector3.UP, deg_to_rad(-_current_drift_offset.x * 30.0)) vm_camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(_current_drift_offset.y * 30.0)) - - # Apply slide tilt and offset - if absf(_current_slide_tilt_z) > 0.001 or absf(_current_slide_offset_x) > 0.001: - vm_camera.translate_object_local(Vector3(_current_slide_offset_x, _current_slide_offset_y, 0)) - vm_camera.rotate_object_local(Vector3.FORWARD, _current_slide_tilt_z) - vm_camera.rotate_object_local(Vector3.RIGHT, _current_slide_tilt_x) + + # The targets above describe the WEAPON'S desired movement. Moving + # the isolated viewmodel camera by the inverse composes that pose + # with every weapon without overwriting weapon-local reload tweens. + vm_camera.translate_object_local(-_movement_pose_position) + vm_camera.rotate_object_local( + Vector3.RIGHT, -_movement_pose_rotation.x) + vm_camera.rotate_object_local( + Vector3.UP, -_movement_pose_rotation.y) + vm_camera.rotate_object_local( + Vector3.FORWARD, -_movement_pose_rotation.z) # Visual Recoil _recoil_pitch = lerpf(_recoil_pitch, 0.0, 15.0 * _delta) @@ -184,6 +227,90 @@ func _process(_delta: float) -> void: vm_camera.fov = lerpf(vm_camera.fov, target_fov, 10.0 * _delta) + +func _movement_pose_target(state: String, machine: Node, + local_velocity: Vector3, speed: float) -> Array[Vector3]: + var position := Vector3.ZERO + var rotation := Vector3.ZERO + var phase := _movement_pose_time + match state: + "ground", "idle": + if machine and machine.input_crouch: + position = Vector3(-0.035, -0.035, 0.018) + rotation = Vector3(3.0, -2.0, 3.0) + "air": + if local_velocity.y > 0.2: + var rise := clampf(local_velocity.y / 10.0, 0.0, 1.0) + position = Vector3(0.035, -0.045, 0.035 + 0.025 * rise) + rotation = Vector3(6.0 + 3.0 * rise, -4.0, 3.0) + else: + var fall := clampf(-local_velocity.y / 15.0, 0.0, 1.0) + position = Vector3(-0.018, 0.008 + 0.018 * fall, 0.025) + rotation = Vector3(-2.0 - 3.0 * fall, 3.0, -2.0) + rotation.z += sin(phase * 4.0) * 0.8 + "dash": + # Drop and tuck right so the burst clears the crosshair. A negative + # roll preserves the weapon's low diagonal instead of standing a + # long gun vertically through the center of the screen. + position = Vector3(0.12, -0.10, 0.080) + rotation = Vector3(8.0, 4.0, -10.0) + "slide": + position = Vector3(0.14, -0.12, 0.055) + position.y += sin(phase * 15.0) * 0.003 + rotation = Vector3(8.0, 3.0, -12.0) + "wall_run": + var side: float = machine.wall_side if machine else 0.0 + if is_zero_approx(side): + side = 1.0 + position = Vector3(-side * 0.085, -0.035, 0.060) + position.y += sin(phase * 10.0) * 0.006 + rotation = Vector3( + 7.0, -side * 6.0, -side * 15.0 + + sin(phase * 7.0) * 1.2) + "wall_cling", "wall_climb": + var side: float = machine.wall_side if machine else 1.0 + position = Vector3(-side * 0.11, -0.095, 0.10) + rotation = Vector3(15.0, -side * 9.0, -side * 18.0) + "grapple": + var pull := clampf(speed / 20.0, 0.0, 1.0) + position = Vector3(0.11, -0.060, 0.065 + 0.025 * pull) + position.y += sin(phase * 5.0) * 0.006 + rotation = Vector3(7.0, 11.0, -15.0) + _: + pass + return [position, rotation * (PI / 180.0)] + + +func _movement_pose_rate(state: String) -> float: + match state: + "dash": + return 22.0 + "slide": + return 16.0 + "wall_run", "wall_cling", "wall_climb": + return 11.0 + "grapple": + return 10.0 + "air": + return 8.0 + _: + return 9.0 + + +func _lerp_euler(from: Vector3, to: Vector3, weight: float) -> Vector3: + return Vector3( + lerp_angle(from.x, to.x, weight), + lerp_angle(from.y, to.y, weight), + lerp_angle(from.z, to.z, weight)) + + +func movement_pose_debug() -> Dictionary: + return { + "state": _movement_pose_state, + "position": _movement_pose_position, + "rotation": _movement_pose_rotation, + } + func _build_loadout() -> void: var is_auth = false if player: @@ -305,6 +432,14 @@ func _spawn_weapon(slot: int, weapon_id: String) -> void: # the world's look. No inverted-hull outline: FBX weapon models have # hard-edged normals that make the hull tear into scratchy artifacts. LevelMaterials.apply_toon_recursive(w, 0.0) + # Toon conversion gives every semantic hand piece its own material. + # Apply the selected glove/bare/android style after that split so a + # shared construction material cannot make the last fingertip color + # overwrite the palm. + for child in w.get_children(): + if child.has_meta("viewmodel_arm"): + _restyle_arm(child) + LevelMaterials.apply_viewmodel_look(w) # Set weapons to viewmodel layer _set_layer_recursive(w, 1 << 19) @@ -328,54 +463,335 @@ func _add_procedural_arms(weapon: Node3D) -> void: # the gun (model_root) and each hand (named pivots) independently. # Hand points come from WeaponGrips so the third-person model reaches for # exactly the same spots this viewmodel uses. - _build_arm(weapon, Vector3(0.25, -0.3, 0.5), WeaponGrips.GRIP, "ArmR") + var grip := WeaponGrips.GRIP + if "weapon_name" in weapon and weapon.weapon_name == "Knife": + # The knife FBX's knuckle-guard grip is offset from the gun-set's + # pistol-grip origin. Put the articulated fingers around the orange + # handle instead of leaving the hand floating behind it. + grip = Vector3(0.17, 0.02, 0.05) + _build_arm(weapon, Vector3(0.25, -0.3, 0.5), grip, "ArmR") if "weapon_name" in weapon and weapon.weapon_name != "Knife": _build_arm(weapon, Vector3(-0.25, -0.3, 0.4), WeaponGrips.SUPPORT, "ArmL") -## A first-person arm styled after the character skin: dark detached sleeve, -## glowing cuff, bare hand — instead of the old featureless blue slab. -func _build_arm(weapon: Node3D, shoulder: Vector3, hand: Vector3, arm_name: String = "") -> void: - var sleeve_mat = StandardMaterial3D.new() - sleeve_mat.albedo_color = Color(0.10, 0.11, 0.14) # near-black sleeve - sleeve_mat.roughness = 0.8 - var cuff_mat = StandardMaterial3D.new() - cuff_mat.albedo_color = Color(0.25, 0.95, 0.90) # signature teal cuff - cuff_mat.emission_enabled = true - cuff_mat.emission = Color(0.10, 0.55, 0.52) - var skin_mat = StandardMaterial3D.new() - skin_mat.albedo_color = Color(0.98, 0.88, 0.82) # skin - skin_mat.roughness = 0.9 +## Rebuild the lightweight first-person arms as soon as the local selection +## changes; each weapon owns its own animated ArmR/ArmL pivots. +func _on_skin_changed(skin_id: String) -> void: + # The signal fires immediately before the player controller mirrors the + # selection into synced_skin_id, so use its payload for this rebuild. + _viewmodel_skin_override = skin_id + for weapon in weapons.values(): + if not is_instance_valid(weapon): + continue + var found_arm := false + for child in weapon.get_children(): + if child.has_meta("viewmodel_arm"): + found_arm = true + _restyle_arm(child) + if not found_arm: + _add_procedural_arms(weapon) + LevelMaterials.apply_toon_recursive(weapon, 0.0) + LevelMaterials.apply_viewmodel_look(weapon) + _set_layer_recursive(weapon, 1 << 19) + _viewmodel_skin_override = "" - var pivot = Node3D.new() + +func _viewmodel_skin() -> PlayerSkin: + var skin_manager = get_node_or_null("/root/SkinManager") + if skin_manager == null: + return null + var skin_id: String = _viewmodel_skin_override + if skin_id.is_empty() and is_instance_valid(player) and "synced_skin_id" in player: + skin_id = player.synced_skin_id + if skin_id.is_empty(): + skin_id = skin_manager.active_skin_id + return skin_manager.get_skin(skin_id) + + +func _set_arm_piece_color(piece: MeshInstance3D, color: Color) -> void: + if piece == null or piece.mesh == null: + return + for surface in piece.mesh.get_surface_count(): + var material := piece.get_active_material(surface) + if material is ShaderMaterial: + material.set_shader_parameter("albedo_color", color) + elif material is BaseMaterial3D: + material.albedo_color = color + + +func _arm_surface_color(surface: String, skin: PlayerSkin) -> Color: + match surface: + "sleeve": + return skin.viewmodel_sleeve_color + "cuff", "hand_accent": + return skin.viewmodel_accent_color + "nail": + return skin.viewmodel_nail_color + "forearm": + return skin.viewmodel_skin_color + "hand_palm", "finger_prox": + if skin.viewmodel_hand_style in ["fingerless", "glove"]: + return skin.viewmodel_glove_color + if skin.viewmodel_hand_style == "android": + return skin.viewmodel_accent_color + return skin.viewmodel_skin_color + "fingertip": + if skin.viewmodel_hand_style == "glove": + return skin.viewmodel_glove_color + return skin.viewmodel_skin_color + return skin.viewmodel_skin_color + + +func _arm_surface_visible(surface: String, skin: PlayerSkin) -> bool: + if surface == "nail": + return skin.viewmodel_hand_style in ["bare", "fingerless"] + if surface == "hand_accent": + return skin.viewmodel_hand_style in ["fingerless", "glove", "android"] + return true + + +func _restyle_arm(pivot: Node3D) -> void: + var selected_skin := _viewmodel_skin() + if selected_skin == null: + return + var sleeve := pivot.get_node_or_null("Sleeve") as MeshInstance3D + var cuff := pivot.get_node_or_null("Cuff") as MeshInstance3D + var forearm := pivot.get_node_or_null("Forearm") as MeshInstance3D + for piece in pivot.find_children("*", "MeshInstance3D", true, false): + var surface: String = str(piece.get_meta("viewmodel_surface", "")) + if surface.is_empty(): + continue + piece.visible = _arm_surface_visible(surface, selected_skin) + _set_arm_piece_color(piece, _arm_surface_color(surface, selected_skin)) + + var arm_length: float = float(pivot.get_meta("arm_length", 0.58)) + var hand_length := minf(0.15, arm_length * 0.25) + var cuff_length := 0.045 + var clothed_length := clampf( + (arm_length - hand_length) * selected_skin.viewmodel_sleeve_ratio, + 0.10, arm_length - hand_length - cuff_length) + var bare_length := maxf(0.0, + arm_length - clothed_length - cuff_length - hand_length) + if sleeve: + sleeve.mesh = _tapered_limb_mesh( + clothed_length, Vector2(0.068, 0.052), Vector2(0.055, 0.043), + sleeve.get_active_material(0)) + sleeve.position = Vector3.ZERO + if cuff: + cuff.mesh = _tapered_limb_mesh( + cuff_length, Vector2(0.058, 0.045), Vector2(0.052, 0.040), + cuff.get_active_material(0)) + cuff.position = Vector3(0, 0, -clothed_length) + if forearm: + forearm.visible = bare_length > 0.005 + forearm.mesh = _tapered_limb_mesh( + maxf(bare_length, 0.01), Vector2(0.050, 0.039), + Vector2(0.044, 0.034), forearm.get_active_material(0)) + forearm.position = Vector3(0, 0, -(clothed_length + cuff_length)) + var hand_root := pivot.get_node_or_null("Hand") as Node3D + if hand_root: + hand_root.position = Vector3(0, 0, -(arm_length - hand_length)) + + +func _tapered_limb_mesh(length: float, near_radius: Vector2, + far_radius: Vector2, material: Material) -> ArrayMesh: + var tool := SurfaceTool.new() + tool.begin(Mesh.PRIMITIVE_TRIANGLES) + var sides := 8 + var near_points: Array[Vector3] = [] + var far_points: Array[Vector3] = [] + for i in sides: + var angle := TAU * float(i) / float(sides) + PI * 0.125 + near_points.append(Vector3( + cos(angle) * near_radius.x, sin(angle) * near_radius.y, 0)) + far_points.append(Vector3( + cos(angle) * far_radius.x, sin(angle) * far_radius.y, -length)) + for i in sides: + var next := (i + 1) % sides + tool.add_vertex(near_points[i]) + tool.add_vertex(far_points[i]) + tool.add_vertex(far_points[next]) + tool.add_vertex(near_points[i]) + tool.add_vertex(far_points[next]) + tool.add_vertex(near_points[next]) + tool.add_vertex(Vector3.ZERO) + tool.add_vertex(near_points[next]) + tool.add_vertex(near_points[i]) + tool.add_vertex(Vector3(0, 0, -length)) + tool.add_vertex(far_points[i]) + tool.add_vertex(far_points[next]) + tool.generate_normals() + var mesh := tool.commit() + if material: + mesh.surface_set_material(0, material) + return mesh + + +func _new_arm_material(color: Color, roughness: float = 0.9) -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.albedo_color = color + material.roughness = roughness + return material + + +func _tag_arm_piece(piece: MeshInstance3D, surface: String) -> void: + piece.set_meta("viewmodel_surface", surface) + + +func _add_hand_piece(parent: Node3D, piece_name: String, mesh: Mesh, + surface: String, position: Vector3 = Vector3.ZERO) -> MeshInstance3D: + var piece := MeshInstance3D.new() + piece.name = piece_name + piece.mesh = mesh + piece.position = position + _tag_arm_piece(piece, surface) + parent.add_child(piece) + return piece + + +func _capsule_between(parent: Node3D, piece_name: String, from: Vector3, + to: Vector3, radius: float, material: Material, + surface: String) -> MeshInstance3D: + var direction := to - from + var capsule := CapsuleMesh.new() + capsule.radius = radius + capsule.height = maxf(direction.length() + radius * 1.35, radius * 2.05) + capsule.radial_segments = 8 + capsule.rings = 2 + capsule.material = material + var piece := _add_hand_piece( + parent, piece_name, capsule, surface, (from + to) * 0.5) + if direction.length_squared() > 0.000001: + piece.basis = Basis(Quaternion(Vector3.UP, direction.normalized())) + return piece + + +func _build_anime_hand(parent: Node3D, hand_length: float, is_left: bool, + skin_mat: Material, glove_mat: Material, accent_mat: Material, + nail_mat: Material) -> void: + parent.name = "Hand" + # The palm is a flattened, tapered eight-plane volume. A discrete palm plus + # articulated digits gives the hand a readable anime silhouette even when + # the weapon covers half of it. + var palm := _add_hand_piece( + parent, "Palm", + _tapered_limb_mesh(hand_length * 0.62, + Vector2(0.053, 0.028), Vector2(0.046, 0.023), skin_mat), + "hand_palm") + palm.position = Vector3.ZERO + + var pad_mesh := BoxMesh.new() + pad_mesh.size = Vector3(0.068, 0.007, hand_length * 0.30) + pad_mesh.material = accent_mat + _add_hand_piece(parent, "BackhandPanel", pad_mesh, "hand_accent", + Vector3(0, 0.028, -hand_length * 0.34)) + + var finger_x := [-0.034, -0.012, 0.012, 0.034] + var finger_scale := [0.82, 1.0, 0.96, 0.76] + for i in 4: + var x: float = finger_x[i] + var scale_f: float = finger_scale[i] + var base := Vector3(x, -0.002, -hand_length * 0.52) + var joint := Vector3( + x, -0.012, -hand_length * (0.70 + 0.08 * scale_f)) + var tip := Vector3( + x, -0.047, -hand_length * (0.79 + 0.08 * scale_f)) + var radius := 0.0105 - float(abs(i - 1)) * 0.0008 + _capsule_between(parent, "Finger%dProx" % i, base, joint, + radius, glove_mat, "finger_prox") + _capsule_between(parent, "Finger%dTip" % i, joint, tip, + radius * 0.92, skin_mat, "fingertip") + var nail_mesh := BoxMesh.new() + nail_mesh.size = Vector3(radius * 1.15, 0.0035, radius * 1.7) + nail_mesh.material = nail_mat + _add_hand_piece(parent, "Nail%d" % i, nail_mesh, "nail", + tip + Vector3(0, 0.006, 0.002)) + + # Opposed thumb: two separate phalanges angle across the grip instead of + # reading as a fifth parallel tube. + var thumb_side := 1.0 if is_left else -1.0 + var thumb_base := Vector3( + thumb_side * 0.043, -0.002, -hand_length * 0.23) + var thumb_joint := Vector3( + thumb_side * 0.069, -0.015, -hand_length * 0.46) + var thumb_tip := Vector3( + thumb_side * 0.049, -0.047, -hand_length * 0.62) + _capsule_between(parent, "ThumbProx", thumb_base, thumb_joint, + 0.0125, glove_mat, "finger_prox") + _capsule_between(parent, "ThumbTip", thumb_joint, thumb_tip, + 0.0115, skin_mat, "fingertip") + + +func _build_arm(weapon: Node3D, shoulder: Vector3, hand: Vector3, arm_name: String = "") -> void: + var selected_skin := _viewmodel_skin() + var sleeve_color := selected_skin.viewmodel_sleeve_color \ + if selected_skin else Color(0.16, 0.34, 0.72) + var accent_color := selected_skin.viewmodel_accent_color \ + if selected_skin else Color(0.28, 0.72, 1.0) + var hand_color := selected_skin.viewmodel_skin_color \ + if selected_skin else Color(0.96, 0.80, 0.72) + var glove_color := selected_skin.viewmodel_glove_color \ + if selected_skin else Color(0.10, 0.11, 0.16) + var nail_color := selected_skin.viewmodel_nail_color \ + if selected_skin else Color(0.92, 0.68, 0.72) + var sleeve_ratio := selected_skin.viewmodel_sleeve_ratio if selected_skin else 0.68 + + var sleeve_mat := _new_arm_material(sleeve_color, 0.82) + var cuff_mat := _new_arm_material(accent_color, 0.78) + var skin_mat := _new_arm_material(hand_color, 0.94) + var glove_mat := _new_arm_material(glove_color, 0.88) + var nail_mat := _new_arm_material(nail_color, 0.74) + + var pivot := Node3D.new() if arm_name != "": pivot.name = arm_name + pivot.set_meta("viewmodel_arm", true) + pivot.set_meta("arm_length", shoulder.distance_to(hand)) pivot.position = shoulder pivot.look_at_from_position(shoulder, hand, Vector3.UP) - var sleeve = MeshInstance3D.new() - var sleeve_mesh = BoxMesh.new() - sleeve_mesh.size = Vector3(0.075, 0.075, 0.46) - sleeve_mesh.material = sleeve_mat - sleeve.mesh = sleeve_mesh - sleeve.position.z = -0.27 + var arm_length := shoulder.distance_to(hand) + var hand_length := minf(0.15, arm_length * 0.25) + var cuff_length := 0.045 + var clothed_length := clampf( + (arm_length - hand_length) * sleeve_ratio, + 0.10, arm_length - hand_length - cuff_length) + var bare_length := maxf(0.0, + arm_length - clothed_length - cuff_length - hand_length) + + var sleeve := MeshInstance3D.new() + sleeve.name = "Sleeve" + sleeve.mesh = _tapered_limb_mesh( + clothed_length, Vector2(0.068, 0.052), Vector2(0.055, 0.043), + sleeve_mat) + _tag_arm_piece(sleeve, "sleeve") pivot.add_child(sleeve) - var cuff = MeshInstance3D.new() - var cuff_mesh = BoxMesh.new() - cuff_mesh.size = Vector3(0.085, 0.085, 0.05) - cuff_mesh.material = cuff_mat - cuff.mesh = cuff_mesh - cuff.position.z = -0.52 + var cuff := MeshInstance3D.new() + cuff.name = "Cuff" + cuff.mesh = _tapered_limb_mesh( + cuff_length, Vector2(0.058, 0.045), Vector2(0.052, 0.040), + cuff_mat) + cuff.position.z = -clothed_length + _tag_arm_piece(cuff, "cuff") pivot.add_child(cuff) - var hand_box = MeshInstance3D.new() - var hand_mesh = BoxMesh.new() - hand_mesh.size = Vector3(0.06, 0.055, 0.12) - hand_mesh.material = skin_mat - hand_box.mesh = hand_mesh - hand_box.position.z = -0.60 - pivot.add_child(hand_box) + var forearm := MeshInstance3D.new() + forearm.name = "Forearm" + forearm.mesh = _tapered_limb_mesh( + maxf(bare_length, 0.01), Vector2(0.050, 0.039), + Vector2(0.044, 0.034), skin_mat) + forearm.position.z = -(clothed_length + cuff_length) + forearm.visible = bare_length > 0.005 + _tag_arm_piece(forearm, "forearm") + pivot.add_child(forearm) + + var hand_root := Node3D.new() + hand_root.position.z = -(arm_length - hand_length) + pivot.add_child(hand_root) + _build_anime_hand( + hand_root, hand_length, arm_name == "ArmL", + skin_mat, glove_mat, cuff_mat, nail_mat) weapon.add_child(pivot)